Merge branch 'stable-2.14' * stable-2.14: Preserve line-endings in inline editing ChangeOwnerIT: Move method to grant permissions on label to base class Change-Id: Ib3a39f2837bb584eb327c303c6ee2c07077a4e86
diff --git a/.bazelproject b/.bazelproject index 41bb27f..e3a7a9c 100644 --- a/.bazelproject +++ b/.bazelproject
@@ -18,3 +18,6 @@ java_language_level: 8 workspace_type: java + +build_flags: + --javacopt=-g
diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 172bb97..d542a0b 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs
@@ -2,8 +2,11 @@ org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate @@ -64,12 +67,14 @@ org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error org.eclipse.jdt.core.compiler.problem.nullReference=warning org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore @@ -92,6 +97,9 @@ org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=warning org.eclipse.jdt.core.compiler.problem.unnecessaryElse=warning org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore @@ -113,4 +121,4 @@ org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning org.eclipse.jdt.core.compiler.processAnnotations=enabled -org.eclipse.jdt.core.compiler.source=1.8 \ No newline at end of file +org.eclipse.jdt.core.compiler.source=1.8
diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs index 3d5f5f6..18c15dd 100644 --- a/.settings/org.eclipse.jdt.ui.prefs +++ b/.settings/org.eclipse.jdt.ui.prefs
@@ -2,4 +2,4 @@ org.eclipse.jdt.ui.ignorelowercasenames=true org.eclipse.jdt.ui.ondemandthreshold=99 org.eclipse.jdt.ui.staticondemandthreshold=99 -org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/> +org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates><template autoinsert\="true" context\="gettercomment_context" deleted\="false" description\="Comment for getter method" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.gettercomment" name\="gettercomment">/**\n * @return the ${bare_field_name}\n */</template><template autoinsert\="true" context\="settercomment_context" deleted\="false" description\="Comment for setter method" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.settercomment" name\="settercomment">/**\n * @param ${param} the ${bare_field_name} to set\n */</template><template autoinsert\="true" context\="constructorcomment_context" deleted\="false" description\="Comment for created constructors" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.constructorcomment" name\="constructorcomment">/**\n * ${tags}\n */</template><template autoinsert\="false" context\="filecomment_context" deleted\="false" description\="Comment for created Java files" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.filecomment" name\="filecomment">// Copyright (C) ${year} The Android Open Source Project\n//\n// Licensed under the Apache License, Version 2.0 (the "License");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http\://www.apache.org/licenses/LICENSE-2.0\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an "AS IS" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.</template><template autoinsert\="true" context\="typecomment_context" deleted\="false" description\="Comment for created types" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.typecomment" name\="typecomment">/**\n * @author ${user}\n *\n * ${tags}\n */</template><template autoinsert\="true" context\="fieldcomment_context" deleted\="false" description\="Comment for fields" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.fieldcomment" name\="fieldcomment">/**\n * \n */</template><template autoinsert\="true" context\="methodcomment_context" deleted\="false" description\="Comment for non-overriding methods" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.methodcomment" name\="methodcomment">/**\n * ${tags}\n */</template><template autoinsert\="true" context\="overridecomment_context" deleted\="false" description\="Comment for overriding methods" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.overridecomment" name\="overridecomment">/* (non-Javadoc)\n * ${see_to_overridden}\n */</template><template autoinsert\="true" context\="delegatecomment_context" deleted\="false" description\="Comment for delegate methods" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.delegatecomment" name\="delegatecomment">/**\n * ${tags}\n * ${see_to_target}\n */</template><template autoinsert\="false" context\="newtype_context" deleted\="false" description\="Newly created files" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.newtype" name\="newtype">${filecomment}\n\n${package_declaration}\n\n${typecomment}\n${type_declaration}</template><template autoinsert\="false" context\="classbody_context" deleted\="false" description\="Code in new class type bodies" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.classbody" name\="classbody"/><template autoinsert\="true" context\="interfacebody_context" deleted\="false" description\="Code in new interface type bodies" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.interfacebody" name\="interfacebody">\n</template><template autoinsert\="true" context\="enumbody_context" deleted\="false" description\="Code in new enum type bodies" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.enumbody" name\="enumbody">\n</template><template autoinsert\="true" context\="annotationbody_context" deleted\="false" description\="Code in new annotation type bodies" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.annotationbody" name\="annotationbody">\n</template><template autoinsert\="false" context\="catchblock_context" deleted\="false" description\="Code in new catch blocks" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.catchblock" name\="catchblock">${exception_var}.printStackTrace();</template><template autoinsert\="false" context\="methodbody_context" deleted\="false" description\="Code in created method stubs" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.methodbody" name\="methodbody">${body_statement}</template><template autoinsert\="false" context\="constructorbody_context" deleted\="false" description\="Code in created constructor stubs" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.constructorbody" name\="constructorbody">${body_statement}</template><template autoinsert\="true" context\="getterbody_context" deleted\="false" description\="Code in created getters" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.getterbody" name\="getterbody">return ${field};</template><template autoinsert\="true" context\="setterbody_context" deleted\="false" description\="Code in created setters" enabled\="true" id\="org.eclipse.jdt.ui.text.codetemplates.setterbody" name\="setterbody">${field} \= ${param};</template></templates>
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt index f64f739..3d132d3 100644 --- a/Documentation/access-control.txt +++ b/Documentation/access-control.txt
@@ -497,7 +497,7 @@ Deletion of references is also possible if `Push` with the force option is granted, however that includes the permission to fast-forward and -force-update references to exiting and new commits. Being able to push +force-update references to existing and new commits. Being able to push references for new commits is bad if bypassing of code review must be prevented. @@ -559,8 +559,6 @@ configuration. Users who are members of an owner group can: * Change the project description -* Create a branch via the ssh command link:cmd-create-branch.html['create-branch'] -* Create/delete a branch through the web UI * Grant/revoke any access rights, including `Owner` To get SSH branch access project owners must grant an access right to a group @@ -850,6 +848,15 @@ Note that this permission is named `submitAs` in the `project.config` file. +[[category_view_private_changes]] +=== View Private Changes + +This category permits users to view all private changes. + +The change owner and any explicitly added reviewers can always see +private changes (even without having the `View Private Changes` access +right assigned). + [[category_view_drafts]] === View Drafts
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index c67b628..a7e4c94 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt
@@ -420,8 +420,14 @@ the "Switch Account" link is displayed next to "Sign Out". + When `auth.type` does not normally enable this URL administrators may -set this to `login/` or `$canonicalWebUrl/login`, allowing users to -begin a new web session. +set this to `login/`, allowing users to begin a new web session. This value +is used as an href in PolyGerrit and the GWT UI, so absolute URLs like +`https://someotherhost/login` work as well. ++ +If a ${path} parameter is included, then PolyGerrit will substitute the +currently viewed path in the link. Be aware that this path will include +a leading slash, so a value like this might be appropriate: `/login${path}`. +Note: in the GWT UI this substitution for ${path} is *always* `/`. [[auth.cookiePath]]auth.cookiePath:: + @@ -964,10 +970,10 @@ [[capability.administrateServer]]capability.administrateServer:: + Names of groups of users that are allowed to exercise the -administrateServer capability, in addition to those listed in +`administrateServer` capability, in addition to those listed in All-Projects. Configuring this option can be a useful fail-safe to recover a server in the event an administrator removed all -groups from the administrateServer capability, or to ensure that +groups from the `administrateServer` capability, or to ensure that specific groups always have administration capabilities. + ---- @@ -981,7 +987,16 @@ is logged and the server will continue normal startup. + If not specified (default), only the groups listed by All-Projects -may use the administrateServer capability. +may use the `administrateServer` capability. + +[[capability.makeFirstUserAdmin]]capability.makeFirstUserAdmin:: ++ +Whether the first user that logs in to the Gerrit server should +automatically be added to the administrator group and hence get the +`administrateServer` capability assigned. This is useful to bootstrap +the authentication database. ++ +Default is true. [[change]] @@ -1015,7 +1030,7 @@ + If 0 the update polling is disabled. + -Default is 30 seconds. +Default is 5 minutes. [[change.allowBlame]]change.allowBlame:: + @@ -1127,6 +1142,16 @@ Default is "Reply and score". In the user interface it becomes "Reply and score (Shortcut: a)". +[[change.robotCommentSizeLimit]]change.robotCommentSizeLimit:: ++ +Maximum allowed size of a robot comment that will be accepted. Robot comments +which exceed the indicated size will be rejected on addition. The specified +value is interpreted as the maximum size in bytes of the JSON representation of +the robot comment. Common unit suffixes of 'k', 'm', or 'g' are supported. +Zero or negative values allow robot comments of unlimited size. ++ +The default limit is 1024kB. + [[changeCleanup]] === Section changeCleanup @@ -1717,7 +1742,7 @@ If `true` enable the automatic mixed mode (see link:http://www.h2database.com/html/features.html#auto_mixed_mode[Automatic Mixed Mode]). This enables concurrent access to the embedded H2 database from command line -utils (e.g. RebuildNoteDb). +utils (e.g. MigrateToNoteDb). + Default is `false`. @@ -2655,6 +2680,19 @@ + Defaults to 1024. +[[index.reindexAfterRefUpdate]]index.reindexAfterRefUpdate:: ++ +Whether to reindex all affected open changes after a ref is updated. This +includes reindexing all open changes to recompute the "mergeable" bit every time +the destination branch moves, as well as reindexing changes to take into account +new project configuration (e.g. label definitions). ++ +Leaving this enabled may result in fresher results, but may cause performance +problems if there are lots of open changes on a project whose branches advance +frequently. ++ +Defaults to true. + [[index.autoReindexIfStale]]index.autoReindexIfStale:: + Whether to automatically check if a document became stale in the index @@ -3880,15 +3918,25 @@ Defaults to an empty string which adds <<sendemail.from,sendemail.from>> as Reply-To if inbound email is enabled and the review's author otherwise. +[[sendemail.allowTLD]]sendemail.allowTLD:: ++ +List of custom TLDs to allow sending emails to in addition to those specified +in the link:http://data.iana.org/TLD/[IANA list]. ++ +Defaults to an empty list, meaning no additional TLDs are allowed. + [[site]] === Section site [[site.allowOriginRegex]]site.allowOriginRegex:: + List of regular expressions matching origins that should be permitted -to use the Gerrit REST API to read content. These should be trusted -applications as the sites may be able to use the user's credentials. -Only applies to GET and HEAD requests. +to use the full Gerrit REST API. These should be trusted applications, +as the sites may be able to use the user's credentials. Applies to +all requests, including state changing methods (PUT, DELETE, POST). ++ +Expressions should not require trailing slash. For example a valid +pattern might be `https://build-status[.]example[.]com`. + By default, unset, denying all cross-origin requests.
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt index 34f39c8..0183534 100644 --- a/Documentation/config-project-config.txt +++ b/Documentation/config-project-config.txt
@@ -212,6 +212,12 @@ values are 'fast forward only', 'merge if necessary', 'rebase if necessary', 'merge always' and 'cherry pick'. The default is 'merge if necessary'. +- 'matchAuthorToCommitterDate': Defines whether to the author date will be changed to match the +submitter date upon submit, so that git log shows when the change was submitted instead of when the +author last committed. Valid values are 'true', 'false', or 'INHERIT'. The default is 'INHERIT'. +This option only takes effect in submit strategies which already modify the commit, i.e. +Cherry Pick, Rebase Always, and (perhaps) Rebase If Necessary. + Merge strategy @@ -291,6 +297,20 @@ 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. + +[[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. + +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`. [[file-groups]] == The file +groups+
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt index 3fd0c91..0ec8395 100644 --- a/Documentation/database-setup.txt +++ b/Documentation/database-setup.txt
@@ -235,7 +235,7 @@ It needs to be stored in the 'lib' folder of the review site. In the following sample database section it is assumed that HANA is running on -the host 'hana.host' with the instance number 00 where a schema/user GERRIT2 +the host 'hana.host' and listening on port '4242' where a schema/user GERRIT2 was created: In $site_path/etc/gerrit.config: @@ -243,8 +243,23 @@ ---- [database] type = hana - instance = 00 hostname = hana.host + port = 4242 + username = GERRIT2 + +---- + +In order to configure a specific database in a multi-database environment (MDC) +the database name has to be specified additionally: + +In $site_path/etc/gerrit.config: + +---- +[database] + type = hana + hostname = hana.host + database = tdb1 + port = 4242 username = GERRIT2 ----
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt index 823424e..e4a7218 100644 --- a/Documentation/dev-contributing.txt +++ b/Documentation/dev-contributing.txt
@@ -154,8 +154,8 @@ To format Java source code, Gerrit uses the link:https://github.com/google/google-java-format[`google-java-format`] tool (version 1.3), and to format Bazel BUILD and WORKSPACE files the -link:https://github.com/bazelbuild/buildifier[`buildifier`] tool. These -tools automatically apply format according to the style guides; this +link:https://github.com/bazelbuild/buildifier[`buildifier`] tool (version 0.4.5). +These tools automatically apply format according to the style guides; this streamlines code review by reducing the need for time-consuming, tedious, and contentious discussions about trivial issues like whitespace. @@ -341,7 +341,20 @@ We have created a link:https://bugs.chromium.org/p/gerrit/issues/list?can=2&q=label%3AStarterProject[StarterProject] category in the issue tracker and try to assign easy hack projects to it. If in -doubt, do not hesitate to ask on the developer mailing list. +doubt, do not hesitate to ask on the developer +link:https://groups.google.com/forum/#!forum/repo-discuss[mailing list]. + +=== Upgrading Libraries + +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. +An exception to this rule is that right after a new Gerrit release was branched +off, all libraries should be upgraded to the latest version to prevent Gerrit +from falling behind. Doing those upgrades should conclude at the latest two +months after the branch was cut. This should happen on the master branch to ensure +that they are vetted long enough before they go into a release and we can be sure +that the update doesn't introduce a regression. GERRIT ------
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt index 39fa333..9743779 100644 --- a/Documentation/dev-eclipse.txt +++ b/Documentation/dev-eclipse.txt
@@ -10,8 +10,8 @@ [[setup]] == Project Setup -In your Eclipse installation's `eclipse.ini` file, add the following line in -the `vmargs` section: +In your Eclipse installation's link:https://wiki.eclipse.org/Eclipse.ini[`eclipse.ini`] file, +add the following line in the `vmargs` section: ---- -DmaxCompiledUnitsAtOnce=10000 @@ -30,7 +30,8 @@ AutoAnnotation_Commands_named cannot be resolved to a type ---- -In Eclipse, choose 'Import existing project' and select the `gerrit` project +First, generate the Eclipse project by running the `tools/eclipse/project.py` script. +Then, in Eclipse, choose 'Import existing project' and select the `gerrit` project from the current working directory. Expand the `gerrit` project, right-click on the `eclipse-out` folder, select
diff --git a/Documentation/dev-note-db.txt b/Documentation/dev-note-db.txt index dd3b316..bacbeda 100644 --- a/Documentation/dev-note-db.txt +++ b/Documentation/dev-note-db.txt
@@ -32,8 +32,8 @@ - Storing the rest of account data is a work in progress. - Storing group data is a work in progress. -To match the current configuration of `googlesource.com`, paste the following -config snippet in your `gerrit.config`: +To use a configuration similar to the current state of `googlesource.com`, paste +the following config snippet in your `gerrit.config`: ---- [noteDb "changes"] @@ -43,6 +43,12 @@ disableReviewDb = true ---- +This is the configuration that will be produced if you enable experimental +NoteDb support on a new site with `init`. + +`googlesource.com` additionally uses `fuseUpdates = true`, because its repo +backend supports atomic multi-ref transactions. Native JGit doesn't, so setting +this option on a dev server would fail. For an example NoteDb change, poke around at this one: ---- @@ -93,33 +99,38 @@ inaccurate results, and writing to NoteDb would compound the problem. + Thus it is up to an admin of a previously-ReviewDb site to ensure MigratePrimaryStorage has been run for all changes. Note that the current - implementation of the `rebuild-note-db` program does not do this. + + implementation of the `migrate-to-note-db` program does not do this. + In this phase, it would be possible to delete the Changes tables out from under a running server with no effect. +- `noteDb.changes.fuseUpdates=true`: Code and meta updates within a single + repository are fused into a single atomic `BatchRefUpdate`, so they either + all succeed or all fail. This mode is only possible on a backend that + supports atomic ref updates, which notably excludes the default file repository + backend. [[migration]] == Migration Once configuration options are set, migration to NoteDb is primarily -accomplished by running the `rebuild-note-db` program. Currently, this program +accomplished by running the `migrate-to-note-db` program. Currently, this program bulk copies ReviewDb data into NoteDb, but leaves primary storage of these changes in ReviewDb, so the site is runnable with `noteDb.changes.{write,read}=true`, but ReviewDb is still required. -Eventually, `rebuild-note-db` will set primary storage to NoteDb for all +Eventually, `migrate-to-note-db` will set primary storage to NoteDb for all changes by default, so a site will be able to stop using ReviewDb for changes immediately after a successful run. There is code in `PrimaryStorageMigrator.java` to migrate individual changes from NoteDb primary to ReviewDb primary. This code is not intended to be used except in the event of a critical bug in NoteDb primary changes in production. -It will likely never be used by `rebuild-note-db`, and in fact it's not -recommended to run `rebuild-note-db` until the code is stable enough that the +It will likely never be used by `migrate-to-note-db`, and in fact it's not +recommended to run `migrate-to-note-db` until the code is stable enough that the reverse migration won't be necessary. === Zero-Downtime Multi-Master Migration -Single-master Gerrit sites can use `rebuild-note-db` on an offline site to +Single-master Gerrit sites can use `migrate-to-note-db` on an offline site to rebuild NoteDb, but this doesn't work in a zero-downtime environment like googlesource.com. @@ -135,3 +146,34 @@ - Run a Flume to migrate all existing changes to NoteDb primary. (Also closed-source, but basically just a wrapper around `PrimaryStorageMigrator`.) - Turn off access to ReviewDb changes tables. + +== Configuration + +This section describes general configuration for the NoteDb backend that is not +directly related to the migration process. These options will continue to be +supported after the migration is complete and the migration-related options are +obsolete. + +[[noteDb.retryMaxWait]]noteDb.retryMaxWait:: ++ +Maximum time to wait between attempts to retry update operations when one +attempt fails due to contention (aka lock failure) on the underlying ref +storage. Operations are retried with exponential backoff, plus some random +jitter, until the interval reaches this limit. After that, retries continue to +occur after a fixed timeout (plus jitter), up to +link:#noteDb.retryTimeout[`noteDb.retryTimeout`]. ++ +Only applicable when `noteDb.changes.fuseUpdates=true`. ++ +Defaults to 5 seconds; unit suffixes are supported, and assumes milliseconds if +not specified. + +[[noteDb.retryTimeout]]noteDb.retryTimeout:: ++ +Total timeout for retrying update operations when one attempt fails due to +contention (aka lock failure) on the underlying ref storage. ++ +Only applicable when `noteDb.changes.fuseUpdates=true`. ++ +Defaults to 20 seconds; unit suffixes are supported, and assumes milliseconds if +not specified.
diff --git a/Documentation/dev-plugin-pg-styling.txt b/Documentation/dev-plugin-pg-styling.txt new file mode 100644 index 0000000..618d984 --- /dev/null +++ b/Documentation/dev-plugin-pg-styling.txt
@@ -0,0 +1,61 @@ += Gerrit Code Review - PolyGerrit Plugin Styling + +CAUTION: Work in progress. Hard hat area. + +This document will be populated with details along with implementation. + +link:https://groups.google.com/d/topic/repo-discuss/vb8WJ4m0hK0/discussion[Join the discussion.] + +== Plugin styles + +Plugins may provide link:https://www.polymer-project.org/2.0/docs/devguide/style-shadow-dom#style-modules[Polymer style modules] for UI CSS-based customization. + +PolyGerrit UI implements number of styling endpoints, which apply CSS mixins link:https://tabatkins.github.io/specs/css-apply-rule/[using @apply] to its direct contents. + +NOTE: Only items (ie CSS properties and mixin targets) documented here are guaranteed to work in the long term, since they are covered by integration tests. + +When there is a need to add new property or endpoint, please link:https://bugs.chromium.org/p/gerrit/issues/entry?template=PolyGerrit%20Issue[file a bug] stating your usecase to track and maintain for future releases. + +Plugin should be html-based and imported following PolyGerrit's link:dev-plugins-pg.html#loading[dev guide]. + +Plugin should provide Style Module, for example: + +``` html + <dom-module id="some-style"> + <style> + :root { + --css-mixin-name: { + property: value; + } + } + </style> + </dom-module> +``` + +Plugin should register style module with a styling endpoint using `Plugin.prototype.registerStyleModule(endpointName, styleModuleName)`, for example: + +``` js + Gerrit.install(function(plugin) { + plugin.registerStyleModule('some-endpoint', 'some-style'); + }); +``` + +== Available styling endpoints +=== change-metadata +Following custom css mixins are recognized: + +* `--change-metadata-assignee` ++ +is applied to `gr-change-metadata section.assignee` +* `--change-metadata-label-status` ++ +is applied to `gr-change-metadata section.labelStatus` +* `--change-metadata-strategy` ++ +is applied to `gr-change-metadata section.strategy` +* `--change-metadata-topic` ++ +is applied to `gr-change-metadata section.topic` + +Following CSS properties have link:https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html[long-term support via integration test]: + +* `display` ++ +can be set to `none` to hide a section.
diff --git a/Documentation/dev-plugins-pg.txt b/Documentation/dev-plugins-pg.txt new file mode 100644 index 0000000..77748a4 --- /dev/null +++ b/Documentation/dev-plugins-pg.txt
@@ -0,0 +1,25 @@ += Gerrit Code Review - PolyGerrit Plugin Development + +CAUTION: Work in progress. Hard hat area. + +This document will be populated with details along with implementation. + +link:https://groups.google.com/d/topic/repo-discuss/vb8WJ4m0hK0/discussion[Join the discussion.] + +[[loading]] +== Plugin loading and initialization + +link:https://gerrit-review.googlesource.com/Documentation/js-api.html#_entry_point[Entry point] for the plugin and the loading method is based on link:http://w3c.github.io/webcomponents/spec/imports/[HTML Imports] spec. + +* Plugin provides index.html, similar to link:https://gerrit-review.googlesource.com/Documentation/dev-plugins.html#deployment[.js Web UI plugins] +* index.html contains a `dom-module` tag with a script that uses `Gerrit.install()`. +* PolyGerrit imports index.html along with all required resources defined in it (fonts, styles, etc) +* For standalone plugins, the entry point file is a `pluginname.html` file located in `gerrit-site/plugins` folder, where pluginname is an alphanumeric plugin name. + +Here's a sample `myplugin.html`: + +``` html +<dom-module id="my-plugin"> + <script> + Gerrit.install(function() { console.log('Ready.'); }); + </script> +</dom-module> +```
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt index 3692dfb7..00c5dc9 100644 --- a/Documentation/dev-plugins.txt +++ b/Documentation/dev-plugins.txt
@@ -712,6 +712,99 @@ } ==== +[[command_options]] +=== Command Options === + +Plugins can provide additional options for each of the gerrit ssh and the +REST API commands by implementing the DynamicBean interface and registering +it to a command class name in the plugin module's `configure()` method. The +plugin's name will be prepended to the name of each @Option annotation found +on the DynamicBean object provided by the plugin. The example below shows a +plugin that adds an option to log a value from the gerrit 'ban-commits' +ssh command. + +[source, java] +---- +public class SshModule extends AbstractModule { + private static final Logger log = LoggerFactory.getLogger(SshModule.class); + + @Override + protected void configure() { + bind(DynamicOptions.DynamicBean.class) + .annotatedWith(Exports.named( + com.google.gerrit.sshd.commands.BanCommitCommand.class)) + .to(BanOptions.class); + } + + public static class BanOptions implements DynamicOptions.DynamicBean { + @Option(name = "--log", aliases = { "-l" }, usage = "Say Hello in the Log") + private void parse(String arg) { + log.error("Say Hello in the Log " + arg); + } + } +---- + +[[query_attributes]] +=== Query Attributes === + +Plugins can provide additional attributes to be returned in Gerrit queries by +implementing the ChangeAttributeFactory interface and registering it to the +ChangeQueryProcessor.ChangeAttributeFactory class in the plugin module's +'configure()' method. The new attribute(s) will be output under a "plugin" +attribute in the change query output. + +The example below shows a plugin that adds two attributes ('exampleName' and +'changeValue'), to the change query output. + +[source, java] +---- +public class Module extends AbstractModule { + @Override + protected void configure() { + bind(ChangeAttributeFactory.class) + .annotatedWith(Exports.named("example")) + .to(AttributeFactory.class); + } +} + +public class AttributeFactory implements ChangeAttributeFactory { + + public class PluginAttribute extends PluginDefinedInfo { + public String exampleName; + public String changeValue; + + public PluginAttribute(ChangeData c) { + this.exampleName = "Attribute Example"; + this.changeValue = Integer.toString(c.getId().get()); + } + } + + @Override + public PluginDefinedInfo create(ChangeData c, ChangeQueryProcessor qp, String plugin) { + return new PluginAttribute(c); + } +} +---- + +Example +---- + +ssh -p 29418 localhost gerrit query "change:1" --format json + +Output: + +{ + "url" : "http://localhost:8080/1", + "plugins" : [ + { + "name" : "myplugin-name", + "exampleName" : "Attribute Example", + "changeValue" : "1" + } + ], + ... +} +---- [[simple-configuration]] == Simple Configuration in `gerrit.config` @@ -912,12 +1005,15 @@ Plugins can refer to groups so that when they are renamed, the project config will also be updated in this section. The proper format to use is -the string representation of a GroupReference, as shown below. +the same as for any other group reference in the `project.config`, as shown below. ---- -Group[group_name / group_uuid] +group group_name ---- +The file `groups` must also contains the mapping of the group name and its UUID, +refer to link:config-project-config.html#file-groups[file groups] + [[project-specific-configuration]] == Project Specific Configuration in own config file @@ -1217,6 +1313,7 @@ @Override public void onPluginLoad() { Plugin.get().panel(GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK, + "my_panel_name", new Panel.EntryPoint() { @Override public void onLoad(Panel panel) { @@ -1228,6 +1325,23 @@ } ---- +Change Screen panel ordering may be specified in the +project config. Values may be either "plugin name" or +"plugin name"."panel name". +Panels not specified in the config will be added +to the end in load order. Panels specified in the config that +are not found will be ignored. + +Example config: +---- +[extension-panels "CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK"] + panel = helloworld.change_id + panel = myotherplugin + panel = myplugin.my_panel_name +---- + + + [[actions]] === Actions @@ -2193,6 +2307,10 @@ FileHistoryWebLinks will appear on the access rights screen. +If a `get*WebLink` implementation returns `null`, the link will be omitted. This +allows the plugin to selectively "enable" itself on a per-project/branch/file +basis. + [[lfs-extension]] == LFS Storage Plugins
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt index d43c863..5f95cb3 100644 --- a/Documentation/dev-release-deploy-config.txt +++ b/Documentation/dev-release-deploy-config.txt
@@ -38,15 +38,21 @@ * Generate and publish a PGP key + +A PGP key is needed to be able to sign the release artifacts before +the upload to Maven Central, and to sign the release announcement email. ++ Generate and publish a PGP key as described in link:http://central.sonatype.org/pages/working-with-pgp-signatures.html[ -Working with PGP Signatures]. +Working with PGP Signatures]. In addition to the keyserver mentioned +there it is recommended to also publish the key to the +link:https://keyserver.ubuntu.com/[Ubuntu key server]. + Please be aware that after publishing your public key it may take a while until it is visible to the Sonatype server. + -The PGP key is needed to be able to sign the artifacts before the -upload to Maven Central. +Add an entry for the public key in the +link:https://gerrit.googlesource.com/homepage/+/md-pages/releases/public-keys.md[key list] +on the homepage. + The PGP passphrase can be put in `~/.m2/settings.xml`: +
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt index 2a857b2..ce89aa2 100644 --- a/Documentation/dev-release.txt +++ b/Documentation/dev-release.txt
@@ -16,8 +16,8 @@ == Gerrit Release Type Here are some guidelines on release approaches depending on the -type of release you want to make (`stable-fix`, `stable`, `RC0`, -`RC1`...). +type of release you want to make (`stable-fix`, `stable`, `rc0`, +`rc1`...). [[stable]] === Stable @@ -27,19 +27,19 @@ * Propose the release with any plans/objectives to the mailing list -* Create a Gerrit `RC0` +* Create a Gerrit `rc0` -* If needed create a Gerrit `RC1` +* If needed create a Gerrit `rc1` [NOTE] You may let in a few features to this release -* If needed create a Gerrit `RC2` +* If needed create a Gerrit `rc2` [NOTE] There should be no new features in this release, only bug fixes -* Finally create the `stable` release (no `RC`) +* Finally create the `stable` release (no `rc`) === Stable-Fix @@ -75,7 +75,6 @@ To create a Gerrit release the following steps have to be done: -. link:#subproject[Release Subprojects] . link:#build-gerrit[Build the Gerrit Release] . link:#publish-gerrit[Publish the Gerrit Release] .. link:#publish-to-maven-central[Publish the Gerrit artifacts to Maven Central] @@ -90,34 +89,10 @@ . link:#merge-stable[Merge `stable` into `master`] -[[subproject]] -=== Release Subprojects - -The subprojects to be released are: - -* `gwtjsonrpc` -* `gwtorm` -* `prolog-cafe` - -For each subproject do: - -* Check the dependency to the Subproject in the Gerrit parent `pom.xml`: -+ -If a `SNAPSHOT` version of the subproject is referenced the subproject -needs to be released so that Gerrit can reference a released version of -the subproject. - -* link:dev-release-subproject.html#make-snapshot[Make a snapshot and test it] -* link:dev-release-subproject.html#prepare-release[Prepare the Release] -* link:dev-release-subproject.html#publish-release[Publish the Release] - -* Update the `artifact`, `sha1`, and `src_sha1` values in the `maven_jar` -for the Subproject in `WORKSPACE` to the released version. - [[update-versions]] === Update Versions and Create Release Tag -Before doing the release build, the `GERRIT_VERSION` in the `VERSION` +Before doing the release build, the `GERRIT_VERSION` in the `version.bzl` file must be updated, e.g. change it from `2.5-SNAPSHOT` to `2.5`. In addition the version must be updated in a number of pom.xml files. @@ -351,28 +326,20 @@ [[announce]] ==== Announce on Mailing List -* Send an email to the mailing list to announce the release, consider -including some or all of the following in the email: -** A link to the release and the release notes -** A link to the docs -** Describe the type of release (stable, bug fix, RC) -** Hash values (SHA1, SHA256, MD5) for the release WAR file. +Send an email, signed with the same PGP key used to sign the release +artifacts, to the mailing list to announce the release. + +Consider including some or all of the following in the email: +* A link to the release and the release notes +* A link to the docs +* Describe the type of release (stable, bug fix, RC) +* Hash values (SHA1, SHA256, MD5) for the release WAR file. + The SHA1 and MD5 can be taken from the artifact page on Sonatype. The SHA256 can be generated with `openssl sha -sha256 bazel-bin/release.war` or an equivalent command. -* Update the new discussion group announcement to be sticky -** Go to: http://groups.google.com/group/repo-discuss/topics -** Click on the announcement thread -** Near the top right, click on actions -** Under actions, click the "Display this top first" checkbox - -* Update the previous discussion group announcement to no longer be sticky -** See above (unclick checkbox) - - [[increase-version]] === Increase Gerrit Version for Current Development @@ -383,7 +350,7 @@ Use the `version` tool to set the version in the `version.bzl` file: ---- - ./tools/version.py 2.11-SNAPSHOT + ./tools/version.py 2.6-SNAPSHOT ---- Verify that the changes made by the tool are sane, then commit them, push
diff --git a/Documentation/dev-stars.txt b/Documentation/dev-stars.txt index 553ac5b..1fb871a 100644 --- a/Documentation/dev-stars.txt +++ b/Documentation/dev-stars.txt
@@ -61,6 +61,19 @@ The ignore star is represented by the special star label 'ignore'. +[[mute-star]] +== Mute Star + +If the "mute/<patchset_id>"-star is set by a user, and <patchset_id> +matches the current patch set, the change is always reported as "reviewed" +in the ChangeInfo. + +This allows users to "de-highlight" changes in a dashboard until a new +patchset has been uploaded. + +The ChangeInfo muted-field will show if the change is currently in a +mute state. + [[query-stars]] == Query Stars
diff --git a/Documentation/intro-quick.txt b/Documentation/intro-quick.txt index c6dad5b..a827bbd 100644 --- a/Documentation/intro-quick.txt +++ b/Documentation/intro-quick.txt
@@ -1,8 +1,7 @@ = Gerrit Code Review - A Quick Introduction Gerrit is a web-based code review tool built on top of the git version -control system, but if you've got as far as reading this guide then -you probably already know that. The purpose of this introduction is to +control system. The purpose of this introduction is to allow you to answer the question, is Gerrit the right tool for me? Will it fit in my work flow and in my organization?
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt index 5e6fe59..9aa0a3b 100644 --- a/Documentation/intro-user.txt +++ b/Documentation/intro-user.txt
@@ -482,9 +482,66 @@ $ git push origin HEAD:refs/heads/master -o topic=multi-master ---- +[[private-changes]] +== Private Changes + +Private changes are changes that are only visible to their owners and +reviewers. Private changes are useful in a number of cases: + +* You want to check what the change looks before formal review starts. + By marking the change private without reviewers, nobody can't + prematurely comment on your changes. + +* You want to use Gerrit to sync data between different devices. By + creating a private throwaway change without reviewers, you can push + from one device, and fetch to another device. + +* You want to do code review on a change that has sensitive + aspects. By reviewing a security fix in a private change, + outsiders can't discover the fix before it is pushed out. Even after + merging the change, the review can be kept private. + +To create a private change, you push it with the `private` option. + +.Push a private change +---- + $ git commit + $ git push origin HEAD:refs/for/master%private +---- + +The change will remain private on subsequent pushes until you specify +the `remove-private` option. Alternatively, the web UI provides buttons +to mark a change private and non-private again. + +When pushing a private change with a commit that is authored by another +user, the other user will not be automatically added as a reviewer and +must be explicitly added. + +For CI systems that must verify private changes, a special permission +can be granted +(link:access-control.html#category_view_private_changes[View Private Changes]). +In that case, care should be taken to prevent the CI system from +exposing secret details. + +[[ignore]] +== Ignoring and Muting Changes + +Changes can be ignored, which means they will not appear in the 'Incoming +Reviews' dashboard and any related email notifications will be suppressed. +This can be useful when you are added as a reviewer to a change on which +you do not actively participate in the review, but do not want to completely +remove yourself. + +Alternatively, rather than completely ignoring the change, it can be muted. +Muting a change means it will always be marked as "reviewed" in dashboards, +until a new patch set is uploaded. + [[drafts]] == Working with Drafts +Drafts is a deprecated feature and will be removed soon. Consider using +private changes instead. + Changes can be uploaded as drafts. By default draft changes are only visible to the change owner. This gives you the possibility to have some staging before making your changes visible to the reviewers. Draft @@ -716,6 +773,12 @@ and `Edit Config` buttons on the project screen, and the `Follow-Up` button on the change screen). +- [[publish-comments-on-push]]`Publish Draft Comments When a Change Is Updated by Push`: ++ +Whether to publish any outstanding draft comments by default when pushing +updates to open changes. This preference just sets the default; the behavior can +still be overridden using a link:user-upload.html#publish-comments[push option]. + - [[use-flash]]`Use Flash Clipboard Widget`: + Whether the Flash clipboard widget should be used. If enabled and the Flash
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt index 091dbb3..8cddce2 100644 --- a/Documentation/metrics.txt +++ b/Documentation/metrics.txt
@@ -63,6 +63,12 @@ * `sql/connection_pool/connections`: SQL database connections. +=== Topics + +* `topic/cross_project_submit`: number of cross-project topic submissions. +* `topic/cross_project_submit_completed`: number of cross-project +topic submissions that concluded successfully. + === JGit * `jgit/block_cache/cache_used`: Bytes of memory retained in JGit block cache. @@ -90,6 +96,9 @@ * `notedb/auto_rebuild_latency`: NoteDb auto-rebuilding latency by table. * `notedb/auto_rebuild_failure_count`: NoteDb auto-rebuilding attempts that failed by table. +* `notedb/external_id_update_count`: Total number of external ID updates. +* `notedb/read_all_external_ids_latency`: Latency for reading all +external ID's from NoteDb. === Reviewer Suggestion @@ -102,6 +111,10 @@ * `reviewer_suggestion/query_groups`: Latency for querying groups for reviewer suggestion. +=== Repo Sequences + +* `sequence/next_id_latency`: Latency of requesting IDs from repo sequences. + === Replication Plugin * `plugins/replication/replication_latency`: Time spent pushing to remote
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt index 13fca66..fbfd5e4 100644 --- a/Documentation/rest-api-accounts.txt +++ b/Documentation/rest-api-accounts.txt
@@ -1248,6 +1248,7 @@ "size_bar_in_change_table": true, "review_category_strategy": "ABBREV", "mute_common_path_prefixes": true, + "publish_comments_on_push": true, "default_base_for_merges": "FIRST_PARENT", "my": [ { @@ -1361,6 +1362,7 @@ "size_bar_in_change_table": true, "review_category_strategy": "NAME", "diff_view": "SIDE_BY_SIDE", + "publish_comments_on_push": true, "mute_common_path_prefixes": true, "my": [ { @@ -2643,6 +2645,9 @@ The base which should be pre-selected in the 'Diff Against' drop-down list when the change screen is opened for a merge commit. Allowed values are `AUTO_MERGE` and `FIRST_PARENT`. +|`publish_comments_on_push` |not set if `false`| +Whether to link:user-upload.html#publish-comments[publish draft comments] on +push by default. |============================================ [[preferences-input]]
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index 69afaa5..d64054a 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt
@@ -1091,6 +1091,19 @@ change is merged ---- +.Notifications + +An email will be sent using the "abandon" template. The notify handling is ALL. +Notifications are suppressed on WIP changes that have never started review. + +[options="header",cols="1,2"] +|============================= +|WIP State |notify=ALL +|Ready for review|owner, reviewers, CCs, stars, ABANDONED_CHANGES watchers +|Work in progress|not sent +|Reviewable WIP |owner, reviewers, CCs, stars, ABANDONED_CHANGES watchers +|============================= + [[restore-change]] === Restore Change -- @@ -2107,6 +2120,195 @@ } ---- +[[set-work-in-pogress]] +=== Set Work-In-Progress +-- +'POST /changes/link:#change-id[\{change-id\}]/wip' +-- + +Marks the change as not ready for review yet. + +The request body does not need to include a +link:#work-in-progress-input[WorkInProgressInput] entity if no review comment +is added. Actions that create a new patch set in a WIP change default to +notifying *OWNER* instead of *ALL*. + +.Request +---- + POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/wip HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "message": "Refactoring needs to be done before we can proceed here." + } +---- + +.Response +---- + HTTP/1.1 200 OK +---- + +[[set-ready-for-review]] +=== Set Ready-For-Review +-- +'POST /changes/link:#change-id[\{change-id\}]/ready' +-- + +Marks the change as ready for review (set WIP property to false). + +Activates notifications of reviewer. The request body does not need +to include a link:#work-in-progress-input[WorkInProgressInput] entity +if no review comment is added. + +.Request +---- + POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/ready HTTP/1.0 + Content-Type: application/json;charset=UTF-8 + + { + "message": "Refactoring is done." + } + +---- + +.Response +---- + HTTP/1.1 200 OK +---- + +[[mark-private]] +=== Mark Private +-- +'POST /changes/link:#change-id[\{change-id\}]/private' +-- + +Marks the change to be private. Changes may only be marked private by the +owner or site administrators. + +A message can be specified in the request body inside a +link:#private-input[PrivateInput] entity. + +.Request +---- + POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "message": "After this security fix has been released we can make it public now." + } +---- + +.Response +---- + HTTP/1.1 201 Created +---- + +If the change was already private the response is "`200 OK`". + +[[unmark-private]] +=== Unmark Private +-- +'DELETE /changes/link:#change-id[\{change-id\}]/private' +-- + +Marks the change to be non-private. Note users can only unmark own private +changes. + +A message can be specified in the request body inside a +link:#private-input[PrivateInput] entity. + +.Request +---- + DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "message": "This is a security fix that must not be public." + } +---- + +.Response +---- + HTTP/1.1 204 No Content +---- + +If the change was already not private, the response is "`409 Conflict`". + +Please note that some proxies prohibit request bodies for DELETE +requests. In this case, if you want to set a message options, use a +POST request: + +.Request +---- + POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private.delete HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "message": "This is a security fix that must not be public." + } +---- + +[[ignore]] +=== Ignore +-- +'PUT /changes/link:#change-id[\{change-id\}]/ignore' +-- + +Marks a change as ignored. The change will not be shown in the incoming +reviews dashboard, and email notifications will be suppressed. + +.Request +---- + PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/ignore HTTP/1.0 +---- + +[[unignore]] +=== Unignore +-- +'PUT /changes/link:#change-id[\{change-id\}]/unignore' +-- + +Un-marks a change as ignored. + +.Request +---- + PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unignore HTTP/1.0 +---- + +[[mute]] +=== Mute +-- +'PUT /changes/link:#change-id[\{change-id\}]/mute' +-- + +Marks a change as muted. + +This allows users to "de-highlight" changes in their dashboard until a new +patch set is uploaded. + +This differs from the link:#ignore[ignore] endpoint, which will mute +emails and hide the change from dashboard completely until it is +link:#unignore[unignored] again. + + +.Request +---- + PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/mute HTTP/1.0 +---- + +[[unmute]] +=== Unmute +-- +'PUT /changes/link:#change-id[\{change-id\}]/unmute' +-- + +Unmutes a change. + +.Request +---- + PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/unmute HTTP/1.0 +---- + [[edit-endpoints]] == Change Edit Endpoints @@ -2605,6 +2807,8 @@ Suggest the reviewers for a given query `q` and result limit `n`. If result limit is not passed, then the default 10 is used. +Groups can be excluded from the results by specifying 'e=f'. + As result a list of link:#suggested-reviewer-info[SuggestedReviewerInfo] entries is returned. .Request @@ -2704,16 +2908,16 @@ )]}' { + "input": "john.doe@example.com", "reviewers": [ { - "input": "john.doe@example.com", + "_account_id": 1000096, + "name": "John Doe", + "email": "john.doe@example.com" "approvals": { "Verified": " 0", "Code-Review": " 0" }, - "_account_id": 1000096, - "name": "John Doe", - "email": "john.doe@example.com" } ] } @@ -2764,6 +2968,52 @@ } ---- +If link:config-project-config.html#reviewer.enableByEmail[reviewer.enableByEmail] is set +for the project, reviewers and CCs are not required to have a Gerrit account. If you POST +an email address of a reviewer or CC then, they will be added to the change even if they +don't have a Gerrit account. + +If this option is disabled, the request would fail with `400 Bad Request` if the email +address can't be resolved to an active Gerrit account. + +Note that the name is optional so both "un.registered@reviewer.com" and +"John Doe <un.registered@reviewer.com>" are valid inputs. + +Reviewers without Gerrit accounts can only be added on changes visible to anonymous users. + +.Request +---- + POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "reviewer": "John Doe <un.registered@reviewer.com>" + } +---- + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "input": "John Doe <un.registered@reviewer.com>" + } +---- + +.Notifications + +An email will be sent using the "newchange" template. + +[options="header",cols="1,1,1"] +|============================= +|WIP State |Default|notify=ALL +|Ready for review|owner, reviewers, CCs|owner, reviewers, CCs +|Work in progress|not sent|owner, reviewers, CCs +|============================= + [[delete-reviewer]] === Delete Reviewer -- @@ -2801,6 +3051,19 @@ HTTP/1.1 204 No Content ---- +.Notifications + +An email will be sent using the "deleteReviewer" template. If deleting the +reviewer resulted in one or more approvals being removed, then the deleted +reviewer will also receive a notification (unless notify=NONE). + +[options="header",cols="1,5"] +|============================= +|WIP State |Default Recipients +|Ready for review|notify=ALL: deleted reviewer (if voted), owner, reviewers, CCs, stars, ALL_COMMENTS watchers +|Work in progress|notify=NONE: deleted reviewer (if voted) +|============================= + [[list-votes]] === List Votes -- @@ -2869,6 +3132,28 @@ HTTP/1.1 204 No Content ---- +[[set-message]] +=== Set Commit Message +-- +'PUT /changes/link:#change-id[\{change-id\}]/message' +-- + +Creates a new patch set with a new commit message. + +The new commit message must be provided in the request body inside a +link:#commit-message-input[CommitMessageInput] entity and contain the change ID footer if +link:project-configuration.html#require-change-id[Require Change-Id] was specified. + +.Request +---- + PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/message HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "message": "New Commit message \n\nChange-Id: I10394472cbd17dd12454f229e4f6de00b143a444\n" + } +---- + [[revision-endpoints]] == Revision Endpoints @@ -3306,11 +3591,17 @@ 'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/review' -- -Sets a review on a revision. +Sets a review on a revision, optionally also publishing draft comments, setting +labels, and adding reviewers or CCs. The review must be provided in the request body as a link:#review-input[ReviewInput] entity. +A review cannot be set on a change edit. Trying to post a review for a +change edit fails with `409 Conflict`. + +Here is an example of using this method to set labels: + .Request ---- POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/review HTTP/1.0 @@ -3346,8 +3637,9 @@ } ---- -As response a link:#review-info[ReviewInfo] entity is returned that -describes the applied labels. +As response a link:#review-result[ReviewResult] entity is returned that +describes the applied labels and any added reviewers (e.g. yourself, +if you set a label but weren't previously a reviewer on this CL). .Response ---- @@ -3363,11 +3655,8 @@ } ---- -A review cannot be set on a change edit. Trying to post a review for a -change edit fails with `409 Conflict`. - -It is also possible to add one or more reviewers to a change simultaneously -with a review. +It is also possible to add one or more reviewers or CCs +to a change simultaneously with a review: .Request ---- @@ -3375,16 +3664,17 @@ Content-Type: application/json; charset=UTF-8 { - "message": "Looks good to me, but Jane and John should also take a look.", - "labels": { - "Code-Review": 1 - }, + "message": "I don't have context here. Jane and maybe John and the project leads should take a look.", "reviewers": [ { "reviewer": "jane.roe@example.com" }, { - "reviewer": "john.doe@example.com" + "reviewer": "john.doe@example.com", + "state": "CC" + } + { + "reviewer": "MyProjectVerifiers", } ] } @@ -3392,8 +3682,8 @@ Each element of the `reviewers` list is an instance of link:#reviewer-input[ReviewerInput]. The corresponding result of -adding each reviewer will be returned in a list of -link:#add-reviewer-result[AddReviewerResult]. +adding each reviewer will be returned in a map of inputs to +link:#add-reviewer-result[AddReviewerResult]s. .Response ---- @@ -3403,36 +3693,66 @@ )]}' { - "labels": { - "Code-Review": 1 - }, - "reviewers": [ - { + "reviewers": { + "jane.roe@example.com": { "input": "jane.roe@example.com", - "approvals": { - "Verified": " 0", - "Code-Review": " 0" - }, - "_account_id": 1000097, - "name": "Jane Roe", - "email": "jane.roe@example.com" + "reviewers": [ + { + "_account_id": 1000097, + "name": "Jane Roe", + "email": "jane.roe@example.com" + "approvals": { + "Verified": " 0", + "Code-Review": " 0" + }, + }, + ] }, - { + "john.doe@example.com": { "input": "john.doe@example.com", - "approvals": { - "Verified": " 0", - "Code-Review": " 0" - }, - "_account_id": 1000096, - "name": "John Doe", - "email": "john.doe@example.com" + "ccs": [ + { + "_account_id": 1000096, + "name": "John Doe", + "email": "john.doe@example.com" + "approvals": { + "Verified": " 0", + "Code-Review": " 0" + }, + } + ] + }, + "MyProjectVerifiers": { + "input": "MyProjectVerifiers", + "reviewers": [ + { + "_account_id": 1000098, + "name": "Alice Ansel", + "email": "alice.ansel@example.com" + "approvals": { + "Verified": " 0", + "Code-Review": " 0" + }, + }, + { + "_account_id": 1000099, + "name": "Bob Bollard", + "email": "bob.bollard@example.com" + "approvals": { + "Verified": " 0", + "Code-Review": " 0" + }, + }, + ] } - ] + } } ---- If there are any errors returned for reviewers, the entire review request will -be rejected with `400 Bad Request`. +be rejected with `400 Bad Request`. None of the entries will have the +`reviewers` or `ccs` field set, and those which specifically failed will have +the `errors` field set containing details of why they failed. .Error Response ---- @@ -3443,6 +3763,13 @@ )]}' { "reviewers": { + "jane.roe@example.com": { + "input": "jane.roe@example.com", + "error": "Account of jane.roe@example.com is inactive." + }, + "john.doe@example.com": { + "input": "john.doe@example.com" + }, "MyProjectVerifiers": { "input": "MyProjectVerifiers", "error": "The group My Group has 15 members. Do you want to add them all as reviewers?", @@ -3452,6 +3779,43 @@ } ---- +.Notifications + +An email will be sent using the "comment" template. + +If the top-level notify property is null or not set, then notification behavior +depends on whether the change is WIP, whether it has started review, and whether +the tag property is null. + +NOTE: If adding reviewers, the notify property of each ReviewerInput is *ignored*. +Use the notify property of the top-level link:#review-input[ReviewInput] instead. + +For the purposes of this table, *everyone* means *owner, reviewers, CCs, stars, and ALL_COMMENTS +watchers*. + +[options="header",cols="2,1,1,2,2"] +|============================= +|WIP State |Review Started|Tag Given|Default |notify=ALL +|Ready for review|N/A |N/A |everyone|everyone +|Work in progress|no |no |not sent|everyone +|Work in progress|no |yes |owner |everyone +|Work in progress|yes |no |everyone|everyone +|Work in progress|yes |yes |owner |everyone + +|============================= + +If reviewers are added, then a second email will be sent using the "newchange" +template. The notification logic for this email is the same as for +link:#add-reviewer[Add Reviewer]. + +[options="header",cols="1,1,1"] +|============================= +|WIP State |Default |notify=ALL +|Ready for review|owner, reviewers, CCs|owner, reviewers, CCs +|Work in progress|not sent |owner, reviewers, CCs +|============================= + + [[rebase-revision]] === Rebase Revision -- @@ -4160,6 +4524,62 @@ } ---- +[[delete-comment]] +=== Delete Comment +-- +'DELETE /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/comments/link:#comment-id[\{comment-id\}]' + +'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/comments/link:#comment-id[\{comment-id\}]/delete' +-- + +Deletes a published comment of a revision. Instead of deleting the +whole comment, this endpoint just replaces the comment's message +with a new message, which contains the name of the user who deletes +the comment and the reason why it's deleted. The reason can be +provided in the request body as a +link:#delete-comment-input[DeleteCommentInput] entity. + +Note that only users with the +link:access-control.html#capability_administrateServer[Administrate Server] +global capability are permitted to delete a comment. + +Please note that some proxies prohibit request bodies for DELETE +requests. In this case, if you want to specify options, use a +POST request: + +.Request +---- + POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/comments/TvcXrmjM/delete HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "reason": "contains confidential information" + } +---- + +As response a link:#comment-info[CommentInfo] entity is returned that +describes the updated comment. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "id": "TvcXrmjM", + "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java", + "line": 23, + "message": "Comment removed by: Administrator; Reason: contains confidential information", + "updated": "2013-02-26 15:40:43.986000000", + "author": { + "_account_id": 1000096, + "name": "John Doe", + "email": "john.doe@example.com" + } + } +---- + [[list-robot-comments]] === List Robot Comments -- @@ -4256,6 +4676,72 @@ } ---- +[[apply-fix]] +=== Apply Fix +-- +'POST /changes/<<change-id,\{change-id\}>>/revisions/<<revision-id,\{revision-id\}>>/fixes/<<fix-id,\{fix-id\}>>/apply' +-- + +Applies a suggested fix by creating a change edit which includes the +modifications indicated by the fix suggestion. If a change edit already exists, +it will be updated accordingly. A fix can only be applied if no change edit +exists and the fix refers to the current patch set, or the fix refers to the +patch set on which the change edit is based. + +.Request +---- + POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/fixes/8f605a55_f6aa4ecc/apply HTTP/1.0 +---- + +If the fix was successfully applied, an <<edit-info,EditInfo>> describing the +resulting change edit is returned. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "commit":{ + "parents":[ + { + "commit":"1eee2c9d8f352483781e772f35dc586a69ff5646", + } + ], + "author":{ + "name":"John Doe", + "email":"john.doe@example.com", + "date":"2013-05-07 15:21:27.000000000", + "tz":120 + }, + "committer":{ + "name":"Jane Doe", + "email":"jane.doe@example.com", + "date":"2013-05-07 15:35:43.000000000", + "tz":120 + }, + "subject":"Implement feature X", + "message":"Implement feature X\n\nWith this feature ..." + }, + "base_revision":"674ac754f91e64a0efb8087e59a176484bd534d1" + } +---- + +If the application failed e.g. due to conflicts with an existing change edit, +the response "`409 Conflict`" including an error message in the response body +is returned. + +.Response +---- + HTTP/1.1 409 Conflict + Content-Disposition: attachment + Content-Type: text/plain; charset=UTF-8 + + The existing change edit could not be merged with another tree. +---- + [[list-files]] === List Files -- @@ -4336,6 +4822,11 @@ Gets the content of a file from a certain revision. +The optional, integer-valued `parent` parameter can be specified to request +the named file from a parent commit of the specified revision. The value is +the 1-based index of the parent's position in the commit object. If the +parameter is omitted or the value is non-positive, the patch set is referenced. + .Request ---- GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/content HTTP/1.0 @@ -4710,7 +5201,8 @@ Cherry picks a revision to a destination branch. The commit message and destination branch must be provided in the request body inside a -link:#cherrypick-input[CherryPickInput] entity. +link:#cherrypick-input[CherryPickInput] entity. If the commit message +does not specify a Change-Id, a new one is picked for the destination change. .Request ---- @@ -4891,12 +5383,13 @@ This can be: +* an ID of the change in the format "'$$<project>~<numericId>$$'" * an ID of the change in the format "'$$<project>~<branch>~<Change-Id>$$'", where for the branch the `refs/heads/` prefix can be omitted ("$$myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940$$") * a Change-Id if it uniquely identifies one change ("I8473b95934b5732ac55d26311a706c9c2bde9940") -* a legacy numeric change ID ("4247") +* a numeric change ID ("4247") [[comment-id]] === \{comment-id\} @@ -5140,6 +5633,8 @@ change. The labels are lexicographically sorted. |`reviewed` |not set if `false`| Whether the change was reviewed by the calling user. +|`muted` |not set if `false`| +Whether the change has been link:#mute[muted] by the calling user. Only set if link:#reviewed[reviewed] is requested. |`submit_type` |optional| The link:project-configuration.html#submit_type[submit type] of the change. + @@ -5177,7 +5672,7 @@ The reviewers that can be removed by the calling user as a list of link:rest-api-accounts.html#account-info[AccountInfo] entities. + Only set if link:#detailed-labels[detailed labels] are requested. -|`reviewers` || +|`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`. + @@ -5186,6 +5681,12 @@ `REMOVED`: Users that were previously reviewers on the change, but have been removed. + Only set if link:#detailed-labels[detailed labels] are requested. +|`pending_reviewers` |optional| +Updates to `reviewers` that have been made while the change was in the +WIP state. Only present on WIP changes and only if there are pending +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:#detailed-labels[detailed labels] are requested. |`reviewer_updates`|optional| Updates to reviewers set for the change as link:#review-update-info[ReviewerUpdateInfo] entities. @@ -5211,6 +5712,12 @@ |`problems` |optional| A list of link:#problem-info[ProblemInfo] entities describing potential problems with this change. Only set if link:#check[CHECK] is set. +|`is_private` |optional, not set if `false`| +When present, change is marked as private. +|`work_in_progress` |optional, not set if `false`| +When present, change is marked as Work In Progress. +|`has_started_review` |optional, not set if `false`| +When present, change has been marked Ready at some point in time. |================================== [[change-input]] @@ -5229,6 +5736,10 @@ |`topic` |optional|The topic to which this change belongs. |`status` |optional, default to `NEW`| The status of the change (only `NEW` and `DRAFT` accepted here). +|`is_private` |optional, default to `false`| +Whether the new change should be marked as private. +|`work_in_progress` |optional, default to `false`| +Whether the new change should be set to work in progress. |`base_change` |optional| A link:#change-id[\{change-id\}] that identifies the base change for a create change operation. @@ -5259,6 +5770,10 @@ Author of the message as an link:rest-api-accounts.html#account-info[AccountInfo] entity. + Unset if written by the Gerrit system. +|`real_author` |optional| +Real author of the message as an +link:rest-api-accounts.html#account-info[AccountInfo] entity. + +Set if the message was posted on behalf of another user. |`date` || The link:rest-api.html#timestamp[timestamp] this message was posted. |`message` ||The text left by the user. @@ -5280,8 +5795,19 @@ |Field Name ||Description |`message` ||Commit message for the cherry-picked change |`destination` ||Destination branch +|`base` |optional| +40-hex digit SHA-1 of the commit which will be the parent commit of the newly created change. +If set, it must be a merged commit or a change revision on the destination branch. |`parent` |optional, defaults to 1| Number of the parent relative to which the cherry-pick should be considered. +|`notify` |optional| +Notify handling that defines to whom email notifications should be sent +after the cherry-pick. + +Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. + +If not set, the default is `NONE`. +|`notify_details` |optional| +Additional information about whom to notify about the update as a map +of recipient type to link:#notify-info[NotifyInfo] entity. |=========================== [[comment-info]] @@ -5419,6 +5945,39 @@ link:#web-link-info[WebLinkInfo] entities. |=========================== +[[commit-message-input]] +=== CommitMessageInput +The `CommitMessageInput` entity contains information for changing +the commit message of a change. + +[options="header",cols="1,^1,5"] +|============================= +|Field Name ||Description +|`message` ||New commit message. +|`notify` |optional| +Notify handling that defines to whom email notifications should be sent +after the commit message was updated. + +Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. + +If not set, the default is `ALL`. +|`notify_details`|optional| +Additional information about whom to notify about the update as a map +of recipient type to link:#notify-info[NotifyInfo] entity. +|============================= + +[[delete-comment-input]] +=== DeleteCommentInput +The `DeleteCommentInput` entity contains the option for deleting a comment. + +[options="header",cols="1,^1,5"] +|============================= +|Field Name ||Description +|`reason` |optional| +The reason why the comment should be deleted. + +If set, the comment's message will be replaced with +"Comment removed by: `name`; Reason: `reason`", +or just "Comment removed by: `name`." if not set. +|============================= + [[delete-reviewer-input]] === DeleteReviewerInput The `DeleteReviewerInput` entity contains options for the deletion of a @@ -5475,19 +6034,21 @@ [options="header",cols="1,^1,5"] |========================== -|Field Name ||Description -|`a` |optional|Content only in the file on side A (deleted in B). -|`b` |optional|Content only in the file on side B (added in B). -|`ab` |optional|Content in the file on both sides (unchanged). -|`edit_a` |only present during a replace, i.e. both `a` and `b` are present| +|Field Name ||Description +|`a` |optional|Content only in the file on side A (deleted in B). +|`b` |optional|Content only in the file on side B (added in B). +|`ab` |optional|Content in the file on both sides (unchanged). +|`edit_a` |only present during a replace, i.e. both `a` and `b` are present| Text sections deleted from side A as a link:#diff-intraline-info[DiffIntralineInfo] entity. -|`edit_b` |only present during a replace, i.e. both `a` and `b` are present| +|`edit_b` |only present during a replace, i.e. both `a` and `b` are present| Text sections inserted in side B as a link:#diff-intraline-info[DiffIntralineInfo] entity. -|`skip` |optional|count of lines skipped on both sides when the file is +|`due_to_rebase`|not set if `false`|Indicates whether this entry was introduced by a +rebase. +|`skip` |optional|count of lines skipped on both sides when the file is too large to include all common lines. -|`common` |optional|Set to `true` if the region is common according +|`common` |optional|Set to `true` if the region is common according to the requested ignore-whitespace parameter, but a and b contain differing amounts of whitespace. When present and true a and b are used instead of ab. @@ -5592,7 +6153,7 @@ |`commit` ||The commit of change edit as link:#commit-info[CommitInfo] entity. |`base_revision`||The revision of the patch set the change edit is based on. -|`fetch` || +|`fetch` |optional| Information about how to fetch this patch set. The fetch information is provided as a map that maps the protocol name ("`git`", "`http`", "`ssh`") to link:#fetch-info[FetchInfo] entities. @@ -5671,8 +6232,8 @@ for input objects. |`description` ||A description of the suggested fix. |`replacements` ||A list of <<fix-replacement-info,FixReplacementInfo>> -entities indicating how the content of the file on which the comment was placed -should be modified. They should refer to non-overlapping regions. +entities indicating how the content of one or several files should be modified. +Within a file, they should refer to non-overlapping regions. |========================== [[fix-replacement-info]] @@ -5683,10 +6244,13 @@ [options="header",cols="1,6"] |========================== |Field Name |Description -|`path` |The path of the file which should be modified. Modifications -are only allowed for the file on which the corresponding comment was placed. +|`path` |The path of the file which should be modified. Any file in +the repository may be modified. |`range` |A <<comment-range,CommentRange>> indicating which content -of the file should be replaced. +of the file should be replaced. Lines in the file are assumed to be separated +by the line feed character, the carriage return character, the carriage return +followed by the line feed character, or one of the other Unicode linebreak +sequences supported by Java. |`replacement` |The content which should be used instead of the current one. |========================== @@ -5799,7 +6363,9 @@ |=========================== |Field Name ||Description |`all` |optional|List of all approvals for this label as a list -of link:#approval-info[ApprovalInfo] entities. +of link:#approval-info[ApprovalInfo] entities. Items in this list may +not represent actual votes cast by users; if a user votes on any label, +a corresponding ApprovalInfo will appear in this list for all labels. |`values` |optional|A map of all values that are allowed for this label. The map maps the values ("`-2`", "`-1`", " `0`", "`+1`", "`+2`") to the value descriptions. @@ -5896,6 +6462,17 @@ identify the accounts that should be should be notified. |======================= +[[private-input]] +=== PrivateInput +The `PrivateInput` entity contains information for changing the private +flag on a change. + +[options="header",cols="1,^1,5"] +|======================= +|Field Name||Description +|`message` |optional|Message describing why the private flag was changed. +|======================= + [[problem-info]] === ProblemInfo The `ProblemInfo` entity contains a description of a potential consistency problem @@ -6120,6 +6697,27 @@ link:rest-api-accounts.html#account-id[\{account-id\}] the review should be posted on behalf of. To use this option the caller must have been granted `labelAs-NAME` permission for all keys of labels. +|`reviewers` |optional| +A list of link:rest-api-changes.html#reviewer-input[ReviewerInput] +representing reviewers that should be added to the change. +|============================ + +[[review-result]] +=== ReviewResult +The `ReviewResult` entity contains information regarding the updates +that were made to a review. + +[options="header",cols="1,^1,5"] +|============================ +|Field Name ||Description +|`labels` |optional| +Map of labels to values after the review was posted. Null if any reviewer +additions were rejected. +|`reviewers` |optional| +Map of account or group identifier to +link:rest-api-changes.html#add-reviewer-result[AddReviewerResult] +representing the outcome of adding as a reviewer. +Absent if no reviewer additions were requested. |============================ [[reviewer-info]] @@ -6138,6 +6736,10 @@ |`approvals` | The approvals of the reviewer as a map that maps the label names to the approval values ("`-2`", "`-1`", "`0`", "`+1`", "`+2`"). +|`_account_id` | +This field is inherited from `AccountInfo` but is optional here if an +unregistered reviewer was added by email. See +link:rest-api-changes.html#add-reviewer[add-reviewer] for details. |========================== [[reviewer-input]] @@ -6226,6 +6828,9 @@ patch set as a link:#push-certificate-info[PushCertificateInfo] entity. This field is always set if the option is requested; if no push certificate was provided, it is set to an empty object. +|`description` |optional| +The description of this patchset, as displayed in the patchset +selector menu. May be null if no description is set. |=========================== [[robot-comment-info]] @@ -6435,6 +7040,18 @@ |`image_url`|URL to the icon of the link. |====================== +[[work-in-progress-input]] +=== WorkInProgressInput +The `WorkInProgressInput` entity contains additional information for a change +set to WorkInProgress/ReadyForReview. + +[options="header",cols="1,^1,5"] +|============================= +|Field Name ||Description +|`message` |optional| +Message to be added as a review comment to the change being set WorkInProgress/ReadyForReview. +|============================= + GERRIT ------ Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt index 62e3ee4..6ce9688 100644 --- a/Documentation/rest-api-config.txt +++ b/Documentation/rest-api-config.txt
@@ -138,6 +138,98 @@ } ---- +[[check-consistency]] +=== Check Consistency +-- +'POST /config/server/check.consistency' +-- + +Runs consistency checks and returns detected problems. + +Input for the consistency checks that should be run must be provided in +the request body inside a +link:#consistency-check-input[ConsistencyCheckInput] entity. + +.Request +---- + POST /config/server/check.consistency HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "check_accounts": {}, + "check_account_external_ids": {} + } +---- + +As result a link:#consistency-check-info[ConsistencyCheckInfo] entity +is returned that contains detected consistency problems. + +.Response +---- + HTTP/1.1 200 OK + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "check_accounts_result": { + "problems": [ + { + "status": "ERROR", + "message": "Account \u00271000024\u0027 has no external ID for its preferred email \u0027foo.bar@example.com\u0027" + } + ] + } + "check_account_external_ids_result": { + "problems": [ + { + "status": "ERROR", + "message": "External ID \u0027uuid:ccb8d323-1361-45aa-8874-41987a660c46\u0027 belongs to account that doesn\u0027t exist: 1000012" + } + ] + } + } +---- + +[[check-access]] +=== Check Access +-- +'POST /config/server/check.access' +-- + +Runs access checks for other users. + +Input for the access checks that should be run must be provided in +the request body inside a +link:#access-check-input[AccessCheckInput] entity. + +.Request +---- + POST /config/server/check HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "project": "medium", + "account": "Kristen.Burns@gerritcodereview.com", + "ref": "refs/heads/secret/bla" + } +---- + +The result is a link:#access-check-info[AccessCheckInfo] entity +detailing the read access of the given user for the given project (or +project-ref combination). + +.Response +---- + HTTP/1.1 200 OK + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "message": "user Kristen Burns \u003cKristen.Burns@gerritcodereview.com\u003e (1000098) cannot see ref refs/heads/secret/master in project medium", + "status": 403 + } +---- + [[confirm-email]] === Confirm Email -- @@ -980,6 +1072,7 @@ "size_bar_in_change_table": true, "review_category_strategy": "NONE", "mute_common_path_prefixes": true, + "publish_comments_on_push": true, "my": [ { "url": "#/dashboard/self", @@ -1062,6 +1155,7 @@ "size_bar_in_change_table": true, "review_category_strategy": "NONE", "mute_common_path_prefixes": true, + "publish_comments_on_push": true, "my": [ { "url": "#/dashboard/self", @@ -1221,6 +1315,34 @@ [[json-entities]] == JSON Entities +[[access-check-info]] +=== AccessCheckInfo +The `AccessCheckInfo` entity is the result of a +an access check. + +[options="header",cols="1,^1,5"] +|========================================= +|Field Name ||Description +|`status`||The HTTP status code for the access. +200 means success, 403 means denied and 404 means the project does not +exist. +|`message`|optional|A clarifying message if `status` is not 200. +========================================= + +[[access-check-input]] +=== AccessCheckInput +The `AccessCheckInput` entity is a tuple of (account, project) or +(account, project, ref) for which we want to check access. + +[options="header",cols="1,^1,5"] +|========================================= +|Field Name ||Description +|`account`||The account for which to check access +|`project`||The project for which to check access +|`ref`|optional|The refname for which to check access +========================================= + + [[auth-info]] === AuthInfo The `AuthInfo` entity contains information about the authentication @@ -1365,6 +1487,72 @@ the whole topic is submitted]. |============================= +[[check-account-external-ids-input]] +=== CheckAccountExternalIdsInput +The `CheckAccountExternalIdsInput` entity contains input for the +account external IDs consistency check. + +Currently this entity contains no fields. + +[[check-account-external-ids-result-info]] +=== CheckAccountExternalIdsResultInfo +The `CheckAccountExternalIdsResultInfo` entity contains the result of +running the account external IDs consistency check. + +[options="header",cols="1,6"] +|====================== +|Field Name|Description +|`problems`|A list of link:#consistency-problem-info[ +ConsistencyProblemInfo] entities. +|====================== + +[[consistency-check-info]] +=== ConsistencyCheckInfo +The `ConsistencyCheckInfo` entity contains the results of running +consistency checks. + +[options="header",cols="1,^1,5"] +|================================================ +|Field Name ||Description +|`check_accounts_result` |optional| +The result of running the account consistency check as a +link:#check-accounts-result-info[CheckAccountsResultInfo] entity. +|`check_account_external_ids_result`|optional| +The result of running the account external ID consistency check as a +link:#check-account-external-ids-result-info[ +CheckAccountExternalIdsResultInfo] entity. +|================================================ + +[[consistency-check-input]] +=== ConsistencyCheckInput +The `ConsistencyCheckInput` entity contains information about which +consistency checks should be run. + +[options="header",cols="1,^1,5"] +|========================================= +|Field Name ||Description +|`check_accounts` |optional| +Input for the account consistency check as +link:#check-accounts-input[CheckAccountsInput] entity. +|`check_account_external_ids`|optional| +Input for the account external ID consistency check as +link:#check-account-external-ids-input[CheckAccountExternalIdsInput] +entity. +|========================================= + +[[consistency-problem-info]] +=== ConsistencyProblemInfo +The `ConsistencyProblemInfo` entity contains information about a +consistency problem. + +[options="header",cols="1,6"] +|====================== +|Field Name|Description +|`status` |The status of the consistency problem. + +Possible values are `ERROR` and `WARNING`. +|`message` |Message describing the consistency problem. +|====================== + [[download-info]] === DownloadInfo The `DownloadInfo` entity contains information about supported download
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt index 7296a22..1fd0bae 100644 --- a/Documentation/rest-api-groups.txt +++ b/Documentation/rest-api-groups.txt
@@ -42,7 +42,8 @@ "description": "Gerrit Site Administrators", "group_id": 1, "owner": "Administrators", - "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389" + "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389", + "created_on": "2013-02-01 09:59:32.126000000" }, "Anonymous Users": { "id": "global%3AAnonymous-Users", @@ -52,7 +53,8 @@ "description": "Any user, signed-in or not", "group_id": 2, "owner": "Administrators", - "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389" + "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389", + "created_on": "2013-02-01 09:59:32.126000000" }, "MyProject_Committers": { "id": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7", @@ -62,7 +64,8 @@ }, "group_id": 6, "owner": "MyProject_Committers", - "owner_id": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7" + "owner_id": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7", + "created_on": "2013-02-01 09:59:32.126000000" }, "Non-Interactive Users": { "id": "5057f3cbd3519d6ab69364429a89ffdffba50f73", @@ -72,7 +75,8 @@ "description": "Users who perform batch actions on Gerrit", "group_id": 4, "owner": "Administrators", - "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389" + "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389", + "created_on": "2013-02-01 09:59:32.126000000" }, "Project Owners": { "id": "global%3AProject-Owners", @@ -82,7 +86,8 @@ "description": "Any owner of the project", "group_id": 5, "owner": "Administrators", - "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389" + "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389", + "created_on": "2013-02-01 09:59:32.126000000" }, "Registered Users": { "id": "global%3ARegistered-Users", @@ -92,7 +97,8 @@ "description": "Any signed-in user", "group_id": 3, "owner": "Administrators", - "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389" + "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389", + "created_on": "2013-02-01 09:59:32.126000000" } } ---- @@ -154,7 +160,8 @@ "description":"contains all committers for MyProject", "group_id": 551, "owner": "MyProject-Owners", - "owner_id": "7ca042f4d5847936fcb90ca91057673157fd06fc" + "owner_id": "7ca042f4d5847936fcb90ca91057673157fd06fc", + "created_on": "2013-02-01 09:59:32.126000000" } } ---- @@ -211,6 +218,7 @@ "group_id": 1, "owner": "Administrators", "owner_id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b", + "created_on": "2013-02-01 09:59:32.126000000", "id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b" } } @@ -254,6 +262,7 @@ "group_id": 20, "owner": "MyProject-Test-Group", "owner_id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b", + "created_on": "2013-02-01 09:59:32.126000000", "id": "68236a40ca78de8be630312d8ba50250bc5638ae" }, { @@ -263,6 +272,7 @@ "group_id": 17, "owner": "ProjectX-Testers", "owner_id": "59b92f35489e62c80d1ab1bf0c2d17843038df8b", + "created_on": "2013-02-01 09:59:32.126000000", "id": "99a534526313324a2667025c3f4e089199b736aa" } ] @@ -330,7 +340,8 @@ "description": "Gerrit Site Administrators", "group_id": 1, "owner": "Administrators", - "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389" + "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389", + "created_on": "2013-02-01 09:59:32.126000000" } ---- @@ -378,7 +389,8 @@ "description":"contains all committers for MyProject", "group_id": 551, "owner": "MyProject-Owners", - "owner_id": "7ca042f4d5847936fcb90ca91057673157fd06fc" + "owner_id": "7ca042f4d5847936fcb90ca91057673157fd06fc", + "created_on": "2013-02-01 09:59:32.126000000" } ---- @@ -419,6 +431,7 @@ "group_id": 1, "owner": "Administrators", "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389", + "created_on": "2013-02-01 09:59:32.126000000", "members": [ { "_account_id": 1000097, @@ -668,7 +681,8 @@ "description": "Gerrit Site Administrators", "group_id": 1, "owner": "Administrators", - "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389" + "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389", + "created_on": "2013-02-01 09:59:32.126000000" } ---- @@ -714,7 +728,8 @@ "description": "Gerrit Site Administrators", "group_id": 1, "owner": "Administrators", - "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389" + "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389", + "created_on": "2013-02-01 09:59:32.126000000" } ---- @@ -751,6 +766,7 @@ "group_id": 3, "owner": "Administrators", "owner_id": "e56678641565e7f59dd5c6878f5bcbc842bf150a", + "created_on": "2013-02-01 09:59:32.126000000", "id": "fdda826a0815859ab48d22a05a43472f0f55f89a", "name": "MyGroup" }, @@ -770,6 +786,7 @@ "group_id": 3, "owner": "Administrators", "owner_id": "e56678641565e7f59dd5c6878f5bcbc842bf150a", + "created_on": "2013-02-01 09:59:32.126000000", "id": "fdda826a0815859ab48d22a05a43472f0f55f89a", "name": "MyGroup" }, @@ -1114,7 +1131,8 @@ }, "group_id": 38, "owner": "MyProject-Verifiers", - "owner_id": "7ca042f4d5847936fcb90ca91057673157fd06fc" + "owner_id": "7ca042f4d5847936fcb90ca91057673157fd06fc", + "created_on": "2013-02-01 09:59:32.126000000" } ] ---- @@ -1150,7 +1168,8 @@ }, "group_id": 38, "owner": "Administrators", - "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389" + "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389", + "created_on": "2013-02-01 09:59:32.126000000" } ---- @@ -1186,7 +1205,8 @@ }, "group_id": 8, "owner": "Administrators", - "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389" + "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389", + "created_on": "2013-02-01 09:59:32.126000000" } ---- @@ -1246,7 +1266,8 @@ }, "group_id": 8, "owner": "Administrators", - "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389" + "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389", + "created_on": "2013-02-01 09:59:32.126000000" }, { "id": "5057f3cbd3519d6ab69364429a89ffdffba50f73", @@ -1256,7 +1277,8 @@ }, "group_id": 10, "owner": "MyOtherGroup", - "owner_id": "5057f3cbd3519d6ab69364429a89ffdffba50f73" + "owner_id": "5057f3cbd3519d6ab69364429a89ffdffba50f73", + "created_on": "2013-02-01 09:59:32.126000000" } ] ---- @@ -1382,6 +1404,8 @@ |`group_id` |only for internal groups|The numeric ID of the group. |`owner` |only for internal groups|The name of the owner group. |`owner_id` |only for internal groups|The URL encoded UUID of the owner group. +|`created_on` |only for internal groups|The +link:rest-api.html#timestamp[timestamp] of when the group was created. |`_more_groups`|optional, only for internal groups, not set if `false`| Whether the query would deliver more results if not limited. + Only set on the last group that is returned by a
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt index 72c6a39..9bd8b54 100644 --- a/Documentation/rest-api-projects.txt +++ b/Documentation/rest-api-projects.txt
@@ -2060,6 +2060,60 @@ Ly8gQ29weXJpZ2h0IChDKSAyMDEwIFRoZSBBbmRyb2lkIE9wZW4gU291cmNlIFByb2plY... ---- + +[[cherry-pick-commit]] +=== Cherry Pick Commit +-- +'POST /projects/link:#project-name[\{project-name\}]/commits/link:#commit-id[\{commit-id\}]/cherrypick' +-- + +Cherry-picks a commit of a project to a destination branch. + +The destination branch must be provided in the request body inside a +link:rest-api-changes.html#cherrypick-input[CherryPickInput] entity. +If the commit message is not set, the commit message of the source +commit will be used. + +.Request +---- + POST /projects/work%2Fmy-project/commits/a8a477efffbbf3b44169bb9a1d3a334cbbd9aa96/cherrypick HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "message" : "Implementing Feature X", + "destination" : "release-branch" + } +---- + +As response a link:rest-api-changes.html#change-info[ChangeInfo] entity is returned that +describes the resulting cherry-picked change. + +.Response +---- + HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + { + "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941", + "project": "myProject", + "branch": "release-branch", + "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941", + "subject": "Implementing Feature X", + "status": "NEW", + "created": "2013-02-01 09:59:32.126000000", + "updated": "2013-02-21 11:16:36.775000000", + "mergeable": true, + "insertions": 12, + "deletions": 11, + "_number": 3965, + "owner": { + "name": "John Doe" + } + } +---- + [[dashboard-endpoints]] == Dashboard Endpoints @@ -2418,6 +2472,9 @@ The default submit type of the project, can be `MERGE_IF_NECESSARY`, `FAST_FORWARD_ONLY`, `REBASE_IF_NECESSARY`, `MERGE_ALWAYS` or `CHERRY_PICK`. +|`match_author_to_committer_date` |optional| +link:#inherited-boolean-info[InheritedBooleanInfo] that indicates whether +a change's author date will be changed to match its submitter date upon submit. |`state` |optional| The state of the project, can be `ACTIVE`, `READ_ONLY` or `HIDDEN`. + Not set if the project state is `ACTIVE`.
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt index 7928512..0957d32 100644 --- a/Documentation/rest-api.txt +++ b/Documentation/rest-api.txt
@@ -32,12 +32,41 @@ results to correspond to what anonymous users can read (which may be nothing at all). -Users (and programs) may authenticate by prefixing the endpoint URL with -`/a/`. For example to authenticate to `/projects/`, request the URL -`/a/projects/`. +Users (and programs) can authenticate with HTTP passwords by prefixing +the endpoint URL with `/a/`. For example to authenticate to +`/projects/`, request the URL `/a/projects/`. Gerrit will use HTTP basic +authentication with the HTTP password from the user's account settings +page. This form of authentication bypasses the need for XSRF tokens. -Gerrit uses HTTP basic authentication with the HTTP password from the -user's account settings page. +An authorization cookie may be presented in the request URL inside the +`access_token` query parameter. XSRF tokens are not required when a +valid `access_token` is used in the URL. + +[[cors]] +=== CORS + +Cross-site scripting may be supported if the administrator has configured +link:config-gerrit.html#site.allowOriginRegex[site.allowOriginRegex]. + +Approved web applications running from an allowed origin can rely on +CORS preflight to authorize requests requiring cookie based +authentication, or mutations (POST, PUT, DELETE). Mutations require a +valid XSRF token in the `X-Gerrit-Auth` request header. + +Alternatively applications can use `access_token` in the URL (see +above) to authorize requests. Mutations sent as POST with a request +content type of `text/plain` can skip CORS preflight. Gerrit accepts +additional query parameters `$m` to override the correct method (PUT, +POST, DELETE) and `$ct` to specify the actual content type, such as +`application/json; charset=UTF-8`. Example: + +---- + POST /changes/42/topic?$m=PUT&$ct=application/json%3B%20charset%3DUTF-8&access_token=secret HTTP/1.1 + Content-Type: text/plain + Content-Length: 23 + + {"topic": "new-topic"} +---- [[preconditions]] === Preconditions @@ -78,6 +107,12 @@ `Accept-Encoding` request header is set to `gzip`. This may save on network transfer time for larger responses. +[[input]] +=== Input Format +Unknown JSON parameters will simply be ignored by Gerrit without causing +an exception. This also applies to case-sensitive parameters, such as +map keys. + [[timestamp]] === Timestamp Timestamps are given in UTC and have the format
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt index 392d5cf..74ae568 100644 --- a/Documentation/user-search.txt +++ b/Documentation/user-search.txt
@@ -367,6 +367,22 @@ Mergeability of abandoned changes is not computed. This operator will not find any abandoned but mergeable changes. +[[ignored]] +is:ignored:: ++ +True if the change is ignored. Same as `star:ignore`. + +[[private]] +is:private:: ++ +True if the change is private, ie. only visible to owner and its +reviewers. + +[[workInProgress]] +is:wip:: ++ +True if the change is Work In Progress. + [[status]] status:open, status:pending:: +
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt index 9efbb21..deec660 100644 --- a/Documentation/user-upload.txt +++ b/Documentation/user-upload.txt
@@ -212,6 +212,42 @@ git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental -o topic=driver/i42 ---- +[[private]] +==== Private Changes + +To push a private change or to turn a change private on push the `private` +option can be specified: + +---- + git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%private +---- + +Omitting the `private` option when pushing updates to a private change +doesn't make change non-private again. To remove the private +flag from a change on push, explicitly specify the `remove-private` option: + +---- + git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%remove-private +---- + +[[wip]] +==== Work-In-Progress Changes + +To push a wip change or to turn a change to wip the `work-in-progress` (or `wip`) +option can be specified: + +---- + git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%wip +---- + +Omitting the `wip` option when pushing updates to a wip change +doesn't make change ready again. To remove the `wip` +flag from a change on push, explicitly specify the `ready` option: + +---- + git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/master%ready +---- + [[message]] ==== Message @@ -226,6 +262,21 @@ git push refs parameter does not allow spaces. Use the '_' character instead, it will then be applied as "This is a rebase on master". +[[publish-comments]] +==== Publish Draft Comments + +If you have draft comments on the change(s) that are updated by the push, the +`publish-comments` option will cause them to be published: + +---- + git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%publish-comments +---- + +The default for this option can be set as a +link:intro-user.html#publish-comments-on-push[user preference]. If the +preference is set so the default behavior is to publish, this can be overridden +with the `no-publish-comments` (or `np`) option. + [[review_labels]] ==== Review Labels
diff --git a/README.md b/README.md index 78c8477..1ca01d5 100644 --- a/README.md +++ b/README.md
@@ -23,7 +23,7 @@ ## Source Our canonical Git repository is located on [googlesource.com](https://gerrit.googlesource.com/gerrit). -There is a mirror of the repository on [Github](https://github.com/gerrit-review/gerrit). +There is a mirror of the repository on [Github](https://github.com/GerritCodeReview/gerrit). ## Reporting bugs
diff --git a/WORKSPACE b/WORKSPACE index f63b8b3..2db3a6c 100644 --- a/WORKSPACE +++ b/WORKSPACE
@@ -100,18 +100,18 @@ sha1 = "5d9e2e895e3111622720157d0aa540066d5fce3a", ) -GWT_VERS = "2.8.0" +GWT_VERS = "2.8.1" maven_jar( name = "user", artifact = "com.google.gwt:gwt-user:" + GWT_VERS, - sha1 = "518579870499e15531f454f35dca0772d7fa31f7", + sha1 = "9a13fbee70848f1f1cddd3ae33ad180af3392d9e", ) maven_jar( name = "dev", artifact = "com.google.gwt:gwt-dev:" + GWT_VERS, - sha1 = "f160a61272c5ebe805cd2d3d3256ed3ecf14893f", + sha1 = "c7e88c07e9cda90cc623b4451d0d9713ae03aa53", ) maven_jar( @@ -194,8 +194,8 @@ maven_jar( name = "joda_time", - artifact = "joda-time:joda-time:2.9.4", - sha1 = "1c295b462f16702ebe720bbb08f62e1ba80da41b", + artifact = "joda-time:joda-time:2.9.9", + sha1 = "f7b520c458572890807d143670c9b24f4de90897", ) maven_jar( @@ -226,8 +226,8 @@ maven_jar( name = "juniversalchardet", - artifact = "com.googlecode.juniversalchardet:juniversalchardet:1.0.3", - sha1 = "cd49678784c46aa8789c060538e0154013bb421b", + artifact = "com.github.albfernandez:juniversalchardet:2.0.0", + sha1 = "28c59f58f5adcc307604602e2aa89e2aca14c554", ) SLF4J_VERS = "1.7.7" @@ -282,8 +282,8 @@ maven_jar( name = "commons_codec", - artifact = "commons-codec:commons-codec:1.4", - sha1 = "4216af16d38465bbab0f3dff8efa14204f7a399a", + artifact = "commons-codec:commons-codec:1.10", + sha1 = "4b95f4897fa13f2cd904aee711aeafc0c5295cd8", ) maven_jar( @@ -294,8 +294,8 @@ maven_jar( name = "commons_compress", - artifact = "org.apache.commons:commons-compress:1.12", - sha1 = "84caa68576e345eb5e7ae61a0e5a9229eb100d7b", + artifact = "org.apache.commons:commons-compress:1.13", + sha1 = "15c5e9584200122924e50203ae210b57616b75ee", ) maven_jar( @@ -336,8 +336,8 @@ maven_jar( name = "commons_validator", - artifact = "commons-validator:commons-validator:1.5.1", - sha1 = "86d05a46e8f064b300657f751b5a98c62807e2a0", + artifact = "commons-validator:commons-validator:1.6", + sha1 = "e989d1e87cdd60575df0765ed5bac65c905d7908", ) maven_jar( @@ -348,8 +348,8 @@ maven_jar( name = "pegdown", - artifact = "org.pegdown:pegdown:1.4.2", - sha1 = "d96db502ed832df867ff5d918f05b51ba3879ea7", + artifact = "org.pegdown:pegdown:1.6.0", + sha1 = "231ae49d913467deb2027d0b8a0b68b231deef4f", ) maven_jar( @@ -434,8 +434,8 @@ maven_jar( name = "auto_value", - artifact = "com.google.auto.value:auto-value:1.4", - sha1 = "6d1448fcd13074bd3658ef915022410b7c48343b", + artifact = "com.google.auto.value:auto-value:1.4.1", + sha1 = "8172ebbd7970188aff304c8a420b9f17168f6f48", ) maven_jar( @@ -444,84 +444,84 @@ sha1 = "18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3", ) -LUCENE_VERS = "5.5.2" +LUCENE_VERS = "5.5.4" maven_jar( name = "lucene_core", artifact = "org.apache.lucene:lucene-core:" + LUCENE_VERS, - sha1 = "de5e5c3161ea01e89f2a09a14391f9b7ed66cdbb", + sha1 = "ab9c77e75cf142aa6e284b310c8395617bd9b19b", ) maven_jar( name = "lucene_analyzers_common", artifact = "org.apache.lucene:lucene-analyzers-common:" + LUCENE_VERS, - sha1 = "f0bc3114a6b43f8e64a33c471d5b9e8ddc51564d", + sha1 = "08ce9d34c8124c80e176e8332ee947480bbb9576", ) maven_jar( name = "lucene_codecs", artifact = "org.apache.lucene:lucene-codecs:" + LUCENE_VERS, - sha1 = "e01fe463d9490bb1b4a6a168e771f7b7255a50b1", + sha1 = "afdad570668469b1734fbd32b8f98561561bed48", ) maven_jar( name = "backward_codecs", artifact = "org.apache.lucene:lucene-backward-codecs:" + LUCENE_VERS, - sha1 = "c5cfcd7a8cf48a0144b61fb991c8e50a0bf868d5", + sha1 = "a933f42e758c54c43083398127ea7342b54d8212", ) maven_jar( name = "lucene_misc", artifact = "org.apache.lucene:lucene-misc:" + LUCENE_VERS, - sha1 = "37bbe5a2fb429499dfbe75d750d1778881fff45d", + sha1 = "a74388857f73614e528ae44d742c60187cb55a5a", ) maven_jar( name = "lucene_queryparser", artifact = "org.apache.lucene:lucene-queryparser:" + LUCENE_VERS, - sha1 = "8ac921563e744463605284c6d9d2d95e1be5b87c", + sha1 = "8a06fad4675473d98d93b61fea529e3f464bf69e", ) maven_jar( name = "lucene_highlighter", artifact = "org.apache.lucene:lucene-highlighter:" + LUCENE_VERS, - sha1 = "d127ac514e9df965ab0b57d92bbe0c68d3d145b8", + sha1 = "433f53f03f1b14337c08d54e507a5410905376fa", ) maven_jar( name = "lucene_join", artifact = "org.apache.lucene:lucene-join:" + LUCENE_VERS, - sha1 = "dac1b322508f3f2696ecc49a97311d34d8382054", + sha1 = "23f9a909a244ed3b28b37c5bb21a6e33e6c0a339", ) maven_jar( name = "lucene_memory", artifact = "org.apache.lucene:lucene-memory:" + LUCENE_VERS, - sha1 = "7409db9863d8fbc265c27793c6cc7511304182c2", + sha1 = "4dbdc2e1a24837722294762a9edb479f79092ab9", ) maven_jar( name = "lucene_sandbox", artifact = "org.apache.lucene:lucene-sandbox:" + LUCENE_VERS, - sha1 = "30a91f120706ba66732d5a974b56c6971b3c8a16", + sha1 = "49498bbb2adc333e98bdca4bf6170ae770cbad11", ) maven_jar( name = "lucene_spatial", artifact = "org.apache.lucene:lucene-spatial:" + LUCENE_VERS, - sha1 = "8ed7a9a43d78222038573dd1c295a61f3c0bb0db", + sha1 = "0217d302dc0ef4d9b8b475ffe327d83c1e0ceba5", ) maven_jar( name = "lucene_suggest", artifact = "org.apache.lucene:lucene-suggest:" + LUCENE_VERS, - sha1 = "e8316b37dddcf2092a54dab2ce6aad0d5ad78585", + sha1 = "0f46dbb3229eed62dff10d008172c885e0e028c8", ) maven_jar( name = "lucene_queries", artifact = "org.apache.lucene:lucene-queries:" + LUCENE_VERS, - sha1 = "692f1ad887cf4e006a23f45019e6de30f3312d3f", + sha1 = "f915357b8b4b43742ab48f1401dedcaa12dfa37a", ) maven_jar( @@ -581,17 +581,17 @@ maven_jar( name = "blame_cache", - artifact = "com/google/gitiles:blame-cache:0.2-1", + artifact = "com/google/gitiles:blame-cache:0.2-2", attach_source = False, repository = GERRIT, - sha1 = "da7977e8b140b63f18054214c1d1b86ffa6896cb", + sha1 = "ac8693b319b3e70506fb27df1b77b598cfdcd00f", ) # Keep this version of Soy synchronized with the version used in Gitiles. maven_jar( name = "soy", - artifact = "com.google.template:soy:2017-02-01", - sha1 = "8638940b207779fe3b75e55b6e65abbefb6af678", + artifact = "com.google.template:soy:2017-04-23", + sha1 = "52f32a5a3801ab97e0909373ef7f73a3460d0802", ) maven_jar( @@ -612,24 +612,24 @@ sha1 = "cd9886f498ee2ab2d994f0c779e5553b2c450416", ) -BC_VERS = "1.56" +BC_VERS = "1.57" maven_jar( name = "bcprov", artifact = "org.bouncycastle:bcprov-jdk15on:" + BC_VERS, - sha1 = "a153c6f9744a3e9dd6feab5e210e1c9861362ec7", + sha1 = "f66a135611d42c992e5745788c3f94eb06464537", ) maven_jar( name = "bcpg", artifact = "org.bouncycastle:bcpg-jdk15on:" + BC_VERS, - sha1 = "9c3f2e7072c8cc1152079b5c25291a9f462631f1", + sha1 = "7b2d587f5e3780b79e1d35af3e84d00634e9420b", ) maven_jar( name = "bcpkix", artifact = "org.bouncycastle:bcpkix-jdk15on:" + BC_VERS, - sha1 = "4648af70268b6fdb24674fb1fd7c1fcc73db1231", + sha1 = "5c96e34bc9bd4cd6870e6d193a99438f1e274ca7", ) # TODO(davido): Remove exlusion of file system provider, when this issue is fixed: @@ -659,6 +659,9 @@ sha1 = "fd369423346b2f1525c413e33f8cf95b09c92cbd", ) +# Note that all of the following org.apache.httpcomponents have newer versions, +# but 4.4.1 is the only version that is available for all of them. +# TODO: Check what combination of new versions are compatible. HTTPCOMP_VERS = "4.4.1" maven_jar( @@ -726,9 +729,10 @@ sha1 = "2862787ce34cb6f385ada891e36ec7f9e7bd0902", ) +# When bumping the easymock version number, make sure to also move powermock to a compatible version maven_jar( name = "easymock", - artifact = "org.easymock:easymock:3.1", # When bumping the version + artifact = "org.easymock:easymock:3.1", sha1 = "3e127311a86fc2e8f550ef8ee4abe094bbcf7e7e", ) @@ -795,60 +799,60 @@ sha1 = "df4b50061e8e4c348ce243b921f53ee63ba9bbe1", ) -JETTY_VERS = "9.3.17.v20170317" +JETTY_VERS = "9.3.18.v20170406" maven_jar( name = "jetty_servlet", artifact = "org.eclipse.jetty:jetty-servlet:" + JETTY_VERS, - sha1 = "ed6986b0d0ca7b9b0f9015c9efb80442e3043a8e", + sha1 = "534e7fa0e4fb6e08f89eb3f6a8c48b4f81ff5738", ) maven_jar( name = "jetty_security", artifact = "org.eclipse.jetty:jetty-security:" + JETTY_VERS, - sha1 = "ca52535569445682d42aaa97c7039442719a0507", + sha1 = "16b900e91b04511f42b706c925c8af6023d2c05e", ) maven_jar( name = "jetty_servlets", artifact = "org.eclipse.jetty:jetty-servlets:" + JETTY_VERS, - sha1 = "6369e945c7da441ac042002e31dbe3ca2068db8f", + sha1 = "f9311d1d8e6124d2792f4db5b29514d0ecf46812", ) maven_jar( name = "jetty_server", artifact = "org.eclipse.jetty:jetty-server:" + JETTY_VERS, - sha1 = "194e9a02e6ba249ef4a3f4bd56b4993087992299", + sha1 = "0a32feea88cba2d43951d22b60861c643454bb3f", ) maven_jar( name = "jetty_jmx", artifact = "org.eclipse.jetty:jetty-jmx:" + JETTY_VERS, - sha1 = "2ba3219f6ee2617ca7f1ec7ae87e4b2128a0c1ce", + sha1 = "f988136dc5aa634afed6c5a35d910ee9599c6c23", ) maven_jar( name = "jetty_continuation", artifact = "org.eclipse.jetty:jetty-continuation:" + JETTY_VERS, - sha1 = "63ff8e2716e20b72787a1dbc666022ef6c1f7b1e", + sha1 = "3c5d89c8204d4a48a360087f95e4cbd4520b5de0", ) maven_jar( name = "jetty_http", artifact = "org.eclipse.jetty:jetty-http:" + JETTY_VERS, - sha1 = "6c02d728e15d4868486254039c867a1ac3e4a52e", + sha1 = "30ece6d732d276442d513b94d914de6fa1075fae", ) maven_jar( name = "jetty_io", artifact = "org.eclipse.jetty:jetty-io:" + JETTY_VERS, - sha1 = "756a8cd2a1cbfb84a94973b6332dd3eccd47c0cd", + sha1 = "36cb411ee89be1b527b0c10747aa3153267fc3ec", ) maven_jar( name = "jetty_util", artifact = "org.eclipse.jetty:jetty-util:" + JETTY_VERS, - sha1 = "b8512ab02819de01f0f5a5c6026163041f579beb", + sha1 = "8600b7d028a38cb462eff338de91390b3ff5040e", ) maven_jar( @@ -879,13 +883,13 @@ maven_jar( name = "codemirror_minified", artifact = "org.webjars.npm:codemirror-minified:" + CM_VERSION, - sha1 = "f84c178b11a188f416b4380bfb2b24f126453d28", + sha1 = "7464d1bf59a36b081981b855a51839bf3a1f302d", ) maven_jar( name = "codemirror_original", artifact = "org.webjars.npm:codemirror:" + CM_VERSION, - sha1 = "5a1f6c10d5aef0b9d2ce513dcc1e2657e4af730d", + sha1 = "8bcf4d541eba5c1c6916cb449fe4baf73d2ebd6f", ) maven_jar( @@ -915,8 +919,8 @@ maven_jar( name = "elasticsearch", - artifact = "org.elasticsearch:elasticsearch:2.4.4", - sha1 = "e69930bc794c539d34778e665d6f8ccbffd42c6f", + artifact = "org.elasticsearch:elasticsearch:2.4.5", + sha1 = "daafe48ae06592029a2fedca1fe2ac0f5eec3185", ) # Java REST client for Elasticsearch. @@ -1092,8 +1096,15 @@ bower_archive( name = "polymer", package = "polymer/polymer", - sha1 = "f2563ed9c8571057814b78d8f6cf275eeb953eeb", - version = "1.7.1", + sha1 = "2c7dd638d55ea91242525139cba18a308b9426d5", + version = "1.9.1", +) + +bower_archive( + name = "polymer-resin", + package = "polymer/polymer-resin", + sha1 = "d759c8c09054a7ec04608a6cb586801c904f79a2", + version = "1.2.6-beta", ) bower_archive( @@ -1122,8 +1133,8 @@ bower_archive( name = "web-component-tester", package = "web-component-tester", - sha1 = "a4a9bc7815a22d143e8f8593e37b3c2028b8c20f", - version = "5.0.0", + sha1 = "4e778f8b7d784ba2a069d83d0cd146125c5c4fcb", + version = "5.0.1", ) # Bower component transitive dependencies.
diff --git a/contrib/abandon_stale.py b/contrib/abandon_stale.py index f62c767..44e7e0e 100755 --- a/contrib/abandon_stale.py +++ b/contrib/abandon_stale.py
@@ -59,7 +59,11 @@ help='gerrit server URL') parser.add_option('-b', '--basic-auth', dest='basic_auth', action='store_true', - help='use HTTP basic authentication instead of digest') + help='(deprecated) use HTTP basic authentication instead' + ' of digest') + parser.add_option('-d', '--digest-auth', dest='digest_auth', + action='store_true', + help='use HTTP digest authentication instead of basic') parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true', help='enable dry-run mode: show stale changes but do ' @@ -115,10 +119,10 @@ message = "Abandoning after %s %s or more of inactivity." % \ (match.group(1), match.group(2)) - if options.basic_auth: - auth_type = HTTPBasicAuthFromNetrc - else: + if options.digest_auth: auth_type = HTTPDigestAuthFromNetrc + else: + auth_type = HTTPBasicAuthFromNetrc try: auth = auth_type(url=options.gerrit_url)
diff --git a/contrib/populate-fixture-data.py b/contrib/populate-fixture-data.py index b77c41a..0e3dffe 100644 --- a/contrib/populate-fixture-data.py +++ b/contrib/populate-fixture-data.py
@@ -46,7 +46,7 @@ PLUGINS_URL = BASE_URL + "plugins/" PROJECTS_URL = BASE_URL + "projects/" -ADMIN_DIGEST = requests.auth.HTTPDigestAuth("admin", "secret") +ADMIN_BASIC_AUTH = requests.auth.HTTPBasicAuth("admin", "secret") # GROUP_ADMIN stores a GroupInfo for the admin group (see Gerrit rest docs) # In addition, GROUP_ADMIN["name"] stores the admin group"s name. @@ -151,8 +151,8 @@ return json_string -def digest_auth(user): - return requests.auth.HTTPDigestAuth(user["username"], user["http_password"]) +def basic_auth(user): + return requests.auth.HTTPBasicAuth(user["username"], user["http_password"]) def fetch_admin_group(): @@ -160,7 +160,7 @@ # Get admin group r = json.loads(clean(requests.get(GROUPS_URL + "?suggest=ad&p=All-Projects", headers=HEADERS, - auth=ADMIN_DIGEST).text)) + auth=ADMIN_BASIC_AUTH).text)) admin_group_name = r.keys()[0] GROUP_ADMIN = r[admin_group_name] GROUP_ADMIN["name"] = admin_group_name @@ -225,7 +225,7 @@ requests.put(GROUPS_URL + g["name"], json.dumps(g), headers=HEADERS, - auth=ADMIN_DIGEST) + auth=ADMIN_BASIC_AUTH) return [g["name"] for g in groups] @@ -247,7 +247,7 @@ requests.put(PROJECTS_URL + p["name"], json.dumps(p), headers=HEADERS, - auth=ADMIN_DIGEST) + auth=ADMIN_BASIC_AUTH) return [p["name"] for p in projects] @@ -256,7 +256,7 @@ requests.put(ACCOUNTS_URL + user["username"], json.dumps(user), headers=HEADERS, - auth=ADMIN_DIGEST) + auth=ADMIN_BASIC_AUTH) def create_change(user, project_name): @@ -270,7 +270,7 @@ requests.post(CHANGES_URL, json.dumps(change), headers=HEADERS, - auth=digest_auth(user)) + auth=basic_auth(user)) def clean_up():
diff --git a/gerrit-acceptance-framework/BUILD b/gerrit-acceptance-framework/BUILD index ca8ffbd..b351c27 100644 --- a/gerrit-acceptance-framework/BUILD +++ b/gerrit-acceptance-framework/BUILD
@@ -38,8 +38,8 @@ "//gerrit-pgm:daemon", "//gerrit-pgm:http-jetty", "//gerrit-pgm:util-nodep", + "//gerrit-server:prolog-common", "//gerrit-server:testutil", - "//gerrit-server/src/main/prolog:common", "//lib:jimfs", "//lib:truth", "//lib:truth-java8-extension",
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml index cd21587..747f5d4 100644 --- a/gerrit-acceptance-framework/pom.xml +++ b/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>2.14.1</version> + <version>2.15-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Acceptance Test Framework</name> <description>Framework for Gerrit's acceptance tests</description>
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java index 22fdceb..ebfb77a 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -21,6 +21,7 @@ import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG; import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.toList; import static org.eclipse.jgit.lib.Constants.HEAD; import static org.eclipse.jgit.lib.Constants.R_TAGS; @@ -64,6 +65,7 @@ import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.ChangeFinder; @@ -89,6 +91,7 @@ import com.google.gerrit.server.index.change.ChangeIndex; import com.google.gerrit.server.index.change.ChangeIndexCollection; import com.google.gerrit.server.index.change.ChangeIndexer; +import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.mail.send.EmailHeader; import com.google.gerrit.server.notedb.ChangeNoteUtil; import com.google.gerrit.server.notedb.ChangeNotes; @@ -97,6 +100,7 @@ import com.google.gerrit.server.project.Util; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.FakeEmailSender; import com.google.gerrit.testutil.FakeEmailSender.Message; @@ -108,6 +112,7 @@ import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.Provider; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -151,6 +156,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.rules.ExpectedException; +import org.junit.rules.RuleChain; import org.junit.rules.TemporaryFolder; import org.junit.rules.TestRule; import org.junit.runner.Description; @@ -162,98 +168,16 @@ private static GerritServer commonServer; @ConfigSuite.Parameter public Config baseConfig; - @ConfigSuite.Name private String configName; - @Inject protected AllProjectsName allProjects; - - @Inject protected AccountCreator accounts; - - @Inject private SchemaFactory<ReviewDb> reviewDbProvider; - - @Inject protected GerritApi gApi; - - @Inject protected AcceptanceTestRequestScope atrScope; - - @Inject protected AccountCache accountCache; - - @Inject protected IdentifiedUser.GenericFactory identifiedUserFactory; - - @Inject protected PushOneCommit.Factory pushFactory; - - @Inject protected MetaDataUpdate.Server metaDataUpdateFactory; - - @Inject protected ProjectCache projectCache; - - @Inject protected GroupCache groupCache; - - @Inject protected GitRepositoryManager repoManager; - - @Inject protected ChangeIndexer indexer; - - @Inject protected Provider<InternalChangeQuery> queryProvider; - - @Inject @CanonicalWebUrl protected Provider<String> canonicalWebUrl; - - @Inject @GerritServerConfig protected Config cfg; - - @Inject protected PluginConfigFactory pluginConfig; - - @Inject private InProcessProtocol inProcessProtocol; - - @Inject private Provider<AnonymousUser> anonymousUser; - - @Inject @GerritPersonIdent protected Provider<PersonIdent> serverIdent; - - @Inject protected ChangeData.Factory changeDataFactory; - - @Inject protected PatchSetUtil psUtil; - - @Inject protected ChangeFinder changeFinder; - - @Inject protected Revisions revisions; - - @Inject protected FakeEmailSender sender; - - @Inject protected ChangeNoteUtil changeNoteUtil; - - @Inject protected ChangeResource.Factory changeResourceFactory; - - @Inject protected SystemGroupBackend systemGroupBackend; - - @Inject private EventRecorder.Factory eventRecorderFactory; - - @Inject private ChangeIndexCollection changeIndexes; - - protected TestRepository<InMemoryRepository> testRepo; - protected GerritServer server; - protected TestAccount admin; - protected TestAccount user; - protected RestSession adminRestSession; - protected RestSession userRestSession; - protected SshSession adminSshSession; - protected SshSession userSshSession; - protected ReviewDb db; - protected Project.NameKey project; - protected EventRecorder eventRecorder; - - @Inject protected TestNotesMigration notesMigration; - - @Inject protected ChangeNotes.Factory notesFactory; - - @Inject protected Abandon changeAbandoner; - @Rule public ExpectedException exception = ExpectedException.none(); - private String resourcePrefix; - private List<Repository> toClose; - private boolean useSsh; + protected final TemporaryFolder tempSiteDir = new TemporaryFolder(); - @Rule - public TestRule testRunner = + private final TestRule testRunner = new TestRule() { @Override - public Statement apply(final Statement base, final Description description) { + public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { @@ -268,7 +192,60 @@ } }; - @Rule public TemporaryFolder tempSiteDir = new TemporaryFolder(); + @Rule public RuleChain ruleChain = RuleChain.outerRule(tempSiteDir).around(testRunner); + + @Inject @CanonicalWebUrl protected Provider<String> canonicalWebUrl; + @Inject @GerritPersonIdent protected Provider<PersonIdent> serverIdent; + @Inject @GerritServerConfig protected Config cfg; + @Inject protected AcceptanceTestRequestScope atrScope; + @Inject protected AccountCache accountCache; + @Inject protected AccountCreator accountCreator; + @Inject protected AllProjectsName allProjects; + @Inject protected BatchUpdate.Factory batchUpdateFactory; + @Inject protected ChangeData.Factory changeDataFactory; + @Inject protected ChangeFinder changeFinder; + @Inject protected ChangeIndexer indexer; + @Inject protected ChangeNoteUtil changeNoteUtil; + @Inject protected ChangeResource.Factory changeResourceFactory; + @Inject protected FakeEmailSender sender; + @Inject protected GerritApi gApi; + @Inject protected GitRepositoryManager repoManager; + @Inject protected GroupCache groupCache; + @Inject protected IdentifiedUser.GenericFactory identifiedUserFactory; + @Inject protected MetaDataUpdate.Server metaDataUpdateFactory; + @Inject protected PatchSetUtil psUtil; + @Inject protected ProjectCache projectCache; + @Inject protected Provider<InternalChangeQuery> queryProvider; + @Inject protected PushOneCommit.Factory pushFactory; + @Inject protected PluginConfigFactory pluginConfig; + @Inject protected Revisions revisions; + @Inject protected SystemGroupBackend systemGroupBackend; + @Inject protected TestNotesMigration notesMigration; + @Inject protected ChangeNotes.Factory notesFactory; + @Inject protected Abandon changeAbandoner; + + protected EventRecorder eventRecorder; + protected GerritServer server; + protected Project.NameKey project; + protected RestSession adminRestSession; + protected RestSession userRestSession; + protected ReviewDb db; + protected SshSession adminSshSession; + protected SshSession userSshSession; + protected TestAccount admin; + protected TestAccount user; + protected TestRepository<InMemoryRepository> testRepo; + protected String resourcePrefix; + protected Description description; + protected boolean testRequiresSsh; + + @Inject private ChangeIndexCollection changeIndexes; + @Inject private EventRecorder.Factory eventRecorderFactory; + @Inject private InProcessProtocol inProcessProtocol; + @Inject private Provider<AnonymousUser> anonymousUser; + @Inject private SchemaFactory<ReviewDb> reviewDbProvider; + + private List<Repository> toClose; @Before public void clearSender() { @@ -282,7 +259,7 @@ @Before public void assumeSshIfRequired() { - if (useSsh) { + 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. @@ -299,7 +276,7 @@ public static void stopCommonServer() throws Exception { if (commonServer != null) { try { - commonServer.stop(); + commonServer.close(); } finally { commonServer = null; } @@ -332,6 +309,7 @@ } protected void beforeTest(Description description) throws Exception { + this.description = description; GerritServer.Description classDesc = GerritServer.Description.forTestClass(description, configName); GerritServer.Description methodDesc = @@ -341,19 +319,18 @@ baseConfig.setInt("receive", null, "changeUpdateThreads", 4); if (classDesc.equals(methodDesc) && !classDesc.sandboxed() && !methodDesc.sandboxed()) { if (commonServer == null) { - commonServer = GerritServer.start(classDesc, baseConfig); + commonServer = GerritServer.initAndStart(classDesc, baseConfig); } server = commonServer; } else { - server = GerritServer.start(methodDesc, baseConfig); + server = GerritServer.initAndStart(methodDesc, baseConfig); } server.getTestInjector().injectMembers(this); - notesMigration.setFromEnv(); Transport.register(inProcessProtocol); toClose = Collections.synchronizedList(new ArrayList<Repository>()); - admin = accounts.admin(); - user = accounts.user(); + admin = accountCreator.admin(); + user = accountCreator.user(); // Evict cached user state in case tests modify it. accountCache.evict(admin.getId()); @@ -364,22 +341,20 @@ db = reviewDbProvider.open(); - if (classDesc.useSsh() || methodDesc.useSsh()) { - useSsh = true; - if (SshMode.useSsh() && (adminSshSession == null || userSshSession == null)) { - // Create Ssh sessions - initSsh(admin); - Context ctx = newRequestContext(user); - atrScope.set(ctx); - userSshSession = ctx.getSession(); - userSshSession.open(); - ctx = newRequestContext(admin); - atrScope.set(ctx); - adminSshSession = ctx.getSession(); - adminSshSession.open(); - } - } else { - useSsh = false; + testRequiresSsh = classDesc.useSshAnnotation() || methodDesc.useSshAnnotation(); + if (testRequiresSsh + && SshMode.useSsh() + && (adminSshSession == null || userSshSession == null)) { + // Create Ssh sessions + initSsh(admin); + Context ctx = newRequestContext(user); + atrScope.set(ctx); + userSshSession = ctx.getSession(); + userSshSession.open(); + ctx = newRequestContext(admin); + atrScope.set(ctx); + adminSshSession = ctx.getSession(); + adminSshSession.open(); } resourcePrefix = @@ -395,7 +370,7 @@ private TestAccount getCloneAsAccount(Description description) { TestProjectInput ann = description.getAnnotation(TestProjectInput.class); - return accounts.get(ann != null ? ann.cloneAs() : "admin"); + return accountCreator.get(ann != null ? ann.cloneAs() : "admin"); } private ProjectInput projectInput(Description description) { @@ -495,12 +470,22 @@ protected TestRepository<InMemoryRepository> cloneProject( Project.NameKey p, TestAccount testAccount) throws Exception { + return GitUtil.cloneProject(p, registerRepoConnection(p, testAccount)); + } + + /** + * Register a repository connection over the test protocol. + * + * @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 { InProcessProtocol.Context ctx = new InProcessProtocol.Context( reviewDbProvider, identifiedUserFactory, testAccount.getId(), p); Repository repo = repoManager.openRepository(p); toClose.add(repo); - return GitUtil.cloneProject(p, inProcessProtocol.register(ctx, repo).toString()); + return inProcessProtocol.register(ctx, repo).toString(); } protected void afterTest() throws Exception { @@ -516,9 +501,10 @@ userSshSession.close(); } if (server != commonServer) { - server.stop(); + server.close(); server = null; } + notesMigration.resetFromEnv(); } protected TestRepository<?>.CommitBuilder commitBuilder() throws Exception { @@ -597,6 +583,10 @@ return pushTo("refs/drafts/master"); } + protected PushOneCommit.Result createWorkInProgressChange() throws Exception { + return pushTo("refs/for/master%wip"); + } + protected PushOneCommit.Result createChange(String subject, String fileName, String content) throws Exception { PushOneCommit push = @@ -823,24 +813,24 @@ } } - protected void deny(String permission, AccountGroup.UUID id, String ref) throws Exception { - deny(project, permission, id, ref); + protected void deny(String ref, String permission, AccountGroup.UUID id) throws Exception { + deny(project, ref, permission, id); } - protected void deny(Project.NameKey p, String permission, AccountGroup.UUID id, String ref) + protected void deny(Project.NameKey p, String ref, String permission, AccountGroup.UUID id) throws Exception { ProjectConfig cfg = projectCache.checkedGet(p).getConfig(); Util.deny(cfg, permission, id, ref); saveProjectConfig(p, cfg); } - protected PermissionRule block(String permission, AccountGroup.UUID id, String ref) + protected PermissionRule block(String ref, String permission, AccountGroup.UUID id) throws Exception { - return block(permission, id, ref, project); + return block(project, ref, permission, id); } protected PermissionRule block( - String permission, AccountGroup.UUID id, String ref, Project.NameKey project) + Project.NameKey project, String ref, String permission, AccountGroup.UUID id) throws Exception { ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); PermissionRule rule = Util.block(cfg, permission, id, ref); @@ -860,21 +850,21 @@ saveProjectConfig(project, cfg); } - protected void grant(String permission, Project.NameKey project, String ref) + protected void grant(Project.NameKey project, String ref, String permission) throws RepositoryNotFoundException, IOException, ConfigInvalidException { - grant(permission, project, ref, false); + grant(project, ref, permission, false); } - protected void grant(String permission, Project.NameKey project, String ref, boolean force) + protected void grant(Project.NameKey project, String ref, String permission, boolean force) throws RepositoryNotFoundException, IOException, ConfigInvalidException { AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators")); - grant(permission, project, ref, force, adminGroup.getGroupUUID()); + grant(project, ref, permission, force, adminGroup.getGroupUUID()); } protected void grant( - String permission, Project.NameKey project, String ref, + String permission, boolean force, AccountGroup.UUID groupUUID) throws RepositoryNotFoundException, IOException, ConfigInvalidException { @@ -915,7 +905,7 @@ } } - protected void removePermission(String permission, Project.NameKey project, String ref) + protected void removePermission(Project.NameKey project, String ref, String permission) throws IOException, ConfigInvalidException { try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) { md.setMessage(String.format("Remove %s on %s", permission, ref)); @@ -929,7 +919,7 @@ } protected void blockRead(String ref) throws Exception { - block(Permission.READ, REGISTERED_USERS, ref); + block(ref, Permission.READ, REGISTERED_USERS); } protected void blockForgeCommitter(Project.NameKey project, String ref) throws Exception { @@ -1043,10 +1033,10 @@ } protected void grantTagPermissions() throws Exception { - grant(Permission.CREATE, project, R_TAGS + "*"); - grant(Permission.DELETE, project, R_TAGS + ""); - grant(Permission.CREATE_TAG, project, R_TAGS + "*"); - grant(Permission.CREATE_SIGNED_TAG, project, R_TAGS + "*"); + grant(project, R_TAGS + "*", Permission.CREATE); + grant(project, R_TAGS + "", Permission.DELETE); + grant(project, R_TAGS + "*", Permission.CREATE_TAG); + grant(project, R_TAGS + "*", Permission.CREATE_SIGNED_TAG); } protected void assertMailReplyTo(Message message, String email) throws Exception { @@ -1098,6 +1088,8 @@ /** * Fetches each bundle into a newly cloned repository, then it applies the bundle, and returns the * resulting tree id. + * + * <p>Omits NoteDb meta refs. */ protected Map<Branch.NameKey, ObjectId> fetchFromBundles(BinaryResult bundles) throws Exception { assertThat(bundles.getContentType()).isEqualTo("application/x-zip"); @@ -1131,11 +1123,12 @@ NullProgressMonitor.INSTANCE, Arrays.asList(new RefSpec("refs/*:refs/preview/*"))); for (Ref r : fr.getAdvertisedRefs()) { - String branchName = r.getName(); - Branch.NameKey n = new Branch.NameKey(proj, branchName); - + String refName = r.getName(); + if (RefNames.isNoteDbMetaRef(refName)) { + continue; + } RevCommit c = localRepo.getRevWalk().parseCommit(r.getObjectId()); - ret.put(n, c.getTree().copy()); + ret.put(new Branch.NameKey(proj, refName), c.getTree().copy()); } } } @@ -1211,8 +1204,8 @@ protected TestRepository<?> createProjectWithPush( String name, @Nullable Project.NameKey parent, SubmitType submitType) throws Exception { Project.NameKey project = createProject(name, parent, true, submitType); - grant(Permission.PUSH, project, "refs/heads/*"); - grant(Permission.SUBMIT, project, "refs/for/refs/heads/*"); + grant(project, "refs/heads/*", Permission.PUSH); + grant(project, "refs/for/refs/heads/*", Permission.SUBMIT); return cloneProject(project); } @@ -1228,21 +1221,29 @@ } protected void assertNotifyTo(TestAccount expected) { + assertNotifyTo(expected.emailAddress); + } + + protected void assertNotifyTo(Address expected) { assertThat(sender.getMessages()).hasSize(1); Message m = sender.getMessages().get(0); - assertThat(m.rcpt()).containsExactly(expected.emailAddress); + assertThat(m.rcpt()).containsExactly(expected); assertThat(((EmailHeader.AddressList) m.headers().get("To")).getAddressList()) - .containsExactly(expected.emailAddress); + .containsExactly(expected); assertThat(m.headers().get("CC").isEmpty()).isTrue(); } protected void assertNotifyCc(TestAccount expected) { + assertNotifyCc(expected.emailAddress); + } + + protected void assertNotifyCc(Address expected) { assertThat(sender.getMessages()).hasSize(1); Message m = sender.getMessages().get(0); - assertThat(m.rcpt()).containsExactly(expected.emailAddress); + assertThat(m.rcpt()).containsExactly(expected); assertThat(m.headers().get("To").isEmpty()).isTrue(); assertThat(((EmailHeader.AddressList) m.headers().get("CC")).getAddressList()) - .containsExactly(expected.emailAddress); + .containsExactly(expected); } protected void assertNotifyBcc(TestAccount expected) { @@ -1253,15 +1254,78 @@ assertThat(m.headers().get("CC").isEmpty()).isTrue(); } - protected void watch(String project, String filter) throws RestApiException { - List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(); + protected interface ProjectWatchInfoConfiguration { + void configure(ProjectWatchInfo pwi); + } + + protected void watch(String project, ProjectWatchInfoConfiguration config) + throws RestApiException { ProjectWatchInfo pwi = new ProjectWatchInfo(); pwi.project = project; - pwi.filter = filter; - pwi.notifyAbandonedChanges = true; - pwi.notifyNewChanges = true; - pwi.notifyAllComments = true; - projectsToWatch.add(pwi); - gApi.accounts().self().setWatchedProjects(projectsToWatch); + config.configure(pwi); + gApi.accounts().self().setWatchedProjects(ImmutableList.of(pwi)); + } + + protected void watch(PushOneCommit.Result r, ProjectWatchInfoConfiguration config) + throws OrmException, RestApiException { + watch(r.getChange().project().get(), config); + } + + protected void watch(String project, String filter) throws RestApiException { + watch( + project, + pwi -> { + pwi.filter = filter; + pwi.notifyAbandonedChanges = true; + pwi.notifyNewChanges = true; + pwi.notifyAllComments = true; + }); + } + + protected void watch(String project) throws RestApiException { + watch(project, (String) null); + } + + protected void assertContent(PushOneCommit.Result pushResult, String path, String expectedContent) + throws Exception { + BinaryResult bin = + gApi.changes() + .id(pushResult.getChangeId()) + .revision(pushResult.getCommit().name()) + .file(path) + .content(); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + bin.writeTo(os); + String res = new String(os.toByteArray(), UTF_8); + assertThat(res).isEqualTo(expectedContent); + } + + protected RevCommit createNewCommitWithoutChangeId(String branch, String file, String content) + throws Exception { + try (Repository repo = repoManager.openRepository(project); + RevWalk walk = new RevWalk(repo)) { + Ref ref = repo.exactRef(branch); + RevCommit tip = null; + if (ref != null) { + tip = walk.parseCommit(ref.getObjectId()); + } + TestRepository<?> testSrcRepo = new TestRepository<>(repo); + TestRepository<?>.BranchBuilder builder = testSrcRepo.branch(branch); + RevCommit revCommit = + tip == null + ? builder.commit().message("commit 1").add(file, content).create() + : builder.commit().parent(tip).message("commit 1").add(file, content).create(); + assertThat(GitUtil.getChangeId(testSrcRepo, revCommit).isPresent()).isFalse(); + return revCommit; + } + } + + protected RevCommit parseCurrentRevision(RevWalk rw, PushOneCommit.Result r) throws Exception { + return parseCurrentRevision(rw, r.getChangeId()); + } + + protected RevCommit parseCurrentRevision(RevWalk rw, String changeId) throws Exception { + return rw.parseCommit( + ObjectId.fromString(get(changeId, ListChangesOption.CURRENT_REVISION).currentRevision)); } }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractNotificationTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractNotificationTest.java new file mode 100644 index 0000000..b2e7415 --- /dev/null +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractNotificationTest.java
@@ -0,0 +1,528 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.truth.Truth.assertAbout; +import static com.google.gerrit.extensions.api.changes.RecipientType.BCC; +import static com.google.gerrit.extensions.api.changes.RecipientType.CC; +import static com.google.gerrit.extensions.api.changes.RecipientType.TO; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.api.changes.ReviewResult; +import com.google.gerrit.extensions.api.projects.ConfigInput; +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.account.WatchConfig.NotifyType; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.send.EmailHeader; +import com.google.gerrit.server.mail.send.EmailHeader.AddressList; +import com.google.gerrit.testutil.FakeEmailSender; +import com.google.gerrit.testutil.FakeEmailSender.Message; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.eclipse.jgit.junit.TestRepository; +import org.junit.After; +import org.junit.Before; + +public abstract class AbstractNotificationTest extends AbstractDaemonTest { + @Before + public void enableReviewerByEmail() throws Exception { + setApiUser(admin); + ConfigInput conf = new ConfigInput(); + conf.enableReviewerByEmail = InheritableBoolean.TRUE; + gApi.projects().name(project.get()).config(conf); + } + + private static final SubjectFactory<FakeEmailSenderSubject, FakeEmailSender> + FAKE_EMAIL_SENDER_SUBJECT_FACTORY = + new SubjectFactory<FakeEmailSenderSubject, FakeEmailSender>() { + @Override + public FakeEmailSenderSubject getSubject( + FailureStrategy failureStrategy, FakeEmailSender target) { + return new FakeEmailSenderSubject(failureStrategy, target); + } + }; + + protected static FakeEmailSenderSubject assertThat(FakeEmailSender sender) { + return assertAbout(FAKE_EMAIL_SENDER_SUBJECT_FACTORY).that(sender); + } + + protected void setEmailStrategy(TestAccount account, EmailStrategy strategy) throws Exception { + setEmailStrategy(account, strategy, true); + } + + protected void setEmailStrategy(TestAccount account, EmailStrategy strategy, boolean record) + throws Exception { + if (record) { + accountsModifyingEmailStrategy.add(account); + } + setApiUser(account); + GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences(); + prefs.emailStrategy = strategy; + gApi.accounts().self().setPreferences(prefs); + } + + protected static class FakeEmailSenderSubject + extends Subject<FakeEmailSenderSubject, FakeEmailSender> { + private Message message; + private StagedUsers users; + private Map<RecipientType, List<String>> recipients = new HashMap<>(); + private Set<String> accountedFor = new HashSet<>(); + + FakeEmailSenderSubject(FailureStrategy failureStrategy, FakeEmailSender target) { + super(failureStrategy, target); + } + + public FakeEmailSenderSubject notSent() { + if (actual().peekMessage() != null) { + fail("a message wasn't sent"); + } + return this; + } + + public FakeEmailSenderSubject sent(String messageType, StagedUsers users) { + message = actual().nextMessage(); + if (message == null) { + fail("a message was sent"); + } + recipients = new HashMap<>(); + recipients.put(TO, parseAddresses(message, "To")); + recipients.put(CC, parseAddresses(message, "CC")); + recipients.put( + BCC, + message + .rcpt() + .stream() + .map(Address::getEmail) + .filter(e -> !recipients.get(TO).contains(e) && !recipients.get(CC).contains(e)) + .collect(Collectors.toList())); + this.users = users; + if (!message.headers().containsKey("X-Gerrit-MessageType")) { + fail("a message was sent with X-Gerrit-MessageType header"); + } + EmailHeader header = message.headers().get("X-Gerrit-MessageType"); + if (!header.equals(new EmailHeader.String(messageType))) { + fail("message of type " + messageType + " was sent; X-Gerrit-MessageType is " + header); + } + + // Return a named subject that displays a human-readable table of + // recipients. + return named(recipientMapToString(recipients, e -> users.emailToName(e))); + } + + private static String recipientMapToString( + Map<RecipientType, List<String>> recipients, Function<String, String> emailToName) { + StringBuilder buf = new StringBuilder(); + buf.append('['); + for (RecipientType type : ImmutableList.of(TO, CC, BCC)) { + buf.append('\n'); + buf.append(type); + buf.append(':'); + String delim = " "; + for (String r : recipients.get(type)) { + buf.append(delim); + buf.append(emailToName.apply(r)); + delim = ", "; + } + } + buf.append("\n]"); + return buf.toString(); + } + + List<String> parseAddresses(Message msg, String headerName) { + EmailHeader header = msg.headers().get(headerName); + if (header == null) { + return ImmutableList.of(); + } + Truth.assertThat(header).isInstanceOf(AddressList.class); + AddressList addrList = (AddressList) header; + return addrList.getAddressList().stream().map(Address::getEmail).collect(Collectors.toList()); + } + + public FakeEmailSenderSubject to(String... emails) { + return rcpt(users.supportReviewersByEmail ? TO : null, emails); + } + + public FakeEmailSenderSubject cc(String... emails) { + return rcpt(users.supportReviewersByEmail ? CC : null, emails); + } + + public FakeEmailSenderSubject bcc(String... emails) { + return rcpt(users.supportReviewersByEmail ? BCC : null, emails); + } + + private FakeEmailSenderSubject rcpt(@Nullable RecipientType type, String[] emails) { + for (String email : emails) { + rcpt(type, email); + } + return this; + } + + private void rcpt(@Nullable RecipientType type, String email) { + rcpt(TO, email, TO.equals(type)); + rcpt(CC, email, CC.equals(type)); + rcpt(BCC, email, BCC.equals(type)); + } + + private void rcpt(@Nullable RecipientType type, String email, boolean expected) { + if (recipients.get(type).contains(email) != expected) { + fail( + expected ? "notifies" : "doesn't notify", + "]\n" + type + ": " + users.emailToName(email) + "\n]"); + } + if (expected) { + accountedFor.add(email); + } + } + + public FakeEmailSenderSubject noOneElse() { + for (Map.Entry<NotifyType, TestAccount> watchEntry : users.watchers.entrySet()) { + if (!accountedFor.contains(watchEntry.getValue().email)) { + notTo(watchEntry.getKey()); + } + } + + Map<RecipientType, List<String>> unaccountedFor = new HashMap<>(); + boolean ok = true; + for (Map.Entry<RecipientType, List<String>> entry : recipients.entrySet()) { + unaccountedFor.put(entry.getKey(), new ArrayList<>()); + for (String address : entry.getValue()) { + if (!accountedFor.contains(address)) { + unaccountedFor.get(entry.getKey()).add(address); + ok = false; + } + } + } + if (!ok) { + fail( + "was fully tested, missing assertions for: " + + recipientMapToString(unaccountedFor, e -> users.emailToName(e))); + } + return this; + } + + public FakeEmailSenderSubject notTo(String... emails) { + return rcpt(null, emails); + } + + public FakeEmailSenderSubject to(TestAccount... accounts) { + return rcpt(TO, accounts); + } + + public FakeEmailSenderSubject cc(TestAccount... accounts) { + return rcpt(CC, accounts); + } + + public FakeEmailSenderSubject bcc(TestAccount... accounts) { + return rcpt(BCC, accounts); + } + + public FakeEmailSenderSubject notTo(TestAccount... accounts) { + return rcpt(null, accounts); + } + + private FakeEmailSenderSubject rcpt(@Nullable RecipientType type, TestAccount[] accounts) { + for (TestAccount account : accounts) { + rcpt(type, account); + } + return this; + } + + private void rcpt(@Nullable RecipientType type, TestAccount account) { + rcpt(type, account.email); + } + + public FakeEmailSenderSubject to(NotifyType... watches) { + return rcpt(TO, watches); + } + + public FakeEmailSenderSubject cc(NotifyType... watches) { + return rcpt(CC, watches); + } + + public FakeEmailSenderSubject bcc(NotifyType... watches) { + return rcpt(BCC, watches); + } + + public FakeEmailSenderSubject notTo(NotifyType... watches) { + return rcpt(null, watches); + } + + private FakeEmailSenderSubject rcpt(@Nullable RecipientType type, NotifyType[] watches) { + for (NotifyType watch : watches) { + rcpt(type, watch); + } + return this; + } + + private void rcpt(@Nullable RecipientType type, NotifyType watch) { + if (!users.watchers.containsKey(watch)) { + fail("configured to watch", watch); + } + rcpt(type, users.watchers.get(watch)); + } + } + + private static final Map<String, StagedUsers> stagedUsers = new HashMap<>(); + + // TestAccount doesn't implement hashCode/equals, so this set is according + // to object identity. That's fine for our purposes. + private Set<TestAccount> accountsModifyingEmailStrategy = new HashSet<>(); + + @After + public void resetEmailStrategies() throws Exception { + for (TestAccount account : accountsModifyingEmailStrategy) { + setEmailStrategy(account, EmailStrategy.ENABLED, false); + } + accountsModifyingEmailStrategy.clear(); + } + + protected class StagedUsers { + public final TestAccount owner; + public final TestAccount author; + public final TestAccount uploader; + public final TestAccount reviewer; + public final TestAccount ccer; + public final TestAccount starrer; + public final TestAccount assignee; + public final TestAccount watchingProjectOwner; + public final String reviewerByEmail = "reviewerByEmail@example.com"; + public final String ccerByEmail = "ccByEmail@example.com"; + private final Map<NotifyType, TestAccount> watchers = new HashMap<>(); + private final Map<String, TestAccount> accountsByEmail = new HashMap<>(); + boolean supportReviewersByEmail; + + private String usersCacheKey() { + return description.getClassName(); + } + + private TestAccount evictAndCopy(TestAccount account) throws IOException { + accountCache.evict(account.id); + return account; + } + + public StagedUsers() throws Exception { + synchronized (stagedUsers) { + if (stagedUsers.containsKey(usersCacheKey())) { + StagedUsers existing = stagedUsers.get(usersCacheKey()); + owner = evictAndCopy(existing.owner); + author = evictAndCopy(existing.author); + uploader = evictAndCopy(existing.uploader); + reviewer = evictAndCopy(existing.reviewer); + ccer = evictAndCopy(existing.ccer); + starrer = evictAndCopy(existing.starrer); + assignee = evictAndCopy(existing.assignee); + watchingProjectOwner = evictAndCopy(existing.watchingProjectOwner); + watchers.putAll(existing.watchers); + return; + } + + owner = testAccount("owner"); + reviewer = testAccount("reviewer"); + author = testAccount("author"); + uploader = testAccount("uploader"); + ccer = testAccount("ccer"); + starrer = testAccount("starrer"); + assignee = testAccount("assignee"); + + watchingProjectOwner = testAccount("watchingProjectOwner", "Administrators"); + setApiUser(watchingProjectOwner); + watch(allProjects.get(), pwi -> pwi.notifyNewChanges = true); + + for (NotifyType watch : NotifyType.values()) { + if (watch == NotifyType.ALL) { + continue; + } + TestAccount watcher = testAccount(watch.toString()); + setApiUser(watcher); + watch( + allProjects.get(), + pwi -> { + pwi.notifyAllComments = watch.equals(NotifyType.ALL_COMMENTS); + pwi.notifyAbandonedChanges = watch.equals(NotifyType.ABANDONED_CHANGES); + pwi.notifyNewChanges = watch.equals(NotifyType.NEW_CHANGES); + pwi.notifyNewPatchSets = watch.equals(NotifyType.NEW_PATCHSETS); + pwi.notifySubmittedChanges = watch.equals(NotifyType.SUBMITTED_CHANGES); + }); + watchers.put(watch, watcher); + } + + stagedUsers.put(usersCacheKey(), this); + } + } + + private String email(String username) { + // Email validator rejects usernames longer than 64 bytes. + if (username.length() > 64) { + username = username.substring(username.length() - 64); + if (username.startsWith(".")) { + username = username.substring(1); + } + } + return username + "@example.com"; + } + + public TestAccount testAccount(String name) throws Exception { + String username = name(name); + TestAccount account = accountCreator.create(username, email(username), name); + accountsByEmail.put(account.email, account); + return account; + } + + public TestAccount testAccount(String name, String groupName) throws Exception { + String username = name(name); + TestAccount account = accountCreator.create(username, email(username), name, groupName); + accountsByEmail.put(account.email, account); + return account; + } + + String emailToName(String email) { + if (accountsByEmail.containsKey(email)) { + return accountsByEmail.get(email).fullName; + } + return email; + } + + protected void addReviewers(PushOneCommit.Result r) throws Exception { + ReviewInput in = + ReviewInput.noScore() + .reviewer(reviewer.email) + .reviewer(reviewerByEmail) + .reviewer(ccer.email, ReviewerState.CC, false) + .reviewer(ccerByEmail, ReviewerState.CC, false); + ReviewResult result = gApi.changes().id(r.getChangeId()).revision("current").review(in); + supportReviewersByEmail = true; + if (result.reviewers.values().stream().anyMatch(v -> v.error != null)) { + supportReviewersByEmail = false; + in = + ReviewInput.noScore() + .reviewer(reviewer.email) + .reviewer(ccer.email, ReviewerState.CC, false); + result = gApi.changes().id(r.getChangeId()).revision("current").review(in); + } + Truth.assertThat(result.reviewers.values().stream().allMatch(v -> v.error == null)).isTrue(); + } + } + + protected interface PushOptionGenerator { + List<String> pushOptions(StagedUsers users); + } + + protected class StagedPreChange extends StagedUsers { + public final TestRepository<?> repo; + protected final PushOneCommit.Result result; + public final String changeId; + + StagedPreChange(String ref) throws Exception { + this(ref, null); + } + + StagedPreChange(String ref, @Nullable PushOptionGenerator pushOptionGenerator) + throws Exception { + super(); + List<String> pushOptions = null; + if (pushOptionGenerator != null) { + pushOptions = pushOptionGenerator.pushOptions(this); + } + if (pushOptions != null) { + ref = ref + '%' + Joiner.on(',').join(pushOptions); + } + setApiUser(owner); + repo = cloneProject(project, owner); + PushOneCommit push = pushFactory.create(db, owner.getIdent(), repo); + result = push.to(ref); + result.assertOkStatus(); + changeId = result.getChangeId(); + } + } + + protected StagedPreChange stagePreChange(String ref) throws Exception { + return new StagedPreChange(ref); + } + + protected StagedPreChange stagePreChange( + String ref, @Nullable PushOptionGenerator pushOptionGenerator) throws Exception { + return new StagedPreChange(ref, pushOptionGenerator); + } + + protected class StagedChange extends StagedPreChange { + StagedChange(String ref) throws Exception { + super(ref); + + setApiUser(starrer); + gApi.accounts().self().starChange(result.getChangeId()); + + setApiUser(owner); + addReviewers(result); + sender.clear(); + } + } + + protected StagedChange stageReviewableChange() throws Exception { + return new StagedChange("refs/for/master"); + } + + protected StagedChange stageWipChange() throws Exception { + return new StagedChange("refs/for/master%wip"); + } + + protected StagedChange stageReviewableWipChange() throws Exception { + StagedChange sc = stageReviewableChange(); + setApiUser(sc.owner); + gApi.changes().id(sc.changeId).setWorkInProgress(); + return sc; + } + + protected StagedChange stageAbandonedReviewableChange() throws Exception { + StagedChange sc = stageReviewableChange(); + setApiUser(sc.owner); + gApi.changes().id(sc.changeId).abandon(); + sender.clear(); + return sc; + } + + protected StagedChange stageAbandonedReviewableWipChange() throws Exception { + StagedChange sc = stageReviewableWipChange(); + setApiUser(sc.owner); + gApi.changes().id(sc.changeId).abandon(); + sender.clear(); + return sc; + } + + protected StagedChange stageAbandonedWipChange() throws Exception { + StagedChange sc = stageWipChange(); + setApiUser(sc.owner); + gApi.changes().id(sc.changeId).abandon(); + sender.clear(); + return sc; + } +}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java index b6547ef..987cb97 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AcceptanceTestRequestScope.java
@@ -191,7 +191,7 @@ static final Scope REQUEST = new Scope() { @Override - public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) { + public <T> Provider<T> scope(Key<T> key, Provider<T> creator) { return new Provider<T>() { @Override public T get() {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java index 20ae2d1..918b7a6 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -18,19 +18,19 @@ import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.US_ASCII; -import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.AccountByEmailCache; import com.google.gerrit.server.account.AccountCache; -import com.google.gerrit.server.account.ExternalId; -import com.google.gerrit.server.account.ExternalIdsUpdate; +import com.google.gerrit.server.account.AccountsUpdate; import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.account.VersionedAuthorizedKeys; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; import com.google.gerrit.server.ssh.SshKeyCache; -import com.google.gerrit.testutil.SshMode; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -50,34 +50,45 @@ private final Map<String, TestAccount> accounts; private final SchemaFactory<ReviewDb> reviewDbProvider; + private final AccountsUpdate.Server accountsUpdate; private final VersionedAuthorizedKeys.Accessor authorizedKeys; private final GroupCache groupCache; private final SshKeyCache sshKeyCache; private final AccountCache accountCache; private final AccountByEmailCache byEmailCache; private final ExternalIdsUpdate.Server externalIdsUpdate; + private final boolean sshEnabled; @Inject AccountCreator( SchemaFactory<ReviewDb> schema, + AccountsUpdate.Server accountsUpdate, VersionedAuthorizedKeys.Accessor authorizedKeys, GroupCache groupCache, SshKeyCache sshKeyCache, AccountCache accountCache, AccountByEmailCache byEmailCache, - ExternalIdsUpdate.Server externalIdsUpdate) { + ExternalIdsUpdate.Server externalIdsUpdate, + @SshEnabled boolean sshEnabled) { accounts = new HashMap<>(); reviewDbProvider = schema; + this.accountsUpdate = accountsUpdate; this.authorizedKeys = authorizedKeys; this.groupCache = groupCache; this.sshKeyCache = sshKeyCache; this.accountCache = accountCache; this.byEmailCache = byEmailCache; this.externalIdsUpdate = externalIdsUpdate; + this.sshEnabled = sshEnabled; } public synchronized TestAccount create( - String username, String email, String fullName, String... groups) throws Exception { + @Nullable String username, + @Nullable String email, + @Nullable String fullName, + String... groups) + throws Exception { + TestAccount account = accounts.get(username); if (account != null) { return account; @@ -86,18 +97,26 @@ Account.Id id = new Account.Id(db.nextAccountId()); List<ExternalId> extIds = new ArrayList<>(2); - String httpPass = "http-pass"; - extIds.add(ExternalId.createUsername(username, id, httpPass)); + String httpPass = null; + if (username != null) { + httpPass = "http-pass"; + extIds.add(ExternalId.createUsername(username, id, httpPass)); + } if (email != null) { extIds.add(ExternalId.createEmail(id, email)); } - externalIdsUpdate.create().insert(db, extIds); + externalIdsUpdate.create().insert(extIds); - Account a = new Account(id, TimeUtil.nowTs()); - a.setFullName(fullName); - a.setPreferredEmail(email); - db.accounts().insert(Collections.singleton(a)); + accountsUpdate + .create() + .insert( + db, + id, + a -> { + a.setFullName(fullName); + a.setPreferredEmail(email); + }); if (groups != null) { for (String n : groups) { @@ -106,31 +125,39 @@ checkArgument(g != null, "group not found: %s", n); AccountGroupMember m = new AccountGroupMember(new AccountGroupMember.Key(id, g.getId())); db.accountGroupMembers().insert(Collections.singleton(m)); + accountCache.evict(id); } } KeyPair sshKey = null; - if (SshMode.useSsh()) { + if (sshEnabled && username != null) { sshKey = genSshKey(); authorizedKeys.addKey(id, publicKey(sshKey, email)); sshKeyCache.evict(username); } - accountCache.evict(id); - accountCache.evictByUsername(username); + if (username != null) { + accountCache.evictByUsername(username); + } byEmailCache.evict(email); account = new TestAccount(id, username, email, fullName, sshKey, httpPass); - accounts.put(username, account); + if (username != null) { + accounts.put(username, account); + } return account; } } - public TestAccount create(String username, String group) throws Exception { + public TestAccount create(@Nullable String username, String group) throws Exception { return create(username, null, username, group); } - public TestAccount create(String username) throws Exception { + public TestAccount create() throws Exception { + return create(null); + } + + public TestAccount create(@Nullable String username) throws Exception { return create(username, null, username, (String[]) null); }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/EventRecorder.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/EventRecorder.java index f25ca83..72e7058 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/EventRecorder.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -58,8 +58,7 @@ } } - public EventRecorder( - DynamicSet<UserScopedEventListener> eventListeners, final IdentifiedUser user) { + public EventRecorder(DynamicSet<UserScopedEventListener> eventListeners, IdentifiedUser user) { recordedEvents = LinkedListMultimap.create(); eventListenerRegistration =
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java index 3bbdd64..72d646e 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -14,6 +14,9 @@ package com.google.gerrit.acceptance; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + import com.google.auto.value.AutoValue; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; @@ -33,40 +36,49 @@ import com.google.gerrit.testutil.NoteDbChecker; import com.google.gerrit.testutil.NoteDbMode; import com.google.gerrit.testutil.SshMode; -import com.google.gerrit.testutil.TempFileUtil; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Module; -import java.io.File; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.URI; +import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.BrokenBarrierException; -import java.util.concurrent.Callable; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.RepositoryCache; import org.eclipse.jgit.util.FS; -public class GerritServer { +public class GerritServer implements AutoCloseable { + public static class StartupException extends Exception { + private static final long serialVersionUID = 1L; + + StartupException(String msg, Throwable cause) { + super(msg, cause); + } + } + @AutoValue - abstract static class Description { - static Description forTestClass(org.junit.runner.Description testDesc, String configName) { + public abstract static class Description { + public static Description forTestClass( + org.junit.runner.Description testDesc, String configName) { return new AutoValue_GerritServer_Description( testDesc, configName, - true, // @UseLocalDisk is only valid on methods. + !has(UseLocalDisk.class, testDesc.getTestClass()), !has(NoHttpd.class, testDesc.getTestClass()), has(Sandboxed.class, testDesc.getTestClass()), has(UseSsh.class, testDesc.getTestClass()), @@ -76,11 +88,13 @@ null); // @GlobalPluginConfigs is only valid on methods. } - static Description forTestMethod(org.junit.runner.Description testDesc, String configName) { + public static Description forTestMethod( + org.junit.runner.Description testDesc, String configName) { return new AutoValue_GerritServer_Description( testDesc, configName, - testDesc.getAnnotation(UseLocalDisk.class) == null, + testDesc.getAnnotation(UseLocalDisk.class) == null + && !has(UseLocalDisk.class, testDesc.getTestClass()), testDesc.getAnnotation(NoHttpd.class) == null && !has(NoHttpd.class, testDesc.getTestClass()), testDesc.getAnnotation(Sandboxed.class) != null @@ -113,7 +127,11 @@ abstract boolean sandboxed(); - abstract boolean useSsh(); + abstract boolean useSshAnnotation(); + + boolean useSsh() { + return useSshAnnotation() && SshMode.useSsh(); + } @Nullable abstract GerritConfig config(); @@ -160,107 +178,165 @@ } } - /** Returns fully started Gerrit server */ - static GerritServer start(Description desc, Config baseConfig) throws Exception { - desc.checkValidAnnotations(); + /** + * Initializes on-disk site but does not start server. + * + * @param desc server description + * @param baseConfig default config values; merged with config from {@code desc} and then written + * into {@code site/etc/gerrit.config}. + * @param site temp directory where site will live. + * @throws Exception + */ + public static void init(Description desc, Config baseConfig, Path site) throws Exception { + checkArgument(!desc.memory(), "can't initialize site path for in-memory test: %s", desc); Config cfg = desc.buildConfig(baseConfig); - Logger.getLogger("com.google.gerrit").setLevel(Level.DEBUG); - final CyclicBarrier serverStarted = new CyclicBarrier(2); - final Daemon daemon = - new Daemon( - new Runnable() { - @Override - public void run() { - try { - serverStarted.await(); - } catch (InterruptedException | BrokenBarrierException e) { - throw new RuntimeException(e); - } - } - }, - Paths.get(baseConfig.getString("gerrit", null, "tempSiteDir"))); - daemon.setEmailModuleForTesting(new FakeEmailSender.Module()); - daemon.setEnableSshd(SshMode.useSsh()); - - final File site; - ExecutorService daemonService = null; - if (desc.memory()) { - site = null; - mergeTestConfig(cfg); - // Set the log4j configuration to an invalid one to prevent system logs - // from getting configured and creating log files. - System.setProperty(SystemLog.LOG4J_CONFIGURATION, "invalidConfiguration"); - cfg.setBoolean("httpd", null, "requestLog", false); - cfg.setBoolean("sshd", null, "requestLog", false); - cfg.setBoolean("index", "lucene", "testInmemory", true); - cfg.setString("gitweb", null, "cgi", ""); - daemon.setEnableHttpd(desc.httpd()); - daemon.setLuceneModule(LuceneIndexModule.singleVersionAllLatest(0)); - daemon.setDatabaseForTesting( - ImmutableList.<Module>of(new InMemoryTestingDatabaseModule(cfg))); - daemon.start(); - } else { - site = initSite(cfg, desc.buildPluginConfigs()); - daemonService = Executors.newSingleThreadExecutor(); - @SuppressWarnings("unused") - Future<?> possiblyIgnoredError = - daemonService.submit( - new Callable<Void>() { - @Override - public Void call() throws Exception { - int rc = - daemon.main( - new String[] { - "-d", - site.getPath(), - "--headless", - "--console-log", - "--show-stack-trace", - }); - if (rc != 0) { - System.err.println("Failed to start Gerrit daemon"); - serverStarted.reset(); - } - return null; - } - }); - serverStarted.await(); - System.out.println("Gerrit Server Started"); - } - - Injector i = createTestInjector(daemon); - return new GerritServer(desc, i, daemon, daemonService); - } - - private static File initSite(Config base, Map<String, Config> pluginConfigs) throws Exception { - File tmp = TempFileUtil.createTempDirectory(); + Map<String, Config> pluginConfigs = desc.buildPluginConfigs(); Init init = new Init(); int rc = init.main( new String[] { - "-d", tmp.getPath(), "--batch", "--no-auto-start", "--skip-plugins", + "-d", site.toString(), "--batch", "--no-auto-start", "--skip-plugins", }); if (rc != 0) { throw new RuntimeException("Couldn't initialize site"); } - MergeableFileBasedConfig cfg = - new MergeableFileBasedConfig(new File(new File(tmp, "etc"), "gerrit.config"), FS.DETECTED); - cfg.load(); - cfg.merge(base); - mergeTestConfig(cfg); - cfg.save(); + MergeableFileBasedConfig gerritConfig = + new MergeableFileBasedConfig( + site.resolve("etc").resolve("gerrit.config").toFile(), FS.DETECTED); + gerritConfig.load(); + gerritConfig.merge(cfg); + mergeTestConfig(gerritConfig); + gerritConfig.save(); for (String pluginName : pluginConfigs.keySet()) { MergeableFileBasedConfig pluginCfg = new MergeableFileBasedConfig( - new File(new File(tmp, "etc"), pluginName + ".config"), FS.DETECTED); + site.resolve("etc").resolve(pluginName + ".config").toFile(), FS.DETECTED); pluginCfg.load(); pluginCfg.merge(pluginConfigs.get(pluginName)); pluginCfg.save(); } + } - return tmp; + /** + * Initializes new Gerrit site and returns started server. + * + * @param desc server description. + * @param baseConfig default config values; merged with config from {@code desc}. Must contain a + * value for {@code gerrit.tempSiteDir} pointing to a temporary directory. This directory is + * only actually used for on-disk sites ({@link Description#memory()} returns false), but it + * must nonetheless exist for in-memory sites. + * @return started server. + * @throws Exception + */ + public static GerritServer initAndStart(Description desc, Config baseConfig) throws Exception { + Path site = Paths.get(baseConfig.getString("gerrit", null, "tempSiteDir")); + if (!desc.memory()) { + init(desc, baseConfig, site); + } + return start(desc, baseConfig, site, null); + } + + /** + * Starts Gerrit server from existing on-disk site. + * + * @param desc server description. + * @param baseConfig default config values; merged with config from {@code desc}. + * @param site existing temporary directory for site. Required, but may be empty, for in-memory + * servers. For on-disk servers, assumes that {@link #init} was previously called to + * initialize this directory. + * @param testSysModule optional additional module to add to the system injector. + * @param additionalArgs additional command-line arguments for the daemon program; only allowed if + * the test is not in-memory. + * @return started server. + * @throws Exception + */ + public static GerritServer start( + Description desc, + Config baseConfig, + Path site, + @Nullable Module testSysModule, + String... additionalArgs) + throws Exception { + checkArgument(site != null, "site is required (even for in-memory server"); + desc.checkValidAnnotations(); + Logger.getLogger("com.google.gerrit").setLevel(Level.DEBUG); + CyclicBarrier serverStarted = new CyclicBarrier(2); + Daemon daemon = + new Daemon( + () -> { + try { + serverStarted.await(); + } catch (InterruptedException | BrokenBarrierException e) { + throw new RuntimeException(e); + } + }, + site); + daemon.setEmailModuleForTesting(new FakeEmailSender.Module()); + daemon.setAdditionalSysModuleForTesting(testSysModule); + daemon.setEnableSshd(desc.useSsh()); + + if (desc.memory()) { + checkArgument(additionalArgs.length == 0, "cannot pass args to in-memory server"); + return startInMemory(desc, baseConfig, daemon); + } + return startOnDisk(desc, site, daemon, serverStarted, additionalArgs); + } + + private static GerritServer startInMemory(Description desc, Config baseConfig, Daemon daemon) + throws Exception { + Config cfg = desc.buildConfig(baseConfig); + mergeTestConfig(cfg); + // Set the log4j configuration to an invalid one to prevent system logs + // from getting configured and creating log files. + System.setProperty(SystemLog.LOG4J_CONFIGURATION, "invalidConfiguration"); + cfg.setBoolean("httpd", null, "requestLog", false); + cfg.setBoolean("sshd", null, "requestLog", false); + cfg.setBoolean("index", "lucene", "testInmemory", true); + cfg.setString("gitweb", null, "cgi", ""); + daemon.setEnableHttpd(desc.httpd()); + daemon.setLuceneModule(LuceneIndexModule.singleVersionAllLatest(0)); + daemon.setDatabaseForTesting(ImmutableList.<Module>of(new InMemoryTestingDatabaseModule(cfg))); + daemon.start(); + return new GerritServer(desc, createTestInjector(daemon), daemon, null); + } + + private static GerritServer startOnDisk( + Description desc, + Path site, + Daemon daemon, + CyclicBarrier serverStarted, + String[] additionalArgs) + throws Exception { + checkNotNull(site); + ExecutorService daemonService = Executors.newSingleThreadExecutor(); + String[] args = + Stream.concat( + Stream.of( + "-d", site.toString(), "--headless", "--console-log", "--show-stack-trace"), + Arrays.stream(additionalArgs)) + .toArray(String[]::new); + @SuppressWarnings("unused") + Future<?> possiblyIgnoredError = + daemonService.submit( + () -> { + int rc = daemon.main(args); + if (rc != 0) { + System.err.println("Failed to start Gerrit daemon"); + serverStarted.reset(); + } + return null; + }); + try { + serverStarted.await(); + } catch (BrokenBarrierException e) { + daemon.stop(); + throw new StartupException("Failed to start Gerrit daemon; see log", e); + } + System.out.println("Gerrit Server Started"); + + return new GerritServer(desc, createTestInjector(daemon), daemon, daemonService); } private static void mergeTestConfig(Config cfg) { @@ -281,6 +357,7 @@ cfg.setInt("sshd", null, "commandStartThreads", 1); cfg.setInt("receive", null, "threadPoolSize", 1); cfg.setInt("index", null, "threads", 1); + cfg.setBoolean("index", null, "reindexAfterRefUpdate", false); } private static Injector createTestInjector(Daemon daemon) throws Exception { @@ -289,6 +366,7 @@ new FactoryModule() { @Override protected void configure() { + bindConstant().annotatedWith(SshEnabled.class).to(daemon.getEnableSshd()); bind(AccountCreator.class); factory(PushOneCommit.Factory.class); install(InProcessProtocol.module()); @@ -348,7 +426,7 @@ return httpAddress; } - Injector getTestInjector() { + public Injector getTestInjector() { return testInjector; } @@ -356,7 +434,8 @@ return desc; } - void stop() throws Exception { + @Override + public void close() throws Exception { try { checkNoteDbState(); } finally {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java index 7e27e67..c9a474f 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GitUtil.java
@@ -56,7 +56,7 @@ private static final AtomicInteger testRepoCount = new AtomicInteger(); private static final int TEST_REPO_WINDOW_DAYS = 2; - public static void initSsh(final TestAccount a) { + public static void initSsh(TestAccount a) { final Properties config = new Properties(); config.put("StrictHostKeyChecking", "no"); JSch.setConfig(config);
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java index 0977e24..1219b0a 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/InProcessProtocol.java
@@ -16,7 +16,6 @@ import com.google.common.collect.Lists; import com.google.gerrit.acceptance.InProcessProtocol.Context; -import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.Capable; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.reviewdb.client.Account; @@ -32,12 +31,9 @@ import com.google.gerrit.server.git.AsyncReceiveCommits; import com.google.gerrit.server.git.ReceiveCommits; import com.google.gerrit.server.git.ReceivePackInitializer; -import com.google.gerrit.server.git.SearchingChangeCacheImpl; -import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.TransferConfig; import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.git.validators.UploadValidators; -import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.util.RequestContext; @@ -95,7 +91,7 @@ private static final Scope REQUEST = new Scope() { @Override - public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) { + public <T> Provider<T> scope(Key<T> key, Provider<T> creator) { return new Provider<T>() { @Override public T get() { @@ -206,12 +202,9 @@ } private static class Upload implements UploadPackFactory<Context> { - private final Provider<ReviewDb> dbProvider; private final Provider<CurrentUser> userProvider; - private final TagCache tagCache; - @Nullable private final SearchingChangeCacheImpl changeCache; private final ProjectControl.GenericFactory projectControlFactory; - private final ChangeNotes.Factory changeNotesFactory; + private final VisibleRefFilter.Factory refFilterFactory; private final TransferConfig transferConfig; private final DynamicSet<PreUploadHook> preUploadHooks; private final UploadValidators.Factory uploadValidatorsFactory; @@ -219,22 +212,16 @@ @Inject Upload( - Provider<ReviewDb> dbProvider, Provider<CurrentUser> userProvider, - TagCache tagCache, - @Nullable SearchingChangeCacheImpl changeCache, ProjectControl.GenericFactory projectControlFactory, - ChangeNotes.Factory changeNotesFactory, + VisibleRefFilter.Factory refFilterFactory, TransferConfig transferConfig, DynamicSet<PreUploadHook> preUploadHooks, UploadValidators.Factory uploadValidatorsFactory, ThreadLocalRequestContext threadContext) { - this.dbProvider = dbProvider; this.userProvider = userProvider; - this.tagCache = tagCache; - this.changeCache = changeCache; this.projectControlFactory = projectControlFactory; - this.changeNotesFactory = changeNotesFactory; + this.refFilterFactory = refFilterFactory; this.transferConfig = transferConfig; this.preUploadHooks = preUploadHooks; this.uploadValidatorsFactory = uploadValidatorsFactory; @@ -242,8 +229,7 @@ } @Override - public UploadPack create(Context req, final Repository repo) - throws ServiceNotAuthorizedException { + public UploadPack create(Context req, Repository repo) throws ServiceNotAuthorizedException { // Set the request context, but don't bother unsetting, since we don't // 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 @@ -259,9 +245,7 @@ UploadPack up = new UploadPack(repo); up.setPackConfig(transferConfig.getPackConfig()); up.setTimeout(transferConfig.getTimeout()); - up.setAdvertiseRefsHook( - new VisibleRefFilter( - tagCache, changeNotesFactory, changeCache, repo, ctl, dbProvider.get(), true)); + up.setAdvertiseRefsHook(refFilterFactory.create(ctl.getProjectState(), repo)); List<PreUploadHook> hooks = Lists.newArrayList(preUploadHooks); hooks.add(uploadValidatorsFactory.create(ctl.getProject(), repo, "localhost-test")); up.setPreUploadHook(PreUploadHookChain.newChain(hooks)); @@ -300,8 +284,7 @@ } @Override - public ReceivePack create(final Context req, Repository db) - throws ServiceNotAuthorizedException { + public ReceivePack create(Context req, Repository db) throws ServiceNotAuthorizedException { // Set the request context, but don't bother unsetting, since we don't // 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
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java index 6ca8384..7ab6868 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/PushOneCommit.java
@@ -16,9 +16,11 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.acceptance.GitUtil.pushHead; +import static java.util.stream.Collectors.toList; 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.Iterables; import com.google.common.collect.Sets; @@ -29,15 +31,19 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.ReviewerStateInternal; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gerrit.testutil.TestNotesMigration; import com.google.gwtorm.server.OrmException; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import org.eclipse.jgit.api.TagCommand; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.PersonIdent; @@ -138,6 +144,7 @@ private final ChangeNotes.Factory notesFactory; private final ApprovalsUtil approvalsUtil; private final Provider<InternalChangeQuery> queryProvider; + private final TestNotesMigration notesMigration; private final ReviewDb db; private final TestRepository<?> testRepo; @@ -155,6 +162,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, Provider<InternalChangeQuery> queryProvider, + TestNotesMigration notesMigration, @Assisted ReviewDb db, @Assisted PersonIdent i, @Assisted TestRepository<?> testRepo) @@ -163,6 +171,7 @@ notesFactory, approvalsUtil, queryProvider, + notesMigration, db, i, testRepo, @@ -176,6 +185,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, Provider<InternalChangeQuery> queryProvider, + TestNotesMigration notesMigration, @Assisted ReviewDb db, @Assisted PersonIdent i, @Assisted TestRepository<?> testRepo, @@ -185,6 +195,7 @@ notesFactory, approvalsUtil, queryProvider, + notesMigration, db, i, testRepo, @@ -199,6 +210,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, Provider<InternalChangeQuery> queryProvider, + TestNotesMigration notesMigration, @Assisted ReviewDb db, @Assisted PersonIdent i, @Assisted TestRepository<?> testRepo, @@ -210,6 +222,7 @@ notesFactory, approvalsUtil, queryProvider, + notesMigration, db, i, testRepo, @@ -224,13 +237,24 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, Provider<InternalChangeQuery> queryProvider, + TestNotesMigration notesMigration, @Assisted ReviewDb db, @Assisted PersonIdent i, @Assisted TestRepository<?> testRepo, @Assisted String subject, @Assisted Map<String, String> files) throws Exception { - this(notesFactory, approvalsUtil, queryProvider, db, i, testRepo, subject, files, null); + this( + notesFactory, + approvalsUtil, + queryProvider, + notesMigration, + db, + i, + testRepo, + subject, + files, + null); } @AssistedInject @@ -238,6 +262,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, Provider<InternalChangeQuery> queryProvider, + TestNotesMigration notesMigration, @Assisted ReviewDb db, @Assisted PersonIdent i, @Assisted TestRepository<?> testRepo, @@ -250,6 +275,7 @@ notesFactory, approvalsUtil, queryProvider, + notesMigration, db, i, testRepo, @@ -262,6 +288,7 @@ ChangeNotes.Factory notesFactory, ApprovalsUtil approvalsUtil, Provider<InternalChangeQuery> queryProvider, + TestNotesMigration notesMigration, ReviewDb db, PersonIdent i, TestRepository<?> testRepo, @@ -274,6 +301,7 @@ this.notesFactory = notesFactory; this.approvalsUtil = approvalsUtil; this.queryProvider = queryProvider; + this.notesMigration = notesMigration; this.subject = subject; this.files = files; this.changeId = changeId; @@ -332,7 +360,7 @@ return new Result(ref, pushHead(testRepo, ref, tag != null, force, pushOptions), c, subject); } - public void setTag(final Tag tag) { + public void setTag(Tag tag) { this.tag = tag; } @@ -392,16 +420,36 @@ public void assertChange( Change.Status expectedStatus, String expectedTopic, TestAccount... expectedReviewers) throws OrmException { + assertChange( + expectedStatus, expectedTopic, Arrays.asList(expectedReviewers), ImmutableList.of()); + } + + public void assertChange( + Change.Status expectedStatus, + String expectedTopic, + List<TestAccount> expectedReviewers, + List<TestAccount> expectedCcs) + throws OrmException { Change c = getChange().change(); assertThat(c.getSubject()).isEqualTo(resSubj); assertThat(c.getStatus()).isEqualTo(expectedStatus); assertThat(Strings.emptyToNull(c.getTopic())).isEqualTo(expectedTopic); - assertReviewers(c, expectedReviewers); + if (notesMigration.readChanges()) { + assertReviewers(c, ReviewerStateInternal.REVIEWER, expectedReviewers); + assertReviewers(c, ReviewerStateInternal.CC, expectedCcs); + } else { + assertReviewers( + c, + ReviewerStateInternal.REVIEWER, + Stream.concat(expectedReviewers.stream(), expectedCcs.stream()).collect(toList())); + } } - private void assertReviewers(Change c, TestAccount... expectedReviewers) throws OrmException { + private void assertReviewers( + Change c, ReviewerStateInternal state, List<TestAccount> expectedReviewers) + throws OrmException { Iterable<Account.Id> actualIds = - approvalsUtil.getReviewers(db, notesFactory.createChecked(db, c)).all(); + approvalsUtil.getReviewers(db, notesFactory.createChecked(db, c)).byState(state); assertThat(actualIds) .containsExactlyElementsIn(Sets.newHashSet(TestAccount.ids(expectedReviewers))); }
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshEnabled.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshEnabled.java new file mode 100644 index 0000000..5349755 --- /dev/null +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/SshEnabled.java
@@ -0,0 +1,24 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; +import java.lang.annotation.Retention; + +@Retention(RUNTIME) +@BindingAnnotation +public @interface SshEnabled {}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/StandaloneSiteTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/StandaloneSiteTest.java new file mode 100644 index 0000000..93273c4 --- /dev/null +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/StandaloneSiteTest.java
@@ -0,0 +1,151 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.truth.Truth.assertThat; +import static java.util.stream.Collectors.joining; +import static org.junit.Assert.fail; + +import com.google.common.collect.Streams; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.launcher.GerritLauncher; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.util.ManualRequestContext; +import com.google.gerrit.server.util.OneOffRequestContext; +import com.google.gerrit.server.util.RequestContext; +import com.google.gerrit.testutil.ConfigSuite; +import com.google.inject.Injector; +import com.google.inject.Module; +import com.google.inject.Provider; +import java.util.Arrays; +import org.eclipse.jgit.lib.Config; +import org.junit.Rule; +import org.junit.rules.RuleChain; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.model.Statement; + +@RunWith(ConfigSuite.class) +@UseLocalDisk +public abstract class StandaloneSiteTest { + protected class ServerContext implements RequestContext, AutoCloseable { + private final GerritServer server; + private final ManualRequestContext ctx; + + private ServerContext(GerritServer server) throws Exception { + this.server = server; + Injector i = server.getTestInjector(); + if (adminId == null) { + adminId = i.getInstance(AccountCreator.class).admin().getId(); + } + ctx = i.getInstance(OneOffRequestContext.class).openAs(adminId); + } + + @Override + public CurrentUser getUser() { + return ctx.getUser(); + } + + @Override + public Provider<ReviewDb> getReviewDbProvider() { + return ctx.getReviewDbProvider(); + } + + public Injector getInjector() { + return server.getTestInjector(); + } + + @Override + public void close() throws Exception { + try { + ctx.close(); + } finally { + server.close(); + } + } + } + + @ConfigSuite.Parameter public Config baseConfig; + @ConfigSuite.Name private String configName; + + private final TemporaryFolder tempSiteDir = new TemporaryFolder(); + + private final TestRule testRunner = + new TestRule() { + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + beforeTest(description); + base.evaluate(); + } + }; + } + }; + + @Rule public RuleChain ruleChain = RuleChain.outerRule(tempSiteDir).around(testRunner); + + protected SitePaths sitePaths; + + private GerritServer.Description serverDesc; + private Account.Id adminId; + + private void beforeTest(Description description) throws Exception { + serverDesc = GerritServer.Description.forTestMethod(description, configName); + sitePaths = new SitePaths(tempSiteDir.getRoot().toPath()); + GerritServer.init(serverDesc, baseConfig, sitePaths.site_path); + } + + protected ServerContext startServer() throws Exception { + return startServer(null); + } + + protected ServerContext startServer(@Nullable Module testSysModule, String... additionalArgs) + throws Exception { + return new ServerContext(startImpl(testSysModule, additionalArgs)); + } + + protected void assertServerStartupFails() throws Exception { + try (GerritServer server = startImpl(null)) { + fail("expected server startup to fail"); + } catch (GerritServer.StartupException e) { + // Expected. + } + } + + private GerritServer startImpl(@Nullable Module testSysModule, String... additionalArgs) + throws Exception { + return GerritServer.start( + serverDesc, baseConfig, sitePaths.site_path, testSysModule, additionalArgs); + } + + protected static void runGerrit(String... args) throws Exception { + assertThat(GerritLauncher.mainImpl(args)) + .named("gerrit.war " + Arrays.stream(args).collect(joining(" "))) + .isEqualTo(0); + } + + @SafeVarargs + protected static void runGerrit(Iterable<String>... multiArgs) throws Exception { + runGerrit( + Arrays.stream(multiArgs).flatMap(args -> Streams.stream(args)).toArray(String[]::new)); + } +}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java index 5117328..7acb135 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/TestAccount.java
@@ -16,12 +16,15 @@ import static java.util.stream.Collectors.toList; +import com.google.common.net.InetAddresses; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.mail.Address; import com.jcraft.jsch.KeyPair; import java.io.ByteArrayOutputStream; +import java.net.InetSocketAddress; import java.util.Arrays; import java.util.List; +import org.apache.http.client.utils.URIBuilder; import org.eclipse.jgit.lib.PersonIdent; public class TestAccount { @@ -29,10 +32,6 @@ return accounts.stream().map(a -> a.id).collect(toList()); } - public static List<Account.Id> ids(TestAccount... accounts) { - return ids(Arrays.asList(accounts)); - } - public static List<String> names(List<TestAccount> accounts) { return accounts.stream().map(a -> a.fullName).collect(toList()); } @@ -77,12 +76,13 @@ } public String getHttpUrl(GerritServer server) { - return String.format( - "http://%s:%s@%s:%d", - username, - httpPassword, - server.getHttpAddress().getAddress().getHostAddress(), - server.getHttpAddress().getPort()); + InetSocketAddress addr = server.getHttpAddress(); + return new URIBuilder() + .setScheme("http") + .setUserInfo(username, httpPassword) + .setHost(InetAddresses.toUriString(addr.getAddress())) + .setPort(addr.getPort()) + .toString(); } public Account.Id getId() {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java index a649095..e177bb4 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/UseLocalDisk.java
@@ -15,11 +15,12 @@ package com.google.gerrit.acceptance; import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; import java.lang.annotation.Target; -@Target({METHOD}) +@Target({TYPE, METHOD}) @Retention(RUNTIME) public @interface UseLocalDisk {}
diff --git a/gerrit-acceptance-tests/BUILD b/gerrit-acceptance-tests/BUILD index 3875dc5..0ad9002 100644 --- a/gerrit-acceptance-tests/BUILD +++ b/gerrit-acceptance-tests/BUILD
@@ -1,7 +1,10 @@ +RESOURCES = glob(["src/test/resources/**/*"]) + java_library( name = "lib", testonly = 1, srcs = ["src/test/java/com/google/gerrit/acceptance/Dummy.java"], + resources = RESOURCES, visibility = ["//visibility:public"], exports = [ "//gerrit-acceptance-framework:lib", @@ -16,9 +19,9 @@ "//gerrit-pgm:pgm", "//gerrit-pgm:util", "//gerrit-reviewdb:server", + "//gerrit-server:prolog-common", "//gerrit-server:server", "//gerrit-server:testutil", - "//gerrit-server/src/main/prolog:common", "//gerrit-sshd:sshd", "//gerrit-test-util:test_util", "//lib:args4j",
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java index 75fd448..50bc0be 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assert_; +import static com.google.gerrit.acceptance.GitUtil.deleteRef; import static com.google.gerrit.acceptance.GitUtil.fetch; import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS; import static com.google.gerrit.gpg.PublicKeyStore.keyToString; @@ -26,10 +27,12 @@ import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration; import static com.google.gerrit.server.StarredChangesUtil.DEFAULT_LABEL; import static com.google.gerrit.server.StarredChangesUtil.IGNORE_LABEL; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY; import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.toSet; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.junit.Assert.fail; import com.google.common.collect.FluentIterable; @@ -37,21 +40,30 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.io.BaseEncoding; +import com.google.common.util.concurrent.AtomicLongMap; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AccountCreator; import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.Sandboxed; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.UseSsh; +import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.Permission; import com.google.gerrit.extensions.api.accounts.EmailInput; import com.google.gerrit.extensions.api.changes.AddReviewerInput; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.StarsInput; -import com.google.gerrit.extensions.client.GeneralPreferencesInfo; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInput; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInput.CheckAccountsInput; import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.GpgKeyInfo; import com.google.gerrit.extensions.common.SshKeyInfo; +import com.google.gerrit.extensions.events.AccountIndexedListener; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.registration.RegistrationHandle; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; @@ -59,17 +71,19 @@ import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.gpg.Fingerprint; import com.google.gerrit.gpg.PublicKeyStore; -import com.google.gerrit.gpg.server.GpgKeys; import com.google.gerrit.gpg.testutil.TestKey; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.account.AccountByEmailCache; -import com.google.gerrit.server.account.ExternalId; -import com.google.gerrit.server.account.ExternalIdsUpdate; +import com.google.gerrit.server.account.AccountConfig; import com.google.gerrit.server.account.WatchConfig; import com.google.gerrit.server.account.WatchConfig.NotifyType; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl; import com.google.gerrit.server.project.RefPattern; import com.google.gerrit.server.util.MagicBranch; import com.google.gerrit.testutil.ConfigSuite; @@ -79,8 +93,8 @@ import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; import java.util.EnumSet; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -93,10 +107,16 @@ import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Ref; 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.eclipse.jgit.transport.PushCertificateIdent; +import org.eclipse.jgit.transport.PushResult; +import org.eclipse.jgit.transport.RemoteRefUpdate; +import org.eclipse.jgit.treewalk.TreeWalk; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -116,18 +136,37 @@ @Inject private AccountByEmailCache byEmailCache; + @Inject private ExternalIds externalIds; + @Inject private ExternalIdsUpdate.User externalIdsUpdateFactory; + @Inject private DynamicSet<AccountIndexedListener> accountIndexedListeners; + + private AccountIndexedCounter accountIndexedCounter; + private RegistrationHandle accountIndexEventCounterHandle; private ExternalIdsUpdate externalIdsUpdate; private List<ExternalId> savedExternalIds; @Before + public void addAccountIndexEventCounter() { + accountIndexedCounter = new AccountIndexedCounter(); + accountIndexEventCounterHandle = accountIndexedListeners.add(accountIndexedCounter); + } + + @After + public void removeAccountIndexEventCounter() { + if (accountIndexEventCounterHandle != null) { + accountIndexEventCounterHandle.remove(); + } + } + + @Before public void saveExternalIds() throws Exception { externalIdsUpdate = externalIdsUpdateFactory.create(); savedExternalIds = new ArrayList<>(); - savedExternalIds.addAll(getExternalIds(admin)); - savedExternalIds.addAll(getExternalIds(user)); + savedExternalIds.addAll(externalIds.byAccount(admin.id)); + savedExternalIds.addAll(externalIds.byAccount(user.id)); } @After @@ -136,9 +175,9 @@ // savedExternalIds is null when we don't run SSH tests and the assume in // @Before in AbstractDaemonTest prevents this class' @Before method from // being executed. - externalIdsUpdate.delete(db, getExternalIds(admin)); - externalIdsUpdate.delete(db, getExternalIds(user)); - externalIdsUpdate.insert(db, savedExternalIds); + externalIdsUpdate.delete(externalIds.byAccount(admin.id)); + externalIdsUpdate.delete(externalIds.byAccount(user.id)); + externalIdsUpdate.insert(savedExternalIds); } } @@ -154,10 +193,6 @@ } } - private Collection<ExternalId> getExternalIds(TestAccount account) throws Exception { - return accountCache.get(account.getId()).getExternalIds(); - } - @After public void deleteGpgKeys() throws Exception { String ref = REFS_GPG_KEYS; @@ -174,11 +209,84 @@ } @Test + public void create() throws Exception { + create(2); // account creation + external ID creation + } + + @Test + @UseSsh + public void createWithSshKeys() throws Exception { + create(3); // account creation + external ID creation + adding SSH keys + } + + private void create(int expectedAccountReindexCalls) throws Exception { + String name = "foo"; + TestAccount foo = accountCreator.create(name); + AccountInfo info = gApi.accounts().id(foo.id.get()).get(); + assertThat(info.username).isEqualTo(name); + assertThat(info.name).isEqualTo(name); + accountIndexedCounter.assertReindexOf(foo, expectedAccountReindexCalls); + + // check user branch + try (Repository repo = repoManager.openRepository(allUsers); + RevWalk rw = new RevWalk(repo); + ObjectReader or = repo.newObjectReader()) { + Ref ref = repo.exactRef(RefNames.refsUsers(foo.getId())); + assertThat(ref).isNotNull(); + RevCommit c = rw.parseCommit(ref.getObjectId()); + long timestampDiffMs = + Math.abs( + c.getCommitTime() * 1000L + - accountCache.get(foo.getId()).getAccount().getRegisteredOn().getTime()); + assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS); + + // Check the 'account.config' file. + try (TreeWalk tw = TreeWalk.forPath(or, AccountConfig.ACCOUNT_CONFIG, c.getTree())) { + assertThat(tw).isNotNull(); + Config cfg = new Config(); + cfg.fromText(new String(or.open(tw.getObjectId(0), OBJ_BLOB).getBytes(), UTF_8)); + assertThat(cfg.getString(AccountConfig.ACCOUNT, null, AccountConfig.KEY_FULL_NAME)) + .isEqualTo(name); + } + } + } + + @Test + public void createAnonymousCoward() throws Exception { + TestAccount anonymousCoward = accountCreator.create(); + accountIndexedCounter.assertReindexOf(anonymousCoward); + + // check user branch + try (Repository repo = repoManager.openRepository(allUsers); + RevWalk rw = new RevWalk(repo); + ObjectReader or = repo.newObjectReader()) { + Ref ref = repo.exactRef(RefNames.refsUsers(anonymousCoward.getId())); + assertThat(ref).isNotNull(); + RevCommit c = rw.parseCommit(ref.getObjectId()); + long timestampDiffMs = + Math.abs( + c.getCommitTime() * 1000L + - accountCache + .get(anonymousCoward.getId()) + .getAccount() + .getRegisteredOn() + .getTime()); + assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS); + + // No account properties were set, hence an 'account.config' file was not created. + try (TreeWalk tw = TreeWalk.forPath(or, AccountConfig.ACCOUNT_CONFIG, c.getTree())) { + assertThat(tw).isNull(); + } + } + } + + @Test public void get() throws Exception { AccountInfo info = gApi.accounts().id("admin").get(); assertThat(info.name).isEqualTo("Administrator"); assertThat(info.email).isEqualTo("admin@example.com"); assertThat(info.username).isEqualTo("admin"); + accountIndexedCounter.assertNoReindex(); } @Test @@ -186,6 +294,7 @@ AccountInfo info = gApi.accounts().id("admin").get(); AccountInfo infoByIntId = gApi.accounts().id(info._accountId).get(); assertThat(info.name).isEqualTo(infoByIntId.name); + accountIndexedCounter.assertNoReindex(); } @Test @@ -195,6 +304,7 @@ info = gApi.accounts().id("self").get(); assertUser(info, admin); + accountIndexedCounter.assertNoReindex(); } @Test @@ -202,8 +312,11 @@ assertThat(gApi.accounts().id("user").getActive()).isTrue(); gApi.accounts().id("user").setActive(false); assertThat(gApi.accounts().id("user").getActive()).isFalse(); + accountIndexedCounter.assertReindexOf(user); + gApi.accounts().id("user").setActive(true); assertThat(gApi.accounts().id("user").getActive()).isTrue(); + accountIndexedCounter.assertReindexOf(user); } @Test @@ -240,6 +353,7 @@ change = info(triplet); assertThat(change.starred).isNull(); assertThat(change.stars).isNull(); + accountIndexedCounter.assertNoReindex(); } @Test @@ -280,6 +394,7 @@ assertThat(starredChange._number).isEqualTo(r.getChange().getId().get()); assertThat(starredChange.starred).isNull(); assertThat(starredChange.stars).containsExactly("red", "yellow").inOrder(); + accountIndexedCounter.assertNoReindex(); setApiUser(user); exception.expect(AuthException.class); @@ -320,13 +435,15 @@ @Test public void ignoreChange() throws Exception { + TestAccount user2 = accountCreator.user2(); + accountIndexedCounter.clear(); + PushOneCommit.Result r = createChange(); AddReviewerInput in = new AddReviewerInput(); in.reviewer = user.email; gApi.changes().id(r.getChangeId()).addReviewer(in); - TestAccount user2 = accounts.user2(); in = new AddReviewerInput(); in.reviewer = user2.email; gApi.changes().id(r.getChangeId()).addReviewer(in); @@ -340,6 +457,7 @@ List<Message> messages = sender.getMessages(); assertThat(messages).hasSize(1); assertThat(messages.get(0).rcpt()).containsExactly(user2.emailAddress); + accountIndexedCounter.assertNoReindex(); } @Test @@ -360,6 +478,7 @@ Message message = messages.get(0); assertThat(message.rcpt()).containsExactly(user.emailAddress); assertMailReplyTo(message, admin.email); + accountIndexedCounter.assertNoReindex(); } @Test @@ -374,17 +493,12 @@ List<AccountInfo> emptyResult = gApi.accounts().suggestAccounts("unknown").get(); assertThat(emptyResult).isEmpty(); + accountIndexedCounter.assertNoReindex(); } @Test public void addEmail() throws Exception { - List<String> emails = - ImmutableList.of( - "new.email@example.com", - "new.email@example.systems", - - // Not in the list of TLDs but added to override in OutgoingEmailValidator - "new.email@example.local"); + List<String> emails = ImmutableList.of("new.email@example.com", "new.email@example.systems"); Set<String> currentEmails = getEmails(); for (String email : emails) { assertThat(currentEmails).doesNotContain(email); @@ -392,6 +506,7 @@ input.email = email; input.noConfirmation = true; gApi.accounts().self().addEmail(input); + accountIndexedCounter.assertReindexOf(admin); } resetCurrentApiUser(); @@ -412,7 +527,7 @@ "@example.com", // Non-supported TLD (see tlds-alpha-by-domain.txt) - "new.email@example.blog"); + "new.email@example.africa"); for (String email : emails) { EmailInput input = new EmailInput(); input.email = email; @@ -424,6 +539,18 @@ assertThat(e).hasMessageThat().isEqualTo("invalid email address"); } } + accountIndexedCounter.assertNoReindex(); + } + + @Test + public void cannotAddNonConfirmedEmailWithoutModifyAccountPermission() throws Exception { + TestAccount account = accountCreator.create(name("user")); + EmailInput input = new EmailInput(); + input.email = "test@test.com"; + input.noConfirmation = true; + setApiUser(user); + exception.expect(AuthException.class); + gApi.accounts().id(account.username).addEmail(input); } @Test @@ -437,7 +564,9 @@ resetCurrentApiUser(); assertThat(getEmails()).contains(email); + accountIndexedCounter.clear(); gApi.accounts().self().deleteEmail(input.email); + accountIndexedCounter.assertReindexOf(admin); resetCurrentApiUser(); assertThat(getEmails()).doesNotContain(email); @@ -452,7 +581,8 @@ ImmutableList.of( ExternalId.createWithEmail(ExternalId.Key.parse(extId1), admin.id, email), ExternalId.createWithEmail(ExternalId.Key.parse(extId2), admin.id, email)); - externalIdsUpdateFactory.create().insert(db, extIds); + externalIdsUpdateFactory.create().insert(extIds); + accountIndexedCounter.assertReindexOf(admin); assertThat( gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet())) .containsAllOf(extId1, extId2); @@ -461,6 +591,7 @@ assertThat(getEmails()).contains(email); gApi.accounts().self().deleteEmail(email); + accountIndexedCounter.assertReindexOf(admin, 2); // for each deleted external ID once resetCurrentApiUser(); assertThat(getEmails()).doesNotContain(email); @@ -476,6 +607,7 @@ input.email = email; input.noConfirmation = true; gApi.accounts().id(user.id.get()).addEmail(input); + accountIndexedCounter.assertReindexOf(user); setApiUser(user); assertThat(getEmails()).contains(email); @@ -483,13 +615,14 @@ // admin can delete email of user setApiUser(admin); gApi.accounts().id(user.id.get()).deleteEmail(email); + accountIndexedCounter.assertReindexOf(user); setApiUser(user); assertThat(getEmails()).doesNotContain(email); // user cannot delete email of admin exception.expect(AuthException.class); - exception.expectMessage("not allowed to delete email address"); + exception.expectMessage("modify account not permitted"); gApi.accounts().id(admin.id.get()).deleteEmail(admin.email); } @@ -502,7 +635,7 @@ String email = "foo.bar@example.com"; externalIdsUpdateFactory .create() - .insert(db, ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id, email)); + .insert(ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id, email)); assertEmail(byEmailCache.get(email), admin); // wrong case doesn't match @@ -524,17 +657,14 @@ admin.status = status; info = gApi.accounts().self().get(); assertUser(info, admin); + accountIndexedCounter.assertReindexOf(admin); } } @Test + @Sandboxed public void fetchUserBranch() throws Exception { - // change something in the user preferences to ensure that the user branch - // is created setApiUser(user); - GeneralPreferencesInfo input = new GeneralPreferencesInfo(); - input.changesPerPage = GeneralPreferencesInfo.defaults().changesPerPage + 10; - gApi.accounts().self().setPreferences(input); TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user); String userRefName = RefNames.refsUsers(user.id); @@ -546,7 +676,7 @@ saveProjectConfig(allUsers, cfg); // deny READ permission that is inherited from All-Projects - deny(allUsers, Permission.READ, ANONYMOUS_USERS, RefNames.REFS + "*"); + deny(allUsers, RefNames.REFS + "*", Permission.READ, ANONYMOUS_USERS); // fetching user branch without READ permission fails try { @@ -558,9 +688,9 @@ // allow each user to read its own user branch grant( - Permission.READ, allUsers, RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", + Permission.READ, false, REGISTERED_USERS); @@ -575,6 +705,8 @@ assertThat(userSelfRef).isNotNull(); assertThat(userSelfRef.getObjectId()).isEqualTo(userRef.getObjectId()); + accountIndexedCounter.assertNoReindex(); + // fetching user branch of another user fails String otherUserRefName = RefNames.refsUsers(admin.id); exception.expect(TransportException.class); @@ -584,30 +716,20 @@ @Test public void pushToUserBranch() throws Exception { - // change something in the user preferences to ensure that the user branch - // is created - GeneralPreferencesInfo input = new GeneralPreferencesInfo(); - input.changesPerPage = GeneralPreferencesInfo.defaults().changesPerPage + 10; - gApi.accounts().self().setPreferences(input); - TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef"); allUsersRepo.reset("userRef"); PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo); push.to(RefNames.refsUsers(admin.id)).assertOkStatus(); + accountIndexedCounter.assertReindexOf(admin); push = pushFactory.create(db, admin.getIdent(), allUsersRepo); push.to(RefNames.REFS_USERS_SELF).assertOkStatus(); + accountIndexedCounter.assertReindexOf(admin); } @Test public void pushToUserBranchForReview() throws Exception { - // change something in the user preferences to ensure that the user branch - // is created - GeneralPreferencesInfo input = new GeneralPreferencesInfo(); - input.changesPerPage = GeneralPreferencesInfo.defaults().changesPerPage + 10; - gApi.accounts().self().setPreferences(input); - String userRefName = RefNames.refsUsers(admin.id); TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); fetch(allUsersRepo, userRefName + ":userRef"); @@ -615,26 +737,54 @@ PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo); PushOneCommit.Result r = push.to(MagicBranch.NEW_CHANGE + userRefName); r.assertOkStatus(); + accountIndexedCounter.assertNoReindex(); assertThat(r.getChange().change().getDest().get()).isEqualTo(userRefName); gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve()); gApi.changes().id(r.getChangeId()).current().submit(); + accountIndexedCounter.assertReindexOf(admin); push = pushFactory.create(db, admin.getIdent(), allUsersRepo); r = push.to(MagicBranch.NEW_CHANGE + RefNames.REFS_USERS_SELF); r.assertOkStatus(); + accountIndexedCounter.assertNoReindex(); assertThat(r.getChange().change().getDest().get()).isEqualTo(userRefName); gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve()); gApi.changes().id(r.getChangeId()).current().submit(); + accountIndexedCounter.assertReindexOf(admin); + } + + @Test + public void pushAccountConfigToUserBranchForReviewIsRejectedOnSubmit() throws Exception { + String userRefName = RefNames.refsUsers(admin.id); + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + fetch(allUsersRepo, userRefName + ":userRef"); + allUsersRepo.reset("userRef"); + + Config ac = getAccountConfig(allUsersRepo); + ac.setString(AccountConfig.ACCOUNT, null, AccountConfig.KEY_STATUS, "OOO"); + + PushOneCommit.Result r = + pushFactory + .create( + db, + admin.getIdent(), + allUsersRepo, + "Update account config", + AccountConfig.ACCOUNT_CONFIG, + ac.toText()) + .to(MagicBranch.NEW_CHANGE + userRefName); + r.assertOkStatus(); + accountIndexedCounter.assertNoReindex(); + assertThat(r.getChange().change().getDest().get()).isEqualTo(userRefName); + gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve()); + exception.expect(ResourceConflictException.class); + exception.expectMessage( + String.format("update of %s not allowed", AccountConfig.ACCOUNT_CONFIG)); + gApi.changes().id(r.getChangeId()).current().submit(); } @Test public void pushWatchConfigToUserBranch() throws Exception { - // change something in the user preferences to ensure that the user branch - // is created - GeneralPreferencesInfo input = new GeneralPreferencesInfo(); - input.changesPerPage = GeneralPreferencesInfo.defaults().changesPerPage + 10; - gApi.accounts().self().setPreferences(input); - TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef"); allUsersRepo.reset("userRef"); @@ -654,6 +804,7 @@ WatchConfig.WATCH_CONFIG, wc.toText()); push.to(RefNames.REFS_USERS_SELF).assertOkStatus(); + accountIndexedCounter.assertReindexOf(admin); String invalidNotifyValue = "]invalid["; wc.setString(WatchConfig.PROJECT, project.get(), WatchConfig.KEY_NOTIFY, invalidNotifyValue); @@ -674,6 +825,147 @@ } @Test + public void pushAccountConfigToUserBranchIsRejected() throws Exception { + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + fetch(allUsersRepo, RefNames.refsUsers(admin.id) + ":userRef"); + allUsersRepo.reset("userRef"); + + Config ac = getAccountConfig(allUsersRepo); + ac.setString(AccountConfig.ACCOUNT, null, AccountConfig.KEY_STATUS, "OOO"); + + PushOneCommit.Result r = + pushFactory + .create( + db, + admin.getIdent(), + allUsersRepo, + "Update account config", + AccountConfig.ACCOUNT_CONFIG, + ac.toText()) + .to(RefNames.REFS_USERS_SELF); + r.assertErrorStatus("account update not allowed"); + } + + @Test + @Sandboxed + public void cannotCreateUserBranch() throws Exception { + grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE); + grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH); + + String userRef = RefNames.refsUsers(new Account.Id(db.nextAccountId())); + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), allUsersRepo).to(userRef); + r.assertErrorStatus(); + assertThat(r.getMessage()).contains("Not allowed to create user branch."); + + try (Repository repo = repoManager.openRepository(allUsers)) { + assertThat(repo.exactRef(userRef)).isNull(); + } + } + + @Test + @Sandboxed + public void createUserBranchWithAccessDatabaseCapability() throws Exception { + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE); + grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH); + + String userRef = RefNames.refsUsers(new Account.Id(db.nextAccountId())); + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + pushFactory.create(db, admin.getIdent(), allUsersRepo).to(userRef).assertOkStatus(); + + try (Repository repo = repoManager.openRepository(allUsers)) { + assertThat(repo.exactRef(userRef)).isNotNull(); + } + } + + @Test + @Sandboxed + public void cannotCreateNonUserBranchUnderRefsUsersWithAccessDatabaseCapability() + throws Exception { + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + grant(allUsers, RefNames.REFS_USERS + "*", Permission.CREATE); + grant(allUsers, RefNames.REFS_USERS + "*", Permission.PUSH); + + String userRef = RefNames.REFS_USERS + "foo"; + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + PushOneCommit.Result r = pushFactory.create(db, admin.getIdent(), allUsersRepo).to(userRef); + r.assertErrorStatus(); + assertThat(r.getMessage()).contains("Not allowed to create non-user branch under refs/users/."); + + try (Repository repo = repoManager.openRepository(allUsers)) { + assertThat(repo.exactRef(userRef)).isNull(); + } + } + + @Test + @Sandboxed + public void createDefaultUserBranch() throws Exception { + try (Repository repo = repoManager.openRepository(allUsers)) { + assertThat(repo.exactRef(RefNames.REFS_USERS_DEFAULT)).isNull(); + } + + grant(allUsers, RefNames.REFS_USERS_DEFAULT, Permission.CREATE); + grant(allUsers, RefNames.REFS_USERS_DEFAULT, Permission.PUSH); + + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + pushFactory + .create(db, admin.getIdent(), allUsersRepo) + .to(RefNames.REFS_USERS_DEFAULT) + .assertOkStatus(); + + try (Repository repo = repoManager.openRepository(allUsers)) { + assertThat(repo.exactRef(RefNames.REFS_USERS_DEFAULT)).isNotNull(); + } + } + + @Test + @Sandboxed + public void cannotDeleteUserBranch() throws Exception { + grant( + allUsers, + RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", + Permission.DELETE, + true, + REGISTERED_USERS); + + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + String userRef = RefNames.refsUsers(admin.id); + PushResult r = deleteRef(allUsersRepo, userRef); + RemoteRefUpdate refUpdate = r.getRemoteUpdate(userRef); + assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON); + assertThat(refUpdate.getMessage()).contains("Not allowed to delete user branch."); + + try (Repository repo = repoManager.openRepository(allUsers)) { + assertThat(repo.exactRef(userRef)).isNotNull(); + } + } + + @Test + @Sandboxed + public void deleteUserBranchWithAccessDatabaseCapability() throws Exception { + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + grant( + allUsers, + RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}", + Permission.DELETE, + true, + REGISTERED_USERS); + + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + String userRef = RefNames.refsUsers(admin.id); + PushResult r = deleteRef(allUsersRepo, userRef); + RemoteRefUpdate refUpdate = r.getRemoteUpdate(userRef); + assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.OK); + + try (Repository repo = repoManager.openRepository(allUsers)) { + assertThat(repo.exactRef(userRef)).isNull(); + } + + // TODO(ekempin): assert that account was deleted from cache and index + } + + @Test public void addGpgKey() throws Exception { TestKey key = validKeyWithoutExpiration(); String id = key.getKeyIdString(); @@ -709,7 +1001,8 @@ public void addOtherUsersGpgKey_Conflict() throws Exception { // Both users have a matching external ID for this key. addExternalIdEmail(admin, "test5@example.com"); - externalIdsUpdate.insert(db, ExternalId.create("foo", "myId", user.getId())); + externalIdsUpdate.insert(ExternalId.create("foo", "myId", user.getId())); + accountIndexedCounter.assertReindexOf(user); TestKey key = validKeyWithSecondUserId(); addGpgKey(key.getPublicKeyArmored()); @@ -730,6 +1023,7 @@ } gApi.accounts().self().putGpgKeys(toAdd, ImmutableList.<String>of()); assertKeys(keys); + accountIndexedCounter.assertReindexOf(admin); } @Test @@ -741,6 +1035,7 @@ assertKeys(key); gApi.accounts().self().gpgKey(id).delete(); + accountIndexedCounter.assertReindexOf(admin); assertKeys(); exception.expect(ResourceNotFoundException.class); @@ -765,6 +1060,7 @@ ImmutableList.of(key5.getKeyIdString())); assertThat(infos.keySet()).containsExactly(key1.getKeyIdString(), key2.getKeyIdString()); assertKeys(key1, key2); + accountIndexedCounter.assertReindexOf(admin); infos = gApi.accounts() @@ -776,6 +1072,7 @@ assertKeyMapContains(key5, infos); assertThat(infos.get(key1.getKeyIdString()).key).isNull(); assertKeys(key2, key5); + accountIndexedCounter.assertReindexOf(admin); exception.expect(BadRequestException.class); exception.expectMessage("Cannot both add and delete key: " + keyToString(key2.getPublicKey())); @@ -798,6 +1095,7 @@ SshKeyInfo key = info.get(0); String inital = AccountCreator.publicKey(admin.sshKey, admin.email); assertThat(key.sshPublicKey).isEqualTo(inital); + accountIndexedCounter.assertNoReindex(); // Add a new key String newKey = AccountCreator.publicKey(AccountCreator.genSshKey(), admin.email); @@ -805,12 +1103,14 @@ info = gApi.accounts().self().listSshKeys(); assertThat(info).hasSize(2); assertSequenceNumbers(info); + accountIndexedCounter.assertReindexOf(admin); // Add an existing key (the request succeeds, but the key isn't added again) gApi.accounts().self().addSshKey(inital); info = gApi.accounts().self().listSshKeys(); assertThat(info).hasSize(2); assertSequenceNumbers(info); + accountIndexedCounter.assertNoReindex(); // Add another new key String newKey2 = AccountCreator.publicKey(AccountCreator.genSshKey(), admin.email); @@ -818,6 +1118,7 @@ info = gApi.accounts().self().listSshKeys(); assertThat(info).hasSize(3); assertSequenceNumbers(info); + accountIndexedCounter.assertReindexOf(admin); // Delete second key gApi.accounts().self().deleteSshKey(2); @@ -825,6 +1126,7 @@ assertThat(info).hasSize(2); assertThat(info.get(0).seq).isEqualTo(1); assertThat(info.get(1).seq).isEqualTo(3); + accountIndexedCounter.assertReindexOf(admin); } // reindex is tested by {@link AbstractQueryAccountsTest#reindex} @@ -833,17 +1135,54 @@ // admin can reindex any account setApiUser(admin); gApi.accounts().id(user.username).index(); + accountIndexedCounter.assertReindexOf(user); // user can reindex own account setApiUser(user); gApi.accounts().self().index(); + accountIndexedCounter.assertReindexOf(user); // user cannot reindex any account exception.expect(AuthException.class); - exception.expectMessage("not allowed to index account"); + exception.expectMessage("modify account not permitted"); gApi.accounts().id(admin.username).index(); } + @Test + @Sandboxed + public void checkConsistency() throws Exception { + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + resetCurrentApiUser(); + + // Create an account with a preferred email. + String username = name("foo"); + String email = username + "@example.com"; + TestAccount account = accountCreator.create(username, email, "Foo Bar"); + + ConsistencyCheckInput input = new ConsistencyCheckInput(); + input.checkAccounts = new CheckAccountsInput(); + ConsistencyCheckInfo checkInfo = gApi.config().server().checkConsistency(input); + assertThat(checkInfo.checkAccountsResult.problems).isEmpty(); + + Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>(); + + // Delete the external ID for the preferred email. This makes the account inconsistent since it + // now doesn't have an external ID for its preferred email. + externalIdsUpdate.delete(ExternalId.createEmail(account.getId(), email)); + expectedProblems.add( + new ConsistencyProblemInfo( + ConsistencyProblemInfo.Status.ERROR, + "Account '" + + account.getId().get() + + "' has no external ID for its preferred email '" + + email + + "'")); + + checkInfo = gApi.config().server().checkConsistency(input); + assertThat(checkInfo.checkAccountsResult.problems).hasSize(expectedProblems.size()); + assertThat(checkInfo.checkAccountsResult.problems).containsExactlyElementsIn(expectedProblems); + } + private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) { int seq = 1; for (SshKeyInfo key : sshKeys) { @@ -908,7 +1247,11 @@ Iterable<String> expectedFps = expected.transform(k -> BaseEncoding.base16().encode(k.getPublicKey().getFingerprint())); Iterable<String> actualFps = - GpgKeys.getGpgExtIds(db, currAccountId).transform(e -> e.key().id()); + externalIds + .byAccount(currAccountId, SCHEME_GPGKEY) + .stream() + .map(e -> e.key().id()) + .collect(toSet()); assertThat(actualFps).named("external IDs in database").containsExactlyElementsIn(expectedFps); // Check raw stored keys. @@ -923,7 +1266,6 @@ assertThat(actual.fingerprint) .named(id) .isEqualTo(Fingerprint.toString(expected.getPublicKey().getFingerprint())); - @SuppressWarnings("unchecked") List<String> userIds = ImmutableList.copyOf(expected.getPublicKey().getUserIDs()); assertThat(actual.userIds).named(id).containsExactlyElementsIn(userIds); assertThat(actual.key).named(id).startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"); @@ -934,12 +1276,16 @@ private void addExternalIdEmail(TestAccount account, String email) throws Exception { checkNotNull(email); externalIdsUpdate.insert( - db, ExternalId.createWithEmail(name("test"), email, account.getId(), email)); + ExternalId.createWithEmail(name("test"), email, account.getId(), email)); + accountIndexedCounter.assertReindexOf(account); setApiUser(account); } private Map<String, GpgKeyInfo> addGpgKey(String armored) throws Exception { - return gApi.accounts().self().putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of()); + Map<String, GpgKeyInfo> gpgKeys = + gApi.accounts().self().putGpgKeys(ImmutableList.of(armored), ImmutableList.<String>of()); + accountIndexedCounter.assertReindexOf(gApi.accounts().self().get()); + return gpgKeys; } private void assertUser(AccountInfo info, TestAccount account) throws Exception { @@ -957,4 +1303,64 @@ assertThat(accounts).hasSize(1); assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.getId()); } + + private Config getAccountConfig(TestRepository<?> allUsersRepo) throws Exception { + Config ac = new Config(); + try (TreeWalk tw = + TreeWalk.forPath( + allUsersRepo.getRepository(), + AccountConfig.ACCOUNT_CONFIG, + getHead(allUsersRepo.getRepository()).getTree())) { + assertThat(tw).isNotNull(); + ac.fromText( + new String( + allUsersRepo + .getRevWalk() + .getObjectReader() + .open(tw.getObjectId(0), OBJ_BLOB) + .getBytes(), + UTF_8)); + } + return ac; + } + + private static class AccountIndexedCounter implements AccountIndexedListener { + private final AtomicLongMap<Integer> countsByAccount = AtomicLongMap.create(); + + @Override + public void onAccountIndexed(int id) { + countsByAccount.incrementAndGet(id); + } + + void clear() { + countsByAccount.clear(); + } + + long getCount(Account.Id accountId) { + return countsByAccount.get(accountId.get()); + } + + void assertReindexOf(TestAccount testAccount) { + assertReindexOf(testAccount, 1); + } + + void assertReindexOf(AccountInfo accountInfo) { + assertReindexOf(new Account.Id(accountInfo._accountId), 1); + } + + void assertReindexOf(TestAccount testAccount, int expectedCount) { + assertThat(getCount(testAccount.id)).isEqualTo(expectedCount); + assertThat(countsByAccount).hasSize(1); + clear(); + } + + void assertReindexOf(Account.Id accountId, int expectedCount) { + assertThat(getCount(accountId)).isEqualTo(expectedCount); + countsByAccount.remove(accountId.get()); + } + + void assertNoReindex() { + assertThat(countsByAccount).isEmpty(); + } + } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java index fbeeafd..b1ca5d0 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/GeneralPreferencesIT.java
@@ -52,7 +52,7 @@ @Before public void setUp() throws Exception { String name = name("user42"); - user42 = accounts.create(name, name + "@example.com", "User 42"); + user42 = accountCreator.create(name, name + "@example.com", "User 42"); } @After @@ -66,7 +66,7 @@ assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED); } } - accountCache.evictAll(); + accountCache.evictAllNoReindex(); } @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/AbandonIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/AbandonIT.java new file mode 100644 index 0000000..cb9d705 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/AbandonIT.java
@@ -0,0 +1,203 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.change; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static java.util.concurrent.TimeUnit.HOURS; +import static java.util.concurrent.TimeUnit.SECONDS; +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.GerritConfig; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.extensions.client.ChangeStatus; +import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.change.AbandonUtil; +import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.testutil.TestTimeUtil; +import com.google.inject.Inject; +import java.util.List; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.TestRepository; +import org.junit.Test; + +public class AbandonIT extends AbstractDaemonTest { + @Inject private AbandonUtil abandonUtil; + + @Test + public void abandon() throws Exception { + PushOneCommit.Result r = createChange(); + String changeId = r.getChangeId(); + assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); + gApi.changes().id(changeId).abandon(); + ChangeInfo info = get(changeId); + assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); + assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("abandoned"); + + exception.expect(ResourceConflictException.class); + exception.expectMessage("change is abandoned"); + gApi.changes().id(changeId).abandon(); + } + + @Test + public void batchAbandon() throws Exception { + CurrentUser user = atrScope.get().getUser(); + PushOneCommit.Result a = createChange(); + List<ChangeControl> controlA = changeFinder.find(a.getChangeId(), user); + assertThat(controlA).hasSize(1); + PushOneCommit.Result b = createChange(); + List<ChangeControl> controlB = changeFinder.find(b.getChangeId(), user); + assertThat(controlB).hasSize(1); + List<ChangeControl> list = ImmutableList.of(controlA.get(0), controlB.get(0)); + changeAbandoner.batchAbandon( + batchUpdateFactory, controlA.get(0).getProject().getNameKey(), user, list, "deadbeef"); + + ChangeInfo info = get(a.getChangeId()); + assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); + assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("abandoned"); + assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("deadbeef"); + + info = get(b.getChangeId()); + assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); + assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("abandoned"); + assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("deadbeef"); + } + + @Test + public void batchAbandonChangeProject() throws Exception { + String project1Name = name("Project1"); + String project2Name = name("Project2"); + gApi.projects().create(project1Name); + gApi.projects().create(project2Name); + TestRepository<InMemoryRepository> project1 = cloneProject(new Project.NameKey(project1Name)); + TestRepository<InMemoryRepository> project2 = cloneProject(new Project.NameKey(project2Name)); + + CurrentUser user = atrScope.get().getUser(); + PushOneCommit.Result a = createChange(project1, "master", "x", "x", "x", ""); + List<ChangeControl> controlA = changeFinder.find(a.getChangeId(), user); + assertThat(controlA).hasSize(1); + PushOneCommit.Result b = createChange(project2, "master", "x", "x", "x", ""); + List<ChangeControl> controlB = changeFinder.find(b.getChangeId(), user); + assertThat(controlB).hasSize(1); + List<ChangeControl> list = ImmutableList.of(controlA.get(0), controlB.get(0)); + exception.expect(ResourceConflictException.class); + exception.expectMessage( + String.format("Project name \"%s\" doesn't match \"%s\"", project2Name, project1Name)); + changeAbandoner.batchAbandon(batchUpdateFactory, new Project.NameKey(project1Name), user, list); + } + + @Test + public void abandonDraft() throws Exception { + PushOneCommit.Result r = createDraftChange(); + String changeId = r.getChangeId(); + assertThat(info(changeId).status).isEqualTo(ChangeStatus.DRAFT); + + exception.expect(ResourceConflictException.class); + exception.expectMessage("draft changes cannot be abandoned"); + gApi.changes().id(changeId).abandon(); + } + + @Test + @GerritConfig(name = "changeCleanup.abandonAfter", value = "1w") + public void abandonInactiveOpenChanges() throws Exception { + TestTimeUtil.resetWithClockStep(1, SECONDS); + + // create 2 changes which will be abandoned ... + int id1 = createChange().getChange().getId().get(); + int id2 = createChange().getChange().getId().get(); + + // ... because they are older than 1 week + TestTimeUtil.incrementClock(7 * 24, HOURS); + + // create 1 new change that will not be abandoned + ChangeData cd = createChange().getChange(); + int id3 = cd.getId().get(); + + assertThat(toChangeNumbers(query("is:open"))).containsExactly(id1, id2, id3); + assertThat(query("is:abandoned")).isEmpty(); + + abandonUtil.abandonInactiveOpenChanges(batchUpdateFactory); + assertThat(toChangeNumbers(query("is:open"))).containsExactly(id3); + assertThat(toChangeNumbers(query("is:abandoned"))).containsExactly(id1, id2); + } + + @Test + public void abandonNotAllowedWithoutPermission() throws Exception { + PushOneCommit.Result r = createChange(); + String changeId = r.getChangeId(); + assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("abandon not permitted"); + gApi.changes().id(changeId).abandon(); + } + + @Test + public void abandonAndRestoreAllowedWithPermission() throws Exception { + PushOneCommit.Result r = createChange(); + String changeId = r.getChangeId(); + assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); + grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS); + setApiUser(user); + gApi.changes().id(changeId).abandon(); + assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED); + gApi.changes().id(changeId).restore(); + assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); + } + + @Test + public void restore() throws Exception { + PushOneCommit.Result r = createChange(); + String changeId = r.getChangeId(); + assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); + gApi.changes().id(changeId).abandon(); + assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED); + + gApi.changes().id(changeId).restore(); + ChangeInfo info = get(changeId); + assertThat(info.status).isEqualTo(ChangeStatus.NEW); + assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("restored"); + + exception.expect(ResourceConflictException.class); + exception.expectMessage("change is new"); + gApi.changes().id(changeId).restore(); + } + + @Test + public void restoreNotAllowedWithoutPermission() throws Exception { + PushOneCommit.Result r = createChange(); + String changeId = r.getChangeId(); + assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); + gApi.changes().id(changeId).abandon(); + setApiUser(user); + assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED); + exception.expect(AuthException.class); + exception.expectMessage("restore not permitted"); + gApi.changes().id(changeId).restore(); + } + + private List<Integer> toChangeNumbers(List<ChangeInfo> changes) { + return changes.stream().map(i -> i._number).collect(toList()); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java index c92006b..b4f55cb 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -22,6 +22,7 @@ import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME; import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT; 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 static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef; import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; @@ -54,6 +55,7 @@ import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.Permission; import com.google.gerrit.extensions.api.changes.AddReviewerInput; +import com.google.gerrit.extensions.api.changes.AddReviewerResult; import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; import com.google.gerrit.extensions.api.changes.DeleteVoteInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; @@ -62,11 +64,14 @@ import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling; +import com.google.gerrit.extensions.api.changes.ReviewResult; import com.google.gerrit.extensions.api.changes.RevisionApi; import com.google.gerrit.extensions.api.projects.BranchInput; +import com.google.gerrit.extensions.api.projects.ConfigInput; import com.google.gerrit.extensions.client.ChangeKind; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.Comment.Range; +import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.client.Side; @@ -90,7 +95,6 @@ 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.UnprocessableEntityException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Branch; @@ -100,7 +104,7 @@ import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; -import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.config.AnonymousCowardNameProvider; import com.google.gerrit.server.git.ChangeMessageModifier; @@ -114,6 +118,7 @@ import com.google.gerrit.testutil.FakeEmailSender.Message; import com.google.gerrit.testutil.TestTimeUtil; import com.google.inject.Inject; +import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; @@ -126,6 +131,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Constants; @@ -142,8 +149,6 @@ public class ChangeIT extends AbstractDaemonTest { private String systemTimeZone; - @Inject private BatchUpdate.Factory updateFactory; - @Inject private DynamicSet<ChangeMessageModifier> changeMessageModifiers; @Before @@ -158,6 +163,16 @@ } @Test + public void reflog() throws Exception { + // Tests are using DfsRepository which does not implement getReflogReader, + // so this will always fail. + // TODO: change this if/when DfsRepository#getReflogReader is implemented. + exception.expect(MethodNotAllowedException.class); + exception.expectMessage("reflog not supported"); + gApi.projects().name(project.get()).branch("master").reflog(); + } + + @Test public void get() throws Exception { PushOneCommit.Result r = createChange(); String triplet = project.get() + "~master~" + r.getChangeId(); @@ -181,6 +196,313 @@ } @Test + public void setPrivateByOwner() throws Exception { + TestRepository<InMemoryRepository> userRepo = cloneProject(project, user); + PushOneCommit.Result result = + pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master"); + + setApiUser(user); + String changeId = result.getChangeId(); + assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); + + gApi.changes().id(changeId).setPrivate(true, null); + ChangeInfo info = gApi.changes().id(changeId).get(); + assertThat(info.isPrivate).isTrue(); + assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private"); + assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE); + + gApi.changes().id(changeId).setPrivate(false, null); + info = gApi.changes().id(changeId).get(); + assertThat(info.isPrivate).isNull(); + assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private"); + assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE); + + String msg = "This is a security fix that must not be public."; + gApi.changes().id(changeId).setPrivate(true, msg); + info = gApi.changes().id(changeId).get(); + assertThat(info.isPrivate).isTrue(); + assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private\n\n" + msg); + assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE); + + msg = "After this security fix has been released we can make it public now."; + gApi.changes().id(changeId).setPrivate(false, msg); + info = gApi.changes().id(changeId).get(); + assertThat(info.isPrivate).isNull(); + assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private\n\n" + msg); + assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE); + } + + @Test + public void administratorCanSetUserChangePrivate() throws Exception { + TestRepository<InMemoryRepository> userRepo = cloneProject(project, user); + PushOneCommit.Result result = + pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master"); + + String changeId = result.getChangeId(); + assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); + + gApi.changes().id(changeId).setPrivate(true, null); + setApiUser(user); + ChangeInfo info = gApi.changes().id(changeId).get(); + assertThat(info.isPrivate).isTrue(); + } + + @Test + public void cannotSetOtherUsersChangePrivate() throws Exception { + PushOneCommit.Result result = createChange(); + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("not allowed to mark private"); + gApi.changes().id(result.getChangeId()).setPrivate(true, null); + } + + @Test + public void accessPrivate() throws Exception { + TestRepository<InMemoryRepository> userRepo = cloneProject(project, user); + PushOneCommit.Result result = + pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master"); + + setApiUser(user); + gApi.changes().id(result.getChangeId()).setPrivate(true, null); + // Owner can always access its private changes. + assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue(); + + // Add admin as a reviewer. + gApi.changes().id(result.getChangeId()).addReviewer(admin.getId().toString()); + + // This change should be visible for admin as a reviewer. + setApiUser(admin); + assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue(); + + // Remove admin from reviewers. + gApi.changes().id(result.getChangeId()).reviewer(admin.getId().toString()).remove(); + + // This change should not be visible for admin anymore. + exception.expect(ResourceNotFoundException.class); + exception.expectMessage("Not found: " + result.getChangeId()); + gApi.changes().id(result.getChangeId()); + } + + @Test + public void privateChangeOfOtherUserCanBeAccessedWithPermission() throws Exception { + PushOneCommit.Result result = createChange(); + gApi.changes().id(result.getChangeId()).setPrivate(true, null); + + allow(Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS, "refs/*"); + setApiUser(user); + assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue(); + } + + @Test + public void administratorCanUnmarkPrivateAfterMerging() throws Exception { + PushOneCommit.Result result = createChange(); + String changeId = result.getChangeId(); + gApi.changes().id(changeId).setPrivate(true, null); + assertThat(gApi.changes().id(changeId).get().isPrivate).isTrue(); + merge(result); + gApi.changes().id(changeId).setPrivate(false, null); + assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); + } + + @Test + public void administratorCanMarkPrivateAfterMerging() throws Exception { + PushOneCommit.Result result = createChange(); + String changeId = result.getChangeId(); + assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); + merge(result); + gApi.changes().id(changeId).setPrivate(true, null); + assertThat(gApi.changes().id(changeId).get().isPrivate).isTrue(); + } + + @Test + public void userCannotMarkPrivateAfterMerging() throws Exception { + TestRepository<InMemoryRepository> userRepo = cloneProject(project, user); + PushOneCommit.Result result = + pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master"); + + String changeId = result.getChangeId(); + assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); + + merge(result); + + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("not allowed to mark private"); + gApi.changes().id(changeId).setPrivate(true, null); + } + + @Test + public void userCannotUnmarkPrivateAfterMerging() throws Exception { + TestRepository<InMemoryRepository> userRepo = cloneProject(project, user); + PushOneCommit.Result result = + pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master"); + + String changeId = result.getChangeId(); + assertThat(gApi.changes().id(changeId).get().isPrivate).isNull(); + gApi.changes().id(changeId).addReviewer(admin.getId().toString()); + gApi.changes().id(changeId).setPrivate(true, null); + assertThat(gApi.changes().id(changeId).get().isPrivate).isTrue(); + + merge(result); + + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("not allowed to unmark private"); + gApi.changes().id(changeId).setPrivate(false, null); + } + + @Test + public void setWorkInProgressNotAllowedWithoutPermission() throws Exception { + PushOneCommit.Result rwip = createChange(); + String changeId = rwip.getChangeId(); + + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("not allowed to set work in progress"); + gApi.changes().id(changeId).setWorkInProgress(); + } + + @Test + public void setReadyForReviewNotAllowedWithoutPermission() throws Exception { + PushOneCommit.Result rready = createChange(); + String changeId = rready.getChangeId(); + gApi.changes().id(changeId).setWorkInProgress(); + + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("not allowed to set ready for review"); + gApi.changes().id(changeId).setReadyForReview(); + } + + @Test + public void hasReviewStarted() throws Exception { + PushOneCommit.Result r = createWorkInProgressChange(); + String changeId = r.getChangeId(); + ChangeInfo info = gApi.changes().id(changeId).get(); + assertThat(info.hasReviewStarted).isFalse(); + + gApi.changes().id(changeId).setReadyForReview(); + info = gApi.changes().id(changeId).get(); + assertThat(info.hasReviewStarted).isTrue(); + } + + @Test + public void pendingReviewersInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + ConfigInput conf = new ConfigInput(); + conf.enableReviewerByEmail = InheritableBoolean.TRUE; + gApi.projects().name(project.get()).config(conf); + + PushOneCommit.Result r = createWorkInProgressChange(); + String changeId = r.getChangeId(); + assertThat(gApi.changes().id(changeId).get().pendingReviewers).isEmpty(); + + // Add some pending reviewers. + TestAccount user1 = + accountCreator.create(name("user1"), name("user1") + "@example.com", "User 1"); + TestAccount user2 = + accountCreator.create(name("user2"), name("user2") + "@example.com", "User 2"); + TestAccount user3 = + accountCreator.create(name("user3"), name("user3") + "@example.com", "User 3"); + TestAccount user4 = + accountCreator.create(name("user4"), name("user4") + "@example.com", "User 4"); + ReviewInput in = + ReviewInput.noScore() + .reviewer(user1.email) + .reviewer(user2.email) + .reviewer(user3.email, CC, false) + .reviewer(user4.email, CC, false) + .reviewer("byemail1@example.com") + .reviewer("byemail2@example.com") + .reviewer("byemail3@example.com", CC, false) + .reviewer("byemail4@example.com", CC, false); + ReviewResult result = gApi.changes().id(changeId).revision("current").review(in); + assertThat(result.reviewers).isNotEmpty(); + ChangeInfo info = gApi.changes().id(changeId).get(); + Function<Collection<AccountInfo>, Collection<String>> toEmails = + ais -> ais.stream().map(ai -> ai.email).collect(Collectors.toSet()); + assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER))) + .containsExactly( + admin.email, user1.email, user2.email, "byemail1@example.com", "byemail2@example.com"); + assertThat(toEmails.apply(info.pendingReviewers.get(CC))) + .containsExactly(user3.email, user4.email, "byemail3@example.com", "byemail4@example.com"); + assertThat(info.pendingReviewers.get(REMOVED)).isNull(); + + // Stage some pending reviewer removals. + gApi.changes().id(changeId).reviewer(user1.email).remove(); + gApi.changes().id(changeId).reviewer(user3.email).remove(); + gApi.changes().id(changeId).reviewer("byemail1@example.com").remove(); + gApi.changes().id(changeId).reviewer("byemail3@example.com").remove(); + info = gApi.changes().id(changeId).get(); + assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER))) + .containsExactly(admin.email, user2.email, "byemail2@example.com"); + assertThat(toEmails.apply(info.pendingReviewers.get(CC))) + .containsExactly(user4.email, "byemail4@example.com"); + assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED))) + .containsExactly(user1.email, user3.email, "byemail1@example.com", "byemail3@example.com"); + + // "Undo" a removal. + in = ReviewInput.noScore().reviewer(user1.email); + gApi.changes().id(changeId).revision("current").review(in); + info = gApi.changes().id(changeId).get(); + assertThat(toEmails.apply(info.pendingReviewers.get(REVIEWER))) + .containsExactly(admin.email, user1.email, user2.email, "byemail2@example.com"); + assertThat(toEmails.apply(info.pendingReviewers.get(CC))) + .containsExactly(user4.email, "byemail4@example.com"); + assertThat(toEmails.apply(info.pendingReviewers.get(REMOVED))) + .containsExactly(user3.email, "byemail1@example.com", "byemail3@example.com"); + + // "Commit" by moving out of WIP. + gApi.changes().id(changeId).setReadyForReview(); + info = gApi.changes().id(changeId).get(); + assertThat(info.pendingReviewers).isEmpty(); + assertThat(toEmails.apply(info.reviewers.get(REVIEWER))) + .containsExactly(admin.email, user1.email, user2.email, "byemail2@example.com"); + assertThat(toEmails.apply(info.reviewers.get(CC))) + .containsExactly(user4.email, "byemail4@example.com"); + assertThat(info.reviewers.get(REMOVED)).isNull(); + } + + @Test + public void toggleWorkInProgressState() throws Exception { + PushOneCommit.Result r = createChange(); + String changeId = r.getChangeId(); + + // With message + gApi.changes().id(changeId).setWorkInProgress("Needs some refactoring"); + + ChangeInfo info = gApi.changes().id(changeId).get(); + + assertThat(info.workInProgress).isTrue(); + assertThat(Iterables.getLast(info.messages).message).contains("Needs some refactoring"); + assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP); + + gApi.changes().id(changeId).setReadyForReview("PTAL"); + + info = gApi.changes().id(changeId).get(); + assertThat(info.workInProgress).isNull(); + assertThat(Iterables.getLast(info.messages).message).contains("PTAL"); + assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY); + + // No message + gApi.changes().id(changeId).setWorkInProgress(); + + info = gApi.changes().id(changeId).get(); + + assertThat(info.workInProgress).isTrue(); + assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set Work In Progress"); + assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_WIP); + + gApi.changes().id(changeId).setReadyForReview(); + + info = gApi.changes().id(changeId).get(); + assertThat(info.workInProgress).isNull(); + assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set Ready For Review"); + assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_READY); + } + + @Test public void getAmbiguous() throws Exception { PushOneCommit.Result r1 = createChange(); String changeId = r1.getChangeId(); @@ -208,96 +530,6 @@ } @Test - public void abandon() throws Exception { - PushOneCommit.Result r = createChange(); - String changeId = r.getChangeId(); - assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); - gApi.changes().id(changeId).abandon(); - ChangeInfo info = get(changeId); - assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); - assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("abandoned"); - - exception.expect(ResourceConflictException.class); - exception.expectMessage("change is abandoned"); - gApi.changes().id(changeId).abandon(); - } - - @Test - public void batchAbandon() throws Exception { - CurrentUser user = atrScope.get().getUser(); - PushOneCommit.Result a = createChange(); - List<ChangeControl> controlA = changeFinder.find(a.getChangeId(), user); - assertThat(controlA).hasSize(1); - PushOneCommit.Result b = createChange(); - List<ChangeControl> controlB = changeFinder.find(b.getChangeId(), user); - assertThat(controlB).hasSize(1); - List<ChangeControl> list = ImmutableList.of(controlA.get(0), controlB.get(0)); - changeAbandoner.batchAbandon(controlA.get(0).getProject().getNameKey(), user, list, "deadbeef"); - - ChangeInfo info = get(a.getChangeId()); - assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); - assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("abandoned"); - assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("deadbeef"); - - info = get(b.getChangeId()); - assertThat(info.status).isEqualTo(ChangeStatus.ABANDONED); - assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("abandoned"); - assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("deadbeef"); - } - - @Test - public void batchAbandonChangeProject() throws Exception { - String project1Name = name("Project1"); - String project2Name = name("Project2"); - gApi.projects().create(project1Name); - gApi.projects().create(project2Name); - TestRepository<InMemoryRepository> project1 = cloneProject(new Project.NameKey(project1Name)); - TestRepository<InMemoryRepository> project2 = cloneProject(new Project.NameKey(project2Name)); - - CurrentUser user = atrScope.get().getUser(); - PushOneCommit.Result a = createChange(project1, "master", "x", "x", "x", ""); - List<ChangeControl> controlA = changeFinder.find(a.getChangeId(), user); - assertThat(controlA).hasSize(1); - PushOneCommit.Result b = createChange(project2, "master", "x", "x", "x", ""); - List<ChangeControl> controlB = changeFinder.find(b.getChangeId(), user); - assertThat(controlB).hasSize(1); - List<ChangeControl> list = ImmutableList.of(controlA.get(0), controlB.get(0)); - exception.expect(ResourceConflictException.class); - exception.expectMessage( - String.format("Project name \"%s\" doesn't match \"%s\"", project2Name, project1Name)); - changeAbandoner.batchAbandon(new Project.NameKey(project1Name), user, list); - } - - @Test - public void abandonDraft() throws Exception { - PushOneCommit.Result r = createDraftChange(); - String changeId = r.getChangeId(); - assertThat(info(changeId).status).isEqualTo(ChangeStatus.DRAFT); - - exception.expect(ResourceConflictException.class); - exception.expectMessage("draft changes cannot be abandoned"); - gApi.changes().id(changeId).abandon(); - } - - @Test - public void restore() throws Exception { - PushOneCommit.Result r = createChange(); - String changeId = r.getChangeId(); - assertThat(info(changeId).status).isEqualTo(ChangeStatus.NEW); - gApi.changes().id(changeId).abandon(); - assertThat(info(changeId).status).isEqualTo(ChangeStatus.ABANDONED); - - gApi.changes().id(changeId).restore(); - ChangeInfo info = get(changeId); - assertThat(info.status).isEqualTo(ChangeStatus.NEW); - assertThat(Iterables.getLast(info.messages).message.toLowerCase()).contains("restored"); - - exception.expect(ResourceConflictException.class); - exception.expectMessage("change is new"); - gApi.changes().id(changeId).restore(); - } - - @Test public void revert() throws Exception { PushOneCommit.Result r = createChange(); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); @@ -321,6 +553,40 @@ } @Test + public void revertPreservesReviewersAndCcs() throws Exception { + PushOneCommit.Result r = createChange(); + + ReviewInput in = ReviewInput.approve(); + in.reviewer(user.email); + in.reviewer(accountCreator.user2().email, ReviewerState.CC, true); + // Add user as reviewer that will create the revert + in.reviewer(accountCreator.admin2().email); + + gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(in); + gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(); + + // expect both the original reviewers and CCs to be preserved + // original owner should be added as reviewer, user requesting the revert (new owner) removed + setApiUser(accountCreator.admin2()); + Map<ReviewerState, Collection<AccountInfo>> result = + gApi.changes().id(r.getChangeId()).revert().get().reviewers; + assertThat(result).containsKey(ReviewerState.REVIEWER); + + List<Integer> reviewers = + result.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList()); + if (notesMigration.readChanges()) { + assertThat(result).containsKey(ReviewerState.CC); + List<Integer> ccs = + result.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList()); + assertThat(ccs).containsExactly(accountCreator.user2().id.get()); + assertThat(reviewers).containsExactly(user.id.get(), admin.id.get()); + } else { + assertThat(reviewers) + .containsExactly(user.id.get(), admin.id.get(), accountCreator.user2().id.get()); + } + } + + @Test @TestProjectInput(createEmptyCommit = false) public void revertInitialCommit() throws Exception { PushOneCommit.Result r = createChange(); @@ -358,12 +624,18 @@ revision.review(ReviewInput.approve()); revision.submit(); + // Add an approval whose score should be copied on trivial rebase + gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.recommend()); + String changeId = r2.getChangeId(); // Rebase the second change rebase.call(changeId); - // Second change should have 2 patch sets - ChangeInfo c2 = gApi.changes().id(changeId).get(); + // Second change should have 2 patch sets and an approval + ChangeInfo c2 = + gApi.changes() + .id(changeId) + .get(EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.DETAILED_LABELS)); assertThat(c2.revisions.get(c2.currentRevision)._number).isEqualTo(2); // ...and the committer and description should be correct @@ -377,6 +649,20 @@ String description = info.revisions.get(info.currentRevision).description; assertThat(description).isEqualTo("Rebase"); + // ...and the approval was copied + LabelInfo cr = c2.labels.get("Code-Review"); + assertThat(cr).isNotNull(); + assertThat(cr.all).hasSize(1); + assertThat(cr.all.get(0).value).isEqualTo(1); + + if (notesMigration.changePrimaryStorage() == PrimaryStorage.REVIEW_DB) { + // Ensure record was actually copied under ReviewDb + List<PatchSetApproval> psas = + db.patchSetApprovals().byPatchSet(new PatchSet.Id(new Change.Id(c2._number), 2)).toList(); + assertThat(psas).hasSize(1); + assertThat(psas.get(0).getValue()).isEqualTo((short) 1); + } + // Rebasing the second change again should fail exception.expect(ResourceConflictException.class); exception.expectMessage("Change is already up to date"); @@ -415,7 +701,7 @@ revision.review(ReviewInput.approve()); revision.submit(); - grant(Permission.REBASE, project, "refs/heads/master", false, REGISTERED_USERS); + grant(project, "refs/heads/master", Permission.REBASE, false, REGISTERED_USERS); // Rebase the second String changeId = r2.getChangeId(); @@ -424,6 +710,50 @@ } @Test + public void rebaseNotAllowedWithoutPushPermission() throws Exception { + // Create two changes both with the same parent + PushOneCommit.Result r = createChange(); + testRepo.reset("HEAD~1"); + PushOneCommit.Result r2 = createChange(); + + // Approve and submit the first change + RevisionApi revision = gApi.changes().id(r.getChangeId()).current(); + revision.review(ReviewInput.approve()); + revision.submit(); + + grant(project, "refs/heads/master", Permission.REBASE, false, REGISTERED_USERS); + block("refs/for/*", Permission.PUSH, REGISTERED_USERS); + + // Rebase the second + String changeId = r2.getChangeId(); + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("rebase not permitted"); + gApi.changes().id(changeId).rebase(); + } + + @Test + public void rebaseNotAllowedForOwnerWithoutPushPermission() throws Exception { + // Create two changes both with the same parent + PushOneCommit.Result r = createChange(); + testRepo.reset("HEAD~1"); + PushOneCommit.Result r2 = createChange(); + + // Approve and submit the first change + RevisionApi revision = gApi.changes().id(r.getChangeId()).current(); + revision.review(ReviewInput.approve()); + revision.submit(); + + block("refs/for/*", Permission.PUSH, REGISTERED_USERS); + + // Rebase the second + String changeId = r2.getChangeId(); + exception.expect(AuthException.class); + exception.expectMessage("rebase not permitted"); + gApi.changes().id(changeId).rebase(); + } + + @Test public void publish() throws Exception { PushOneCommit.Result r = createChange("refs/drafts/master"); assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.DRAFT); @@ -456,11 +786,10 @@ PushOneCommit.Result changeResult = pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master"); String changeId = changeResult.getChangeId(); - Change.Id id = changeResult.getChange().getId(); setApiUser(user); exception.expect(AuthException.class); - exception.expectMessage(String.format("Deleting change %s is not permitted", id)); + exception.expectMessage("delete not permitted"); gApi.changes().id(changeId).delete(); } @@ -473,13 +802,18 @@ PushOneCommit.Result changeResult = pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master"); String changeId = changeResult.getChangeId(); + int id = changeResult.getChange().getId().id; + RevCommit commit = changeResult.getCommit(); setApiUser(user); gApi.changes().id(changeId).delete(); assertThat(query(changeId)).isEmpty(); + + String ref = new Change.Id(id).toRefPrefix() + "1"; + eventRecorder.assertRefUpdatedEvents(project.get(), ref, null, commit, commit, null); } finally { - removePermission(Permission.DELETE_OWN_CHANGES, project, "refs/*"); + removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES); } } @@ -504,14 +838,13 @@ try { PushOneCommit.Result changeResult = createChange(); String changeId = changeResult.getChangeId(); - Change.Id id = changeResult.getChange().getId(); setApiUser(user); exception.expect(AuthException.class); - exception.expectMessage(String.format("Deleting change %s is not permitted", id)); + exception.expectMessage("delete not permitted"); gApi.changes().id(changeId).delete(); } finally { - removePermission(Permission.DELETE_OWN_CHANGES, project, "refs/*"); + removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES); } } @@ -532,13 +865,12 @@ PushOneCommit.Result changeResult = pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master"); String changeId = changeResult.getChangeId(); - Change.Id id = changeResult.getChange().getId(); setApiUser(user); gApi.changes().id(changeId).abandon(); exception.expect(AuthException.class); - exception.expectMessage(String.format("Deleting change %s is not permitted", id)); + exception.expectMessage("delete not permitted"); gApi.changes().id(changeId).delete(); } @@ -560,12 +892,11 @@ public void deleteMergedChange() throws Exception { PushOneCommit.Result changeResult = createChange(); String changeId = changeResult.getChangeId(); - Change.Id id = changeResult.getChange().getId(); merge(changeResult); exception.expect(MethodNotAllowedException.class); - exception.expectMessage(String.format("Deleting merged change %s is not allowed", id)); + exception.expectMessage("delete not permitted"); gApi.changes().id(changeId).delete(); } @@ -578,16 +909,15 @@ PushOneCommit.Result changeResult = pushFactory.create(db, user.getIdent(), testRepo).to("refs/for/master"); String changeId = changeResult.getChangeId(); - Change.Id id = changeResult.getChange().getId(); merge(changeResult); setApiUser(user); exception.expect(MethodNotAllowedException.class); - exception.expectMessage(String.format("Deleting merged change %s is not allowed", id)); + exception.expectMessage("delete not permitted"); gApi.changes().id(changeId).delete(); } finally { - removePermission(Permission.DELETE_OWN_CHANGES, project, "refs/*"); + removePermission(project, "refs/*", Permission.DELETE_OWN_CHANGES); } } @@ -961,23 +1291,52 @@ setApiUser(admin); AddReviewerInput in = new AddReviewerInput(); in.reviewer = user.email; - exception.expect(UnprocessableEntityException.class); - exception.expectMessage("Change not visible to " + user.email); - gApi.changes().id(result.getChangeId()).addReviewer(in); + AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in); + + assertThat(r.input).isEqualTo(user.email); + assertThat(r.error).contains("does not have permission to see this change"); + assertThat(r.reviewers).isNull(); } @Test public void addReviewerThatIsInactive() throws Exception { - PushOneCommit.Result r = createChange(); + PushOneCommit.Result result = createChange(); String username = name("new-user"); gApi.accounts().create(username).setActive(false); AddReviewerInput in = new AddReviewerInput(); in.reviewer = username; - exception.expect(UnprocessableEntityException.class); - exception.expectMessage("Account of " + username + " is inactive."); - gApi.changes().id(r.getChangeId()).addReviewer(in); + AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in); + + assertThat(r.input).isEqualTo(username); + assertThat(r.error).contains("identifies an inactive account"); + assertThat(r.reviewers).isNull(); + } + + @Test + public void addReviewerThatIsInactiveEmailFallback() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + ConfigInput conf = new ConfigInput(); + conf.enableReviewerByEmail = InheritableBoolean.TRUE; + gApi.projects().name(project.get()).config(conf); + + PushOneCommit.Result result = createChange(); + + String username = "user@domain.com"; + gApi.accounts().create(username).setActive(false); + + AddReviewerInput in = new AddReviewerInput(); + in.reviewer = username; + in.state = ReviewerState.CC; + AddReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in); + + assertThat(r.input).isEqualTo(username); + assertThat(r.error).isNull(); + // When adding by email, the reviewers field is also empty because we can't + // render a ReviewerInfo object for a non-account. + assertThat(r.reviewers).isNull(); } @Test @@ -1018,6 +1377,40 @@ } @Test + public void notificationsForAddedWorkInProgressReviewers() throws Exception { + AddReviewerInput in = new AddReviewerInput(); + in.reviewer = user.email; + ReviewInput batchIn = new ReviewInput(); + batchIn.reviewers = ImmutableList.of(in); + + // Added reviewers not notified by default. + PushOneCommit.Result r = createWorkInProgressChange(); + gApi.changes().id(r.getChangeId()).addReviewer(in); + assertThat(sender.getMessages()).hasSize(0); + + // Default notification handling can be overridden. + r = createWorkInProgressChange(); + in.notify = NotifyHandling.OWNER_REVIEWERS; + gApi.changes().id(r.getChangeId()).addReviewer(in); + assertThat(sender.getMessages()).hasSize(1); + sender.clear(); + + // Reviewers added via PostReview also not notified by default. + // In this case, the child ReviewerInput has a notify=OWNER_REVIEWERS + // that should be ignored. + r = createWorkInProgressChange(); + gApi.changes().id(r.getChangeId()).revision("current").review(batchIn); + assertThat(sender.getMessages()).hasSize(0); + + // Top-level notify property can force notifications when adding reviewer + // via PostReview. + r = createWorkInProgressChange(); + batchIn.notify = NotifyHandling.OWNER_REVIEWERS; + gApi.changes().id(r.getChangeId()).revision("current").review(batchIn); + assertThat(sender.getMessages()).hasSize(1); + } + + @Test public void addReviewerWithNoteDbWhenDummyApprovalInReviewDbExists() throws Exception { assume().that(notesMigration.enabled()).isTrue(); assume().that(notesMigration.changePrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB); @@ -1399,7 +1792,7 @@ in.notify = NotifyHandling.NONE; // notify unrelated account as TO - TestAccount user2 = accounts.user2(); + TestAccount user2 = accountCreator.user2(); setApiUser(user); recommend(r.getChangeId()); setApiUser(admin); @@ -1603,7 +1996,7 @@ Iterables.getOnlyElement(query("project:{" + project.get() + "} owner:self")).changeId) .isEqualTo(r.getChangeId()); setApiUser(user); - assertThat(query("owner:self")).isEmpty(); + assertThat(query("owner:self project:{" + project.get() + "}")).isEmpty(); } @Test @@ -1631,6 +2024,26 @@ } @Test + public void editTopicWithoutPermissionNotAllowed() throws Exception { + PushOneCommit.Result r = createChange(); + assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo(""); + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("edit topic name not permitted"); + gApi.changes().id(r.getChangeId()).topic("mytopic"); + } + + @Test + public void editTopicWithPermissionAllowed() throws Exception { + PushOneCommit.Result r = createChange(); + assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo(""); + grant(project, "refs/heads/master", Permission.EDIT_TOPIC_NAME, false, REGISTERED_USERS); + setApiUser(user); + gApi.changes().id(r.getChangeId()).topic("mytopic"); + assertThat(gApi.changes().id(r.getChangeId()).topic()).isEqualTo("mytopic"); + } + + @Test public void submitted() throws Exception { PushOneCommit.Result r = createChange(); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); @@ -1657,6 +2070,26 @@ } @Test + public void submitNotAllowedWithoutPermission() throws Exception { + PushOneCommit.Result r = createChange(); + gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("submit not permitted"); + gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(); + } + + @Test + public void submitAllowedWithPermission() throws Exception { + PushOneCommit.Result r = createChange(); + gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); + grant(project, "refs/heads/master", Permission.SUBMIT, false, REGISTERED_USERS); + setApiUser(user); + gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).submit(); + assertThat(gApi.changes().id(r.getChangeId()).info().status).isEqualTo(ChangeStatus.MERGED); + } + + @Test public void check() throws Exception { // TODO(dborowitz): Re-enable when ConsistencyChecker supports NoteDb. assume().that(notesMigration.enabled()).isFalse(); @@ -1999,7 +2432,7 @@ TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user); // Block default permission - block(Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/for/*", p); + block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS); // Create change as admin PushOneCommit push = pushFactory.create(db, admin.getIdent(), adminTestRepo); @@ -2043,7 +2476,7 @@ TestRepository<?> adminTestRepo = cloneProject(project, admin); // Block default permission - block(Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/for/*", p); + block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS); // Create change as admin PushOneCommit push = pushFactory.create(db, admin.getIdent(), adminTestRepo); @@ -2094,7 +2527,7 @@ TestRepository<?> userTestRepo = cloneProject(p, user); // Block default permission - block(Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/for/*", p); + block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS); // Create change as admin PushOneCommit push = pushFactory.create(db, admin.getIdent(), adminTestRepo); @@ -2496,6 +2929,99 @@ gApi.changes().id(result2.getChangeId()).current().submit(); } + @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. + PushOneCommit.Result r = createChange(); + r.assertOkStatus(); + assertThat(getCommitMessage(r.getChangeId())) + .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n"); + + for (TestAccount acc : ImmutableList.of(admin, user)) { + setApiUser(acc); + String newMessage = + "modified commit by " + acc.username + "\n\nChange-Id: " + r.getChangeId() + "\n"; + gApi.changes().id(r.getChangeId()).setMessage(newMessage); + RevisionApi rApi = gApi.changes().id(r.getChangeId()).current(); + assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt"); + assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage); + assertThat(rApi.description()).isEqualTo("Edit commit message"); + } + } + + @Test + public void changeCommitMessageWithNoChangeIdSucceedsIfChangeIdNotRequired() throws Exception { + ConfigInput configInput = new ConfigInput(); + configInput.requireChangeId = InheritableBoolean.FALSE; + gApi.projects().name(project.get()).config(configInput); + + PushOneCommit.Result r = createChange(); + r.assertOkStatus(); + assertThat(getCommitMessage(r.getChangeId())) + .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n"); + + String newMessage = "modified commit\n"; + gApi.changes().id(r.getChangeId()).setMessage(newMessage); + RevisionApi rApi = gApi.changes().id(r.getChangeId()).current(); + assertThat(rApi.files().keySet()).containsExactly("/COMMIT_MSG", "a.txt"); + assertThat(getCommitMessage(r.getChangeId())).isEqualTo(newMessage); + } + + @Test + public void changeCommitMessageWithNoChangeIdFails() throws Exception { + PushOneCommit.Result r = createChange(); + assertThat(getCommitMessage(r.getChangeId())) + .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n"); + exception.expect(ResourceConflictException.class); + exception.expectMessage("missing Change-Id footer"); + gApi.changes().id(r.getChangeId()).setMessage("modified commit\n"); + } + + @Test + public void changeCommitMessageWithWrongChangeIdFails() throws Exception { + PushOneCommit.Result otherChange = createChange(); + PushOneCommit.Result r = createChange(); + assertThat(getCommitMessage(r.getChangeId())) + .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n"); + exception.expect(ResourceConflictException.class); + exception.expectMessage("wrong Change-Id footer"); + gApi.changes() + .id(r.getChangeId()) + .setMessage("modified commit\n\nChange-Id: " + otherChange.getChangeId() + "\n"); + } + + @Test + public void changeCommitMessageWithoutPermissionFails() throws Exception { + // Create new project with clean permissions + Project.NameKey p = createProject("addPatchSetEdit"); + TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user); + // Block default permission + block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS); + // Create change as user + PushOneCommit push = pushFactory.create(db, user.getIdent(), userTestRepo); + PushOneCommit.Result r = push.to("refs/for/master"); + r.assertOkStatus(); + // Try to change the commit message + exception.expect(AuthException.class); + exception.expectMessage("modifying commit message not permitted"); + gApi.changes().id(r.getChangeId()).setMessage("foo"); + } + + @Test + public void changeCommitMessageWithSameMessageFails() throws Exception { + PushOneCommit.Result r = createChange(); + assertThat(getCommitMessage(r.getChangeId())) + .isEqualTo("test commit\n\nChange-Id: " + r.getChangeId() + "\n"); + exception.expect(ResourceConflictException.class); + exception.expectMessage("new and existing commit message are the same"); + gApi.changes().id(r.getChangeId()).setMessage(getCommitMessage(r.getChangeId())); + } + + private String getCommitMessage(String changeId) throws RestApiException, IOException { + return gApi.changes().id(changeId).current().file("/COMMIT_MSG").content().asString(); + } + private void addComment( PushOneCommit.Result r, String message, @@ -2542,7 +3068,7 @@ private void setChangeStatus(Change.Id id, Change.Status newStatus) throws Exception { try (BatchUpdate batchUpdate = - updateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) { + batchUpdateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) { batchUpdate.addOp(id, new ChangeStatusUpdateOp(newStatus)); batchUpdate.execute(); }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIdIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIdIT.java new file mode 100644 index 0000000..e0fc358 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIdIT.java
@@ -0,0 +1,122 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.change; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.extensions.api.changes.ChangeApi; +import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.common.ChangeInput; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.reviewdb.client.Project; +import org.junit.Before; +import org.junit.Test; + +@NoHttpd +public class ChangeIdIT extends AbstractDaemonTest { + private ChangeInfo changeInfo; + + @Before + public void setup() throws Exception { + changeInfo = gApi.changes().create(new ChangeInput(project.get(), "master", "msg")).get(); + } + + @Test + public void projectChangeNumberReturnsChange() throws Exception { + ChangeApi cApi = gApi.changes().id(project.get(), changeInfo._number); + assertThat(cApi.get().changeId).isEqualTo(changeInfo.changeId); + } + + @Test + public void projectChangeNumberReturnsChangeWhenProjectContainsSlashes() throws Exception { + Project.NameKey p = createProject("foo/bar"); + ChangeInfo ci = gApi.changes().create(new ChangeInput(p.get(), "master", "msg")).get(); + ChangeApi cApi = gApi.changes().id(p.get(), ci._number); + assertThat(cApi.get().changeId).isEqualTo(ci.changeId); + } + + @Test + public void wrongProjectInProjectChangeNumberReturnsNotFound() throws Exception { + exception.expect(ResourceNotFoundException.class); + exception.expectMessage("Not found: unknown~" + changeInfo._number); + gApi.changes().id("unknown", changeInfo._number); + } + + @Test + public void wrongIdInProjectChangeNumberReturnsNotFound() throws Exception { + exception.expect(ResourceNotFoundException.class); + exception.expectMessage("Not found: " + project.get() + "~" + Integer.MAX_VALUE); + gApi.changes().id(project.get(), Integer.MAX_VALUE); + } + + @Test + public void changeNumberReturnsChange() throws Exception { + ChangeApi cApi = gApi.changes().id(changeInfo._number); + assertThat(cApi.get().changeId).isEqualTo(changeInfo.changeId); + } + + @Test + public void wrongChangeNumberReturnsNotFound() throws Exception { + exception.expect(ResourceNotFoundException.class); + gApi.changes().id(Integer.MAX_VALUE); + } + + @Test + public void tripletChangeIdReturnsChange() throws Exception { + ChangeApi cApi = gApi.changes().id(project.get(), changeInfo.branch, changeInfo.changeId); + assertThat(cApi.get().changeId).isEqualTo(changeInfo.changeId); + } + + @Test + public void wrongProjectInTripletChangeIdReturnsNotFound() throws Exception { + exception.expect(ResourceNotFoundException.class); + exception.expectMessage("Not found: unknown~" + changeInfo.branch + "~" + changeInfo.changeId); + gApi.changes().id("unknown", changeInfo.branch, changeInfo.changeId); + } + + @Test + public void wrongBranchInTripletChangeIdReturnsNotFound() throws Exception { + exception.expect(ResourceNotFoundException.class); + exception.expectMessage("Not found: " + project.get() + "~unknown~" + changeInfo.changeId); + gApi.changes().id(project.get(), "unknown", changeInfo.changeId); + } + + @Test + public void wrongIdInTripletChangeIdReturnsNotFound() throws Exception { + String unknownId = "I1234567890"; + exception.expect(ResourceNotFoundException.class); + exception.expectMessage( + "Not found: " + project.get() + "~" + changeInfo.branch + "~" + unknownId); + gApi.changes().id(project.get(), changeInfo.branch, unknownId); + } + + @Test + public void changeIdReturnsChange() throws Exception { + // ChangeId is not unique and this method needs a unique changeId to work. + // Hence we generate a new change with a different content. + ChangeInfo ci = + gApi.changes().create(new ChangeInput(project.get(), "master", "different message")).get(); + ChangeApi cApi = gApi.changes().id(ci.changeId); + assertThat(cApi.get()._number).isEqualTo(ci._number); + } + + @Test + public void wrongChangeIdReturnsNotFound() throws Exception { + exception.expect(ResourceNotFoundException.class); + gApi.changes().id("I1234567890"); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java index 2337246..54b2a47 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/config/GeneralPreferencesIT.java
@@ -41,7 +41,7 @@ assertThat(u.delete()).isEqualTo(RefUpdate.Result.FORCED); } } - accountCache.evictAll(); + accountCache.evictAllNoReindex(); } @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java index c9d5a8f..d0db01b 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupAssert.java
@@ -42,6 +42,7 @@ assertThat(toBoolean(info.options.visibleToAll)).isEqualTo(group.isVisibleToAll()); assertThat(info.description).isEqualTo(group.getDescription()); assertThat(Url.decode(info.ownerId)).isEqualTo(group.getOwnerGroupUUID().get()); + assertThat(info.createdOn).isEqualTo(group.getCreatedOn()); } public static boolean toBoolean(Boolean b) {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java index 7752f3e..56adc5c 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -20,11 +20,13 @@ import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; 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.GerritConfig; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.extensions.api.groups.GroupApi; import com.google.gerrit.extensions.api.groups.GroupInput; @@ -92,8 +94,8 @@ @Test public void addMultipleMembers() throws Exception { String g = createGroup("users"); - TestAccount u1 = accounts.create("u1", "u1@example.com", "Full Name 1"); - TestAccount u2 = accounts.create("u2", "u2@example.com", "Full Name 2"); + TestAccount u1 = accountCreator.create("u1", "u1@example.com", "Full Name 1"); + TestAccount u2 = accountCreator.create("u2", "u2@example.com", "Full Name 2"); gApi.groups().id(g).addMembers(u1.username, u2.username); assertMembers(g, u1, u2); } @@ -101,10 +103,10 @@ @Test public void addMembersWithAtSign() throws Exception { String g = createGroup("users"); - TestAccount u10 = accounts.create("u10", "u10@example.com", "Full Name 10"); + TestAccount u10 = accountCreator.create("u10", "u10@example.com", "Full Name 10"); TestAccount u11_at = - accounts.create("u11@something", "u11@example.com", "Full Name 11 With At"); - accounts.create("u11", "u11.another@example.com", "Full Name 11 Without At"); + accountCreator.create("u11@something", "u11@example.com", "Full Name 11 With At"); + accountCreator.create("u11", "u11.another@example.com", "Full Name 11 Without At"); gApi.groups().id(g).addMembers(u10.username, u11_at.username); assertMembers(g, u10, u11_at); } @@ -221,6 +223,25 @@ } @Test + public void createdOnFieldIsPopulatedForNewGroup() throws Exception { + Timestamp testStartTime = TimeUtil.nowTs(); + String newGroupName = name("newGroup"); + GroupInfo group = gApi.groups().create(newGroupName).get(); + + assertThat(group.createdOn).isAtLeast(testStartTime); + } + + @Test + public void createdOnFieldDefaultsToAuditCreationInstantBeforeSchemaUpgrade() throws Exception { + String newGroupName = name("newGroup"); + GroupInfo newGroup = gApi.groups().create(newGroupName).get(); + setCreatedOnToNull(new AccountGroup.Id(newGroup.groupId)); + + GroupInfo updatedGroup = gApi.groups().id(newGroup.id).get(); + assertThat(updatedGroup.createdOn).isEqualTo(AccountGroup.auditCreationInstantTs()); + } + + @Test public void getGroup() throws Exception { AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators")); testGetGroup(adminGroup.getGroupUUID().get(), adminGroup); @@ -529,7 +550,7 @@ // reindex is tested by {@link AbstractQueryGroupsTest#reindex} @Test public void reindexPermissions() throws Exception { - TestAccount groupOwner = accounts.user2(); + TestAccount groupOwner = accountCreator.user2(); GroupInput in = new GroupInput(); in.name = name("group"); in.members = @@ -611,7 +632,14 @@ private String createAccount(String name, String group) throws Exception { name = name(name); - accounts.create(name, group); + accountCreator.create(name, group); return name; } + + private void setCreatedOnToNull(AccountGroup.Id groupId) throws Exception { + AccountGroup group = db.accountGroups().get(groupId); + group.setCreatedOn(null); + db.accountGroups().update(ImmutableList.of(group)); + groupCache.evict(group); + } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java index e30e9b3..8688409 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/project/ProjectIT.java
@@ -25,7 +25,10 @@ import com.google.gerrit.extensions.api.projects.ConfigInput; import com.google.gerrit.extensions.api.projects.DescriptionInput; import com.google.gerrit.extensions.api.projects.ProjectInput; +import com.google.gerrit.extensions.client.InheritableBoolean; +import com.google.gerrit.extensions.client.ProjectState; import com.google.gerrit.extensions.client.SubmitType; +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.reviewdb.client.RefNames; @@ -106,7 +109,7 @@ } @Test - public void description() throws Exception { + public void descriptionChangeCausesRefUpdate() throws Exception { RevCommit initialHead = getRemoteHead(project, RefNames.REFS_CONFIG); assertThat(gApi.projects().name(project.get()).description()).isEmpty(); DescriptionInput in = new DescriptionInput(); @@ -120,7 +123,19 @@ } @Test - public void config() throws Exception { + public void descriptionIsDeletedWhenNotSpecified() throws Exception { + assertThat(gApi.projects().name(project.get()).description()).isEmpty(); + DescriptionInput in = new DescriptionInput(); + in.description = "new project description"; + gApi.projects().name(project.get()).description(in); + assertThat(gApi.projects().name(project.get()).description()).isEqualTo(in.description); + in.description = null; + gApi.projects().name(project.get()).description(in); + assertThat(gApi.projects().name(project.get()).description()).isEmpty(); + } + + @Test + public void configChangeCausesRefUpdate() throws Exception { RevCommit initialHead = getRemoteHead(project, RefNames.REFS_CONFIG); ConfigInfo info = gApi.projects().name(project.get()).config(); @@ -136,4 +151,77 @@ eventRecorder.assertRefUpdatedEvents( project.get(), RefNames.REFS_CONFIG, initialHead, updatedHead); } + + @Test + public void setConfig() throws Exception { + ConfigInput input = createTestConfigInput(); + ConfigInfo info = gApi.projects().name(project.get()).config(input); + assertThat(info.description).isEqualTo(input.description); + assertThat(info.useContributorAgreements.configuredValue) + .isEqualTo(input.useContributorAgreements); + assertThat(info.useContentMerge.configuredValue).isEqualTo(input.useContentMerge); + assertThat(info.useSignedOffBy.configuredValue).isEqualTo(input.useSignedOffBy); + assertThat(info.createNewChangeForAllNotInTarget.configuredValue) + .isEqualTo(input.createNewChangeForAllNotInTarget); + assertThat(info.requireChangeId.configuredValue).isEqualTo(input.requireChangeId); + assertThat(info.rejectImplicitMerges.configuredValue).isEqualTo(input.rejectImplicitMerges); + assertThat(info.enableReviewerByEmail.configuredValue).isEqualTo(input.enableReviewerByEmail); + assertThat(info.createNewChangeForAllNotInTarget.configuredValue) + .isEqualTo(input.createNewChangeForAllNotInTarget); + assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo(input.maxObjectSizeLimit); + assertThat(info.submitType).isEqualTo(input.submitType); + assertThat(info.state).isEqualTo(input.state); + } + + @Test + public void setPartialConfig() throws Exception { + ConfigInput input = createTestConfigInput(); + ConfigInfo info = gApi.projects().name(project.get()).config(input); + + ConfigInput partialInput = new ConfigInput(); + partialInput.useContributorAgreements = InheritableBoolean.FALSE; + info = gApi.projects().name(project.get()).config(partialInput); + + assertThat(info.description).isNull(); + assertThat(info.useContributorAgreements.configuredValue) + .isEqualTo(partialInput.useContributorAgreements); + assertThat(info.useContentMerge.configuredValue).isEqualTo(input.useContentMerge); + assertThat(info.useSignedOffBy.configuredValue).isEqualTo(input.useSignedOffBy); + assertThat(info.createNewChangeForAllNotInTarget.configuredValue) + .isEqualTo(input.createNewChangeForAllNotInTarget); + assertThat(info.requireChangeId.configuredValue).isEqualTo(input.requireChangeId); + assertThat(info.rejectImplicitMerges.configuredValue).isEqualTo(input.rejectImplicitMerges); + assertThat(info.enableReviewerByEmail.configuredValue).isEqualTo(input.enableReviewerByEmail); + assertThat(info.createNewChangeForAllNotInTarget.configuredValue) + .isEqualTo(input.createNewChangeForAllNotInTarget); + assertThat(info.maxObjectSizeLimit.configuredValue).isEqualTo(input.maxObjectSizeLimit); + assertThat(info.submitType).isEqualTo(input.submitType); + assertThat(info.state).isEqualTo(input.state); + } + + @Test + public void nonOwnerCannotSetConfig() throws Exception { + ConfigInput input = createTestConfigInput(); + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("restricted to project owner"); + gApi.projects().name(project.get()).config(input); + } + + private ConfigInput createTestConfigInput() { + ConfigInput input = new ConfigInput(); + input.description = "some description"; + input.useContributorAgreements = InheritableBoolean.TRUE; + input.useContentMerge = InheritableBoolean.TRUE; + input.useSignedOffBy = InheritableBoolean.TRUE; + input.createNewChangeForAllNotInTarget = InheritableBoolean.TRUE; + input.requireChangeId = InheritableBoolean.TRUE; + input.rejectImplicitMerges = InheritableBoolean.TRUE; + input.enableReviewerByEmail = InheritableBoolean.TRUE; + input.createNewChangeForAllNotInTarget = InheritableBoolean.TRUE; + input.maxObjectSizeLimit = "5m"; + input.submitType = SubmitType.CHERRY_PICK; + input.state = ProjectState.HIDDEN; + return input; + } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java new file mode 100644 index 0000000..8c0b927 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -0,0 +1,1215 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.extensions.common.DiffInfoSubject.assertThat; +import static com.google.gerrit.extensions.common.FileInfoSubject.assertThat; +import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableMap; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.GitUtil; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.PushOneCommit.Result; +import com.google.gerrit.common.RawInputUtil; +import com.google.gerrit.extensions.api.changes.RebaseInput; +import com.google.gerrit.extensions.common.ChangeType; +import com.google.gerrit.extensions.common.DiffInfo; +import com.google.gerrit.extensions.common.FileInfo; +import com.google.gerrit.extensions.restapi.BinaryResult; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import javax.imageio.ImageIO; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Before; +import org.junit.Test; + +public class RevisionDiffIT extends AbstractDaemonTest { + private static final String FILE_NAME = "some_file.txt"; + private static final String FILE_NAME2 = "another_file.txt"; + private static final String FILE_CONTENT = + IntStream.rangeClosed(1, 100) + .mapToObj(number -> String.format("Line %d\n", number)) + .collect(Collectors.joining()); + private static final String FILE_CONTENT2 = "1st line\n2nd line\n3rd line\n"; + + private ObjectId commit1; + private String changeId; + private String initialPatchSetId; + + @Before + public void setUp() throws Exception { + ObjectId headCommit = testRepo.getRepository().resolve("HEAD"); + commit1 = + addCommit(headCommit, ImmutableMap.of(FILE_NAME, FILE_CONTENT, FILE_NAME2, FILE_CONTENT2)); + + Result result = createEmptyChange(); + changeId = result.getChangeId(); + initialPatchSetId = result.getPatchSetId().getId(); + } + + @Test + public void diff() throws Exception { + String fileName = "a_new_file.txt"; + String fileContent = "First line\nSecond line\n"; + PushOneCommit.Result result = createChange("Add a file", fileName, fileContent); + assertDiffForNewFile(result, fileName, fileContent); + assertDiffForNewFile(result, COMMIT_MSG, result.getCommit().getFullMessage()); + } + + @Test + public void diffDeletedFile() throws Exception { + gApi.changes().id(changeId).edit().deleteFile(FILE_NAME); + gApi.changes().id(changeId).edit().publish(); + + Map<String, FileInfo> changedFiles = gApi.changes().id(changeId).current().files(); + assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, FILE_NAME); + + DiffInfo diff = gApi.changes().id(changeId).current().file(FILE_NAME).diff(); + assertThat(diff.metaA.lines).isEqualTo(100); + assertThat(diff.metaB).isNull(); + } + + @Test + public void addedFileIsIncludedInDiff() throws Exception { + String newFilePath = "a_new_file.txt"; + String newFileContent = "arbitrary content"; + gApi.changes().id(changeId).edit().modifyFile(newFilePath, RawInputUtil.create(newFileContent)); + gApi.changes().id(changeId).edit().publish(); + + Map<String, FileInfo> changedFiles = gApi.changes().id(changeId).current().files(); + assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, newFilePath); + } + + @Test + public void renamedFileIsIncludedInDiff() throws Exception { + String newFilePath = "a_new_file.txt"; + gApi.changes().id(changeId).edit().renameFile(FILE_NAME, newFilePath); + gApi.changes().id(changeId).edit().publish(); + + Map<String, FileInfo> changedFiles = gApi.changes().id(changeId).current().files(); + assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, newFilePath); + } + + @Test + public void addedBinaryFileIsIncludedInDiff() throws Exception { + String imageFileName = "an_image.png"; + byte[] imageBytes = createRgbImage(255, 0, 0); + gApi.changes().id(changeId).edit().modifyFile(imageFileName, RawInputUtil.create(imageBytes)); + gApi.changes().id(changeId).edit().publish(); + + Map<String, FileInfo> changedFiles = gApi.changes().id(changeId).current().files(); + assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, imageFileName); + } + + @Test + public void modifiedBinaryFileIsIncludedInDiff() throws Exception { + String imageFileName = "an_image.png"; + byte[] imageBytes1 = createRgbImage(255, 100, 0); + ObjectId commit2 = addCommit(commit1, imageFileName, imageBytes1); + + rebaseChangeOn(changeId, commit2); + byte[] imageBytes2 = createRgbImage(0, 100, 255); + gApi.changes().id(changeId).edit().modifyFile(imageFileName, RawInputUtil.create(imageBytes2)); + gApi.changes().id(changeId).edit().publish(); + + Map<String, FileInfo> changedFiles = gApi.changes().id(changeId).current().files(); + assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, imageFileName); + } + + @Test + public void diffOnMergeCommitChange() throws Exception { + PushOneCommit.Result r = createMergeCommitChange("refs/for/master"); + + DiffInfo diff; + + // automerge + diff = gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file("foo").diff(); + assertThat(diff.metaA.lines).isEqualTo(5); + assertThat(diff.metaB.lines).isEqualTo(1); + + diff = gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file("bar").diff(); + assertThat(diff.metaA.lines).isEqualTo(5); + assertThat(diff.metaB.lines).isEqualTo(1); + + // parent 1 + diff = gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file("bar").diff(1); + assertThat(diff.metaA.lines).isEqualTo(1); + assertThat(diff.metaB.lines).isEqualTo(1); + + // parent 2 + diff = gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file("foo").diff(2); + assertThat(diff.metaA.lines).isEqualTo(1); + assertThat(diff.metaB.lines).isEqualTo(1); + } + + @Test + public void addedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase() throws Exception { + ObjectId commit2 = addCommit(commit1, "file_added_in_another_commit.txt", "Some file content"); + + rebaseChangeOn(changeId, commit2); + addModifiedPatchSet(changeId, FILE_NAME, "Another line\n"::concat); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, FILE_NAME); + } + + @Test + public void removedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase() throws Exception { + ObjectId commit2 = addCommitRemovingFiles(commit1, FILE_NAME2); + + rebaseChangeOn(changeId, commit2); + addModifiedPatchSet(changeId, FILE_NAME, "Another line\n"::concat); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, FILE_NAME); + } + + @Test + public void renamedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase() throws Exception { + ObjectId commit2 = addCommitRenamingFile(commit1, FILE_NAME2, "a_new_file_name.txt"); + + rebaseChangeOn(changeId, commit2); + addModifiedPatchSet(changeId, FILE_NAME, "Another line\n"::concat); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, FILE_NAME); + } + + @Test + public void renamedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase_WhenEquallyModifiedInBoth() + throws Exception { + Function<String, String> contentModification = + fileContent -> fileContent.replace("1st line\n", "First line\n"); + addModifiedPatchSet(changeId, FILE_NAME2, contentModification); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + + // Revert the modification to be able to rebase. + addModifiedPatchSet( + changeId, FILE_NAME2, fileContent -> fileContent.replace("First line\n", "1st line\n")); + + String renamedFileName = "renamed_file.txt"; + ObjectId commit2 = addCommitRenamingFile(commit1, FILE_NAME2, renamedFileName); + rebaseChangeOn(changeId, commit2); + addModifiedPatchSet(changeId, renamedFileName, contentModification); + addModifiedPatchSet(changeId, FILE_NAME, "Another line\n"::concat); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(previousPatchSetId); + assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, FILE_NAME); + } + + @Test + public void filesNotTouchedByPatchSetsAndContainingOnlyRebaseHunksAreIgnored() throws Exception { + String newFileContent = FILE_CONTENT.replace("Line 10\n", "Line ten\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + ObjectId commit3 = addCommitRenamingFile(commit2, FILE_NAME2, "a_new_file_name.txt"); + + rebaseChangeOn(changeId, commit3); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG); + } + + @Test + public void filesTouchedByPatchSetsAndContainingOnlyRebaseHunksAreIgnored() throws Exception { + addModifiedPatchSet( + changeId, FILE_NAME, fileContent -> fileContent.replace("Line 50\n", "Line fifty\n")); + addModifiedPatchSet( + changeId, FILE_NAME2, fileContent -> fileContent.replace("1st line\n", "First line\n")); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + // Revert the modification to allow rebasing. + addModifiedPatchSet( + changeId, FILE_NAME2, fileContent -> fileContent.replace("First line\n", "1st line\n")); + + String newFileContent = FILE_CONTENT.replace("Line 10\n", "Line ten\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + String newFilePath = "a_new_file_name.txt"; + ObjectId commit3 = addCommitRenamingFile(commit2, FILE_NAME2, newFilePath); + + rebaseChangeOn(changeId, commit3); + // Apply the modification again to bring the file into the same state as for the previous + // patch set. + addModifiedPatchSet( + changeId, newFilePath, fileContent -> fileContent.replace("1st line\n", "First line\n")); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(previousPatchSetId); + assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG); + } + + @Test + public void rebaseHunksAtStartOfFileAreIdentified() throws Exception { + String newFileContent = + FILE_CONTENT.replace("Line 1\n", "Line one\n").replace("Line 5\n", "Line five\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + Function<String, String> contentModification = + fileContent -> fileContent.replace("Line 50\n", "Line fifty\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId); + assertThat(diffInfo).content().element(0).linesOfA().containsExactly("Line 1"); + assertThat(diffInfo).content().element(0).linesOfB().containsExactly("Line one"); + assertThat(diffInfo).content().element(0).isDueToRebase(); + assertThat(diffInfo).content().element(1).commonLines().hasSize(3); + assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 5"); + assertThat(diffInfo).content().element(2).linesOfB().containsExactly("Line five"); + assertThat(diffInfo).content().element(2).isDueToRebase(); + assertThat(diffInfo).content().element(3).commonLines().hasSize(44); + assertThat(diffInfo).content().element(4).linesOfA().containsExactly("Line 50"); + assertThat(diffInfo).content().element(4).linesOfB().containsExactly("Line fifty"); + assertThat(diffInfo).content().element(4).isNotDueToRebase(); + assertThat(diffInfo).content().element(5).commonLines().hasSize(50); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1); + } + + @Test + public void rebaseHunksAtEndOfFileAreIdentified() throws Exception { + String newFileContent = + FILE_CONTENT + .replace("Line 60\n", "Line sixty\n") + .replace("Line 100\n", "Line one hundred\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + Function<String, String> contentModification = + fileContent -> fileContent.replace("Line 50\n", "Line fifty\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId); + assertThat(diffInfo).content().element(0).commonLines().hasSize(49); + assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 50"); + assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line fifty"); + assertThat(diffInfo).content().element(1).isNotDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(9); + assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 60"); + assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line sixty"); + assertThat(diffInfo).content().element(3).isDueToRebase(); + assertThat(diffInfo).content().element(4).commonLines().hasSize(39); + assertThat(diffInfo).content().element(5).linesOfA().containsExactly("Line 100"); + assertThat(diffInfo).content().element(5).linesOfB().containsExactly("Line one hundred"); + assertThat(diffInfo).content().element(5).isDueToRebase(); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1); + } + + @Test + public void rebaseHunksInBetweenRegularHunksAreIdentified() throws Exception { + String newFileContent = + FILE_CONTENT.replace("Line 40\n", "Line forty\n").replace("Line 45\n", "Line forty five\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + Function<String, String> contentModification = + fileContent -> + fileContent + .replace("Line 1\n", "Line one\n") + .replace("Line 100\n", "Line one hundred\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId); + assertThat(diffInfo).content().element(0).linesOfA().containsExactly("Line 1"); + assertThat(diffInfo).content().element(0).linesOfB().containsExactly("Line one"); + assertThat(diffInfo).content().element(0).isNotDueToRebase(); + assertThat(diffInfo).content().element(1).commonLines().hasSize(38); + assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 40"); + assertThat(diffInfo).content().element(2).linesOfB().containsExactly("Line forty"); + assertThat(diffInfo).content().element(2).isDueToRebase(); + assertThat(diffInfo).content().element(3).commonLines().hasSize(4); + assertThat(diffInfo).content().element(4).linesOfA().containsExactly("Line 45"); + assertThat(diffInfo).content().element(4).linesOfB().containsExactly("Line forty five"); + assertThat(diffInfo).content().element(4).isDueToRebase(); + assertThat(diffInfo).content().element(5).commonLines().hasSize(54); + assertThat(diffInfo).content().element(6).linesOfA().containsExactly("Line 100"); + assertThat(diffInfo).content().element(6).linesOfB().containsExactly("Line one hundred"); + assertThat(diffInfo).content().element(6).isNotDueToRebase(); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2); + } + + @Test + public void rebaseHunkIsIdentifiedWhenMovedDownInPreviousPatchSet() throws Exception { + // Move the code down by introducing additional lines (pure insert + enlarging replacement) in + // the previous patch set. + Function<String, String> contentModification1 = + fileContent -> + "Line zero\n" + fileContent.replace("Line 10\n", "Line ten\nLine ten and a half\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification1); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + + String newFileContent = FILE_CONTENT.replace("Line 40\n", "Line forty\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + Function<String, String> contentModification2 = + fileContent -> fileContent.replace("Line 100\n", "Line one hundred\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification2); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(previousPatchSetId); + assertThat(diffInfo).content().element(0).commonLines().hasSize(41); + assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 40"); + assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line forty"); + assertThat(diffInfo).content().element(1).isDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(59); + assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 100"); + assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line one hundred"); + assertThat(diffInfo).content().element(3).isNotDueToRebase(); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(previousPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1); + } + + @Test + public void rebaseHunkIsIdentifiedWhenMovedDownInLatestPatchSet() throws Exception { + String newFileContent = FILE_CONTENT.replace("Line 40\n", "Line forty\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + // Move the code down by introducing additional lines (pure insert + enlarging replacement) in + // the latest patch set. + Function<String, String> contentModification = + fileContent -> + "Line zero\n" + fileContent.replace("Line 10\n", "Line ten\nLine ten and a half\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId); + assertThat(diffInfo).content().element(0).linesOfA().isNull(); + assertThat(diffInfo).content().element(0).linesOfB().containsExactly("Line zero"); + assertThat(diffInfo).content().element(0).isNotDueToRebase(); + assertThat(diffInfo).content().element(1).commonLines().hasSize(9); + assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 10"); + assertThat(diffInfo) + .content() + .element(2) + .linesOfB() + .containsExactly("Line ten", "Line ten and a half"); + assertThat(diffInfo).content().element(2).isNotDueToRebase(); + assertThat(diffInfo).content().element(3).commonLines().hasSize(29); + assertThat(diffInfo).content().element(4).linesOfA().containsExactly("Line 40"); + assertThat(diffInfo).content().element(4).linesOfB().containsExactly("Line forty"); + assertThat(diffInfo).content().element(4).isDueToRebase(); + assertThat(diffInfo).content().element(5).commonLines().hasSize(60); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(3); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1); + } + + @Test + public void rebaseHunkIsIdentifiedWhenMovedUpInPreviousPatchSet() throws Exception { + // Move the code up by removing lines (pure deletion + shrinking replacement) in the previous + // patch set. + Function<String, String> contentModification1 = + fileContent -> + fileContent.replace("Line 1\n", "").replace("Line 10\nLine 11\n", "Line ten\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification1); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + + String newFileContent = FILE_CONTENT.replace("Line 40\n", "Line forty\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + Function<String, String> contentModification2 = + fileContent -> fileContent.replace("Line 100\n", "Line one hundred\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification2); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(previousPatchSetId); + assertThat(diffInfo).content().element(0).commonLines().hasSize(37); + assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 40"); + assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line forty"); + assertThat(diffInfo).content().element(1).isDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(59); + assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 100"); + assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line one hundred"); + assertThat(diffInfo).content().element(3).isNotDueToRebase(); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(previousPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1); + } + + @Test + public void rebaseHunkIsIdentifiedWhenMovedUpInLatestPatchSet() throws Exception { + String newFileContent = FILE_CONTENT.replace("Line 40\n", "Line forty\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + // Move the code up by removing lines (pure deletion + shrinking replacement) in the latest + // patch set. + Function<String, String> contentModification = + fileContent -> + fileContent.replace("Line 1\n", "").replace("Line 10\nLine 11\n", "Line ten\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId); + assertThat(diffInfo).content().element(0).linesOfA().containsExactly("Line 1"); + assertThat(diffInfo).content().element(0).linesOfB().isNull(); + assertThat(diffInfo).content().element(0).isNotDueToRebase(); + assertThat(diffInfo).content().element(1).commonLines().hasSize(8); + assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 10", "Line 11"); + assertThat(diffInfo).content().element(2).linesOfB().containsExactly("Line ten"); + assertThat(diffInfo).content().element(2).isNotDueToRebase(); + assertThat(diffInfo).content().element(3).commonLines().hasSize(28); + assertThat(diffInfo).content().element(4).linesOfA().containsExactly("Line 40"); + assertThat(diffInfo).content().element(4).linesOfB().containsExactly("Line forty"); + assertThat(diffInfo).content().element(4).isDueToRebase(); + assertThat(diffInfo).content().element(5).commonLines().hasSize(60); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(3); + } + + @Test + public void modifiedRebaseHunkWithSameRegionConsideredAsRegularHunk() throws Exception { + String newFileContent = FILE_CONTENT.replace("Line 40\n", "Line forty\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + Function<String, String> contentModification = + fileContent -> fileContent.replace("Line forty\n", "Line modified after rebase\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId); + assertThat(diffInfo).content().element(0).commonLines().hasSize(39); + assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 40"); + assertThat(diffInfo) + .content() + .element(1) + .linesOfB() + .containsExactly("Line modified after rebase"); + assertThat(diffInfo).content().element(1).isNotDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(60); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1); + } + + @Test + public void rebaseHunkOverlappingAtBeginningConsideredAsRegularHunk() throws Exception { + String newFileContent = + FILE_CONTENT.replace("Line 40\nLine 41\n", "Line forty\nLine forty one\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + Function<String, String> contentModification = + fileContent -> + fileContent + .replace("Line 39\n", "Line thirty nine\n") + .replace("Line forty one\n", "Line 41\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId); + assertThat(diffInfo).content().element(0).commonLines().hasSize(38); + assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 39", "Line 40"); + assertThat(diffInfo) + .content() + .element(1) + .linesOfB() + .containsExactly("Line thirty nine", "Line forty"); + assertThat(diffInfo).content().element(1).isNotDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(60); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2); + } + + @Test + public void rebaseHunkOverlappingAtEndConsideredAsRegularHunk() throws Exception { + String newFileContent = + FILE_CONTENT.replace("Line 40\nLine 41\n", "Line forty\nLine forty one\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + Function<String, String> contentModification = + fileContent -> + fileContent + .replace("Line forty\n", "Line 40\n") + .replace("Line 42\n", "Line forty two\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId); + assertThat(diffInfo).content().element(0).commonLines().hasSize(40); + assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 41", "Line 42"); + assertThat(diffInfo) + .content() + .element(1) + .linesOfB() + .containsExactly("Line forty one", "Line forty two"); + assertThat(diffInfo).content().element(1).isNotDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(58); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2); + } + + @Test + public void rebaseHunkModifiedInsideConsideredAsRegularHunk() throws Exception { + String newFileContent = + FILE_CONTENT.replace( + "Line 39\nLine 40\nLine 41\n", "Line thirty nine\nLine forty\nLine forty one\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + Function<String, String> contentModification = + fileContent -> fileContent.replace("Line forty\n", "A different line forty\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId); + assertThat(diffInfo).content().element(0).commonLines().hasSize(38); + assertThat(diffInfo) + .content() + .element(1) + .linesOfA() + .containsExactly("Line 39", "Line 40", "Line 41"); + assertThat(diffInfo) + .content() + .element(1) + .linesOfB() + .containsExactly("Line thirty nine", "A different line forty", "Line forty one"); + assertThat(diffInfo).content().element(1).isNotDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(59); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(3); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(3); + } + + @Test + public void rebaseHunkAfterLineNumberChangingOverlappingHunksIsIdentified() throws Exception { + String newFileContent = + FILE_CONTENT + .replace("Line 40\nLine 41\n", "Line forty\nLine forty one\n") + .replace("Line 60\n", "Line sixty\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + Function<String, String> contentModification = + fileContent -> + fileContent + .replace("Line forty\n", "Line 40\n") + .replace("Line 42\n", "Line forty two\nLine forty two and a half\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId); + assertThat(diffInfo).content().element(0).commonLines().hasSize(40); + assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 41", "Line 42"); + assertThat(diffInfo) + .content() + .element(1) + .linesOfB() + .containsExactly("Line forty one", "Line forty two", "Line forty two and a half"); + assertThat(diffInfo).content().element(1).isNotDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(17); + assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 60"); + assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line sixty"); + assertThat(diffInfo).content().element(3).isDueToRebase(); + assertThat(diffInfo).content().element(4).commonLines().hasSize(40); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(3); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2); + } + + @Test + public void rebaseHunksOneLineApartFromRegularHunkAreIdentified() throws Exception { + String newFileContent = + FILE_CONTENT.replace("Line 1\n", "Line one\n").replace("Line 5\n", "Line five\n"); + ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent); + + rebaseChangeOn(changeId, commit2); + Function<String, String> contentModification = + fileContent -> fileContent.replace("Line 3\n", "Line three\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId); + assertThat(diffInfo).content().element(0).linesOfA().containsExactly("Line 1"); + assertThat(diffInfo).content().element(0).linesOfB().containsExactly("Line one"); + assertThat(diffInfo).content().element(0).isDueToRebase(); + assertThat(diffInfo).content().element(1).commonLines().hasSize(1); + assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 3"); + assertThat(diffInfo).content().element(2).linesOfB().containsExactly("Line three"); + assertThat(diffInfo).content().element(2).isNotDueToRebase(); + assertThat(diffInfo).content().element(3).commonLines().hasSize(1); + assertThat(diffInfo).content().element(4).linesOfA().containsExactly("Line 5"); + assertThat(diffInfo).content().element(4).linesOfB().containsExactly("Line five"); + assertThat(diffInfo).content().element(4).isDueToRebase(); + assertThat(diffInfo).content().element(5).commonLines().hasSize(95); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1); + } + + @Test + public void multipleRebaseEditsMixedWithRegularEditsCanBeIdentified() throws Exception { + addModifiedPatchSet( + changeId, + FILE_NAME, + fileContent -> fileContent.replace("Line 7\n", "Line seven\n").replace("Line 24\n", "")); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + + ObjectId commit2 = + addCommit( + commit1, + FILE_NAME, + FILE_CONTENT + .replace("Line 2\n", "Line two\n") + .replace("Line 18\nLine 19\n", "Line eighteen\nLine nineteen\n") + .replace("Line 50\n", "Line fifty\n")); + + rebaseChangeOn(changeId, commit2); + addModifiedPatchSet( + changeId, + FILE_NAME, + fileContent -> + fileContent + .replace("Line seven\n", "Line 7\n") + .replace("Line 9\n", "Line nine\n") + .replace("Line 60\n", "Line sixty\n")); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(previousPatchSetId); + assertThat(diffInfo).content().element(0).commonLines().hasSize(1); + assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 2"); + assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line two"); + assertThat(diffInfo).content().element(1).isDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(4); + assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line seven"); + assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line 7"); + assertThat(diffInfo).content().element(3).isNotDueToRebase(); + assertThat(diffInfo).content().element(4).commonLines().hasSize(1); + assertThat(diffInfo).content().element(5).linesOfA().containsExactly("Line 9"); + assertThat(diffInfo).content().element(5).linesOfB().containsExactly("Line nine"); + assertThat(diffInfo).content().element(5).isNotDueToRebase(); + assertThat(diffInfo).content().element(6).commonLines().hasSize(8); + assertThat(diffInfo).content().element(7).linesOfA().containsExactly("Line 18", "Line 19"); + assertThat(diffInfo) + .content() + .element(7) + .linesOfB() + .containsExactly("Line eighteen", "Line nineteen"); + assertThat(diffInfo).content().element(7).isDueToRebase(); + assertThat(diffInfo).content().element(8).commonLines().hasSize(29); + assertThat(diffInfo).content().element(9).linesOfA().containsExactly("Line 50"); + assertThat(diffInfo).content().element(9).linesOfB().containsExactly("Line fifty"); + assertThat(diffInfo).content().element(9).isDueToRebase(); + assertThat(diffInfo).content().element(10).commonLines().hasSize(9); + assertThat(diffInfo).content().element(11).linesOfA().containsExactly("Line 60"); + assertThat(diffInfo).content().element(11).linesOfB().containsExactly("Line sixty"); + assertThat(diffInfo).content().element(11).isNotDueToRebase(); + assertThat(diffInfo).content().element(12).commonLines().hasSize(40); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(previousPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(3); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(3); + } + + @Test + public void multipleRebaseEditsMixedWithRegularEditsCanBeIdentified_WithIntraline() + throws Exception { + addModifiedPatchSet( + changeId, + FILE_NAME, + fileContent -> fileContent.replace("Line 7\n", "Line seven\n").replace("Line 24\n", "")); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + + ObjectId commit2 = + addCommit( + commit1, + FILE_NAME, + FILE_CONTENT + .replace("Line 2\n", "Line two\n") + .replace("Line 18\nLine 19\n", "Line eighteen\nLine nineteen\n") + .replace("Line 50\n", "Line fifty\n")); + + rebaseChangeOn(changeId, commit2); + addModifiedPatchSet( + changeId, + FILE_NAME, + fileContent -> + fileContent + .replace("Line seven\n", "Line 7\n") + .replace("Line 9\n", "Line nine\n") + .replace("Line 60\n", "Line sixty\n")); + + DiffInfo diffInfo = + gApi.changes() + .id(changeId) + .current() + .file(FILE_NAME) + .diffRequest() + .withBase(previousPatchSetId) + .withIntraline(true) + .get(); + assertThat(diffInfo).content().element(0).commonLines().hasSize(1); + assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 2"); + assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line two"); + assertThat(diffInfo).content().element(1).isDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(4); + assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line seven"); + assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line 7"); + assertThat(diffInfo).content().element(3).isNotDueToRebase(); + assertThat(diffInfo).content().element(4).commonLines().hasSize(1); + assertThat(diffInfo).content().element(5).linesOfA().containsExactly("Line 9"); + assertThat(diffInfo).content().element(5).linesOfB().containsExactly("Line nine"); + assertThat(diffInfo).content().element(5).isNotDueToRebase(); + assertThat(diffInfo).content().element(6).commonLines().hasSize(8); + assertThat(diffInfo).content().element(7).linesOfA().containsExactly("Line 18", "Line 19"); + assertThat(diffInfo) + .content() + .element(7) + .linesOfB() + .containsExactly("Line eighteen", "Line nineteen"); + assertThat(diffInfo).content().element(7).isDueToRebase(); + assertThat(diffInfo).content().element(8).commonLines().hasSize(29); + assertThat(diffInfo).content().element(9).linesOfA().containsExactly("Line 50"); + assertThat(diffInfo).content().element(9).linesOfB().containsExactly("Line fifty"); + assertThat(diffInfo).content().element(9).isDueToRebase(); + assertThat(diffInfo).content().element(10).commonLines().hasSize(9); + assertThat(diffInfo).content().element(11).linesOfA().containsExactly("Line 60"); + assertThat(diffInfo).content().element(11).linesOfB().containsExactly("Line sixty"); + assertThat(diffInfo).content().element(11).isNotDueToRebase(); + assertThat(diffInfo).content().element(12).commonLines().hasSize(40); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(previousPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(3); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(3); + } + + @Test + public void deletedFileDuringRebaseConsideredAsRegularHunkWhenModifiedInDiff() throws Exception { + // Modify the file and revert the modifications to allow rebasing. + addModifiedPatchSet( + changeId, FILE_NAME, fileContent -> fileContent.replace("Line 50\n", "Line fifty\n")); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + addModifiedPatchSet( + changeId, FILE_NAME, fileContent -> fileContent.replace("Line fifty\n", "Line 50\n")); + + ObjectId commit2 = addCommitRemovingFiles(commit1, FILE_NAME); + + rebaseChangeOn(changeId, commit2); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(previousPatchSetId); + assertThat(diffInfo).changeType().isEqualTo(ChangeType.DELETED); + assertThat(diffInfo).content().element(0).linesOfA().hasSize(100); + assertThat(diffInfo).content().element(0).linesOfB().isNull(); + assertThat(diffInfo).content().element(0).isNotDueToRebase(); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(previousPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isNull(); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(100); + } + + @Test + public void addedFileDuringRebaseConsideredAsRegularHunkWhenModifiedInDiff() throws Exception { + String newFilePath = "a_new_file.txt"; + ObjectId commit2 = addCommit(commit1, newFilePath, "1st line\n2nd line\n3rd line\n"); + + rebaseChangeOn(changeId, commit2); + addModifiedPatchSet( + changeId, newFilePath, fileContent -> fileContent.replace("1st line\n", "First line\n")); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(newFilePath).diff(initialPatchSetId); + assertThat(diffInfo).changeType().isEqualTo(ChangeType.ADDED); + assertThat(diffInfo).content().element(0).linesOfA().isNull(); + assertThat(diffInfo).content().element(0).linesOfB().hasSize(3); + assertThat(diffInfo).content().element(0).isNotDueToRebase(); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(newFilePath)).linesInserted().isEqualTo(3); + assertThat(changedFiles.get(newFilePath)).linesDeleted().isNull(); + } + + @Test + public void rebaseHunkInRenamedFileIsIdentified_WhenFileIsRenamedDuringRebase() throws Exception { + String renamedFilePath = "renamed_some_file.txt"; + ObjectId commit2 = + addCommit(commit1, FILE_NAME, FILE_CONTENT.replace("Line 1\n", "Line one\n")); + ObjectId commit3 = addCommitRenamingFile(commit2, FILE_NAME, renamedFilePath); + + rebaseChangeOn(changeId, commit3); + Function<String, String> contentModification = + fileContent -> fileContent.replace("Line 50\n", "Line fifty\n"); + addModifiedPatchSet(changeId, renamedFilePath, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(renamedFilePath).diff(initialPatchSetId); + assertThat(diffInfo).content().element(0).linesOfA().containsExactly("Line 1"); + assertThat(diffInfo).content().element(0).linesOfB().containsExactly("Line one"); + assertThat(diffInfo).content().element(0).isDueToRebase(); + assertThat(diffInfo).content().element(1).commonLines().hasSize(48); + assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 50"); + assertThat(diffInfo).content().element(2).linesOfB().containsExactly("Line fifty"); + assertThat(diffInfo).content().element(2).isNotDueToRebase(); + assertThat(diffInfo).content().element(3).commonLines().hasSize(50); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(initialPatchSetId); + assertThat(changedFiles.get(renamedFilePath)).linesInserted().isEqualTo(1); + assertThat(changedFiles.get(renamedFilePath)).linesDeleted().isEqualTo(1); + } + + @Test + public void rebaseHunkInRenamedFileIsIdentified_WhenFileIsRenamedInPatchSets() throws Exception { + String renamedFilePath = "renamed_some_file.txt"; + gApi.changes().id(changeId).edit().renameFile(FILE_NAME, renamedFilePath); + gApi.changes().id(changeId).edit().publish(); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + + // Revert the renaming to be able to rebase. + gApi.changes().id(changeId).edit().renameFile(renamedFilePath, FILE_NAME); + gApi.changes().id(changeId).edit().publish(); + + ObjectId commit2 = + addCommit(commit1, FILE_NAME, FILE_CONTENT.replace("Line 5\n", "Line five\n")); + + rebaseChangeOn(changeId, commit2); + gApi.changes().id(changeId).edit().renameFile(FILE_NAME, renamedFilePath); + gApi.changes().id(changeId).edit().publish(); + Function<String, String> contentModification = + fileContent -> fileContent.replace("Line 50\n", "Line fifty\n"); + addModifiedPatchSet(changeId, renamedFilePath, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(renamedFilePath).diff(previousPatchSetId); + assertThat(diffInfo).content().element(0).commonLines().hasSize(4); + assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 5"); + assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line five"); + assertThat(diffInfo).content().element(1).isDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(44); + assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 50"); + assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line fifty"); + assertThat(diffInfo).content().element(3).isNotDueToRebase(); + assertThat(diffInfo).content().element(4).commonLines().hasSize(50); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(previousPatchSetId); + assertThat(changedFiles.get(renamedFilePath)).linesInserted().isEqualTo(1); + assertThat(changedFiles.get(renamedFilePath)).linesDeleted().isEqualTo(1); + } + + /* + * change PS B + * | + * change PS A commit4 + * | | + * commit2 commit3 + * | / + * commit1 -------- + */ + @Test + public void rebaseHunksWhenRebasingOnAnotherChangeOrPatchSetAreIdentified() throws Exception { + ObjectId commit2 = + addCommit(commit1, FILE_NAME, FILE_CONTENT.replace("Line 5\n", "Line five\n")); + rebaseChangeOn(changeId, commit2); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + + String commit3FileContent = FILE_CONTENT.replace("Line 35\n", "Line thirty five\n"); + ObjectId commit3 = addCommit(commit1, FILE_NAME, commit3FileContent); + ObjectId commit4 = + addCommit(commit3, FILE_NAME, commit3FileContent.replace("Line 60\n", "Line sixty\n")); + + rebaseChangeOn(changeId, commit4); + Function<String, String> contentModification = + fileContent -> fileContent.replace("Line 20\n", "Line twenty\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + DiffInfo diffInfo = + gApi.changes().id(changeId).current().file(FILE_NAME).diff(previousPatchSetId); + assertThat(diffInfo).content().element(0).commonLines().hasSize(4); + assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line five"); + assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 5"); + assertThat(diffInfo).content().element(1).isDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(14); + assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 20"); + assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line twenty"); + assertThat(diffInfo).content().element(3).isNotDueToRebase(); + assertThat(diffInfo).content().element(4).commonLines().hasSize(14); + assertThat(diffInfo).content().element(5).linesOfA().containsExactly("Line 35"); + assertThat(diffInfo).content().element(5).linesOfB().containsExactly("Line thirty five"); + assertThat(diffInfo).content().element(5).isDueToRebase(); + assertThat(diffInfo).content().element(6).commonLines().hasSize(24); + assertThat(diffInfo).content().element(7).linesOfA().containsExactly("Line 60"); + assertThat(diffInfo).content().element(7).linesOfB().containsExactly("Line sixty"); + assertThat(diffInfo).content().element(7).isDueToRebase(); + assertThat(diffInfo).content().element(8).commonLines().hasSize(40); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(previousPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1); + } + + /* + * change PS B + * | + * change PS A commit4 + * | | + * commit2 commit3 + * | / + * commit1 -------- + */ + @Test + public void unrelatedFileWhenRebasingOnAnotherChangeOrPatchSetIsIgnored() throws Exception { + ObjectId commit2 = + addCommit(commit1, FILE_NAME, FILE_CONTENT.replace("Line 5\n", "Line five\n")); + rebaseChangeOn(changeId, commit2); + String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision; + + ObjectId commit3 = + addCommit(commit1, FILE_NAME2, FILE_CONTENT2.replace("2nd line\n", "Second line\n")); + ObjectId commit4 = + addCommit(commit3, FILE_NAME, FILE_CONTENT.replace("Line 60\n", "Line sixty\n")); + + rebaseChangeOn(changeId, commit4); + Function<String, String> contentModification = + fileContent -> fileContent.replace("Line 20\n", "Line twenty\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).current().files(previousPatchSetId); + assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, FILE_NAME); + } + + @Test + public void rebaseHunksWhenReversingPatchSetOrderAreIdentified() throws Exception { + ObjectId commit2 = + addCommit( + commit1, + FILE_NAME, + FILE_CONTENT.replace("Line 5\n", "Line five\n").replace("Line 35\n", "")); + + rebaseChangeOn(changeId, commit2); + Function<String, String> contentModification = + fileContent -> fileContent.replace("Line 20\n", "Line twenty\n"); + addModifiedPatchSet(changeId, FILE_NAME, contentModification); + + String currentPatchSetId = gApi.changes().id(changeId).get().currentRevision; + DiffInfo diffInfo = + gApi.changes() + .id(changeId) + .revision(initialPatchSetId) + .file(FILE_NAME) + .diff(currentPatchSetId); + assertThat(diffInfo).content().element(0).commonLines().hasSize(4); + assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line five"); + assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 5"); + assertThat(diffInfo).content().element(1).isDueToRebase(); + assertThat(diffInfo).content().element(2).commonLines().hasSize(14); + assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line twenty"); + assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line 20"); + assertThat(diffInfo).content().element(3).isNotDueToRebase(); + assertThat(diffInfo).content().element(4).commonLines().hasSize(14); + assertThat(diffInfo).content().element(5).linesOfA().isNull(); + assertThat(diffInfo).content().element(5).linesOfB().containsExactly("Line 35"); + assertThat(diffInfo).content().element(5).isDueToRebase(); + assertThat(diffInfo).content().element(6).commonLines().hasSize(65); + + Map<String, FileInfo> changedFiles = + gApi.changes().id(changeId).revision(initialPatchSetId).files(currentPatchSetId); + assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1); + assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1); + } + + private void assertDiffForNewFile( + PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception { + DiffInfo diff = + gApi.changes() + .id(pushResult.getChangeId()) + .revision(pushResult.getCommit().name()) + .file(path) + .diff(); + + List<String> headers = new ArrayList<>(); + if (path.equals(COMMIT_MSG)) { + RevCommit c = pushResult.getCommit(); + + RevCommit parentCommit = c.getParents()[0]; + String parentCommitId = + testRepo.getRevWalk().getObjectReader().abbreviate(parentCommit.getId(), 8).name(); + headers.add("Parent: " + parentCommitId + " (" + parentCommit.getShortMessage() + ")"); + + SimpleDateFormat dtfmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US); + PersonIdent author = c.getAuthorIdent(); + dtfmt.setTimeZone(author.getTimeZone()); + headers.add("Author: " + author.getName() + " <" + author.getEmailAddress() + ">"); + headers.add("AuthorDate: " + dtfmt.format(author.getWhen().getTime())); + + PersonIdent committer = c.getCommitterIdent(); + dtfmt.setTimeZone(committer.getTimeZone()); + headers.add("Commit: " + committer.getName() + " <" + committer.getEmailAddress() + ">"); + headers.add("CommitDate: " + dtfmt.format(committer.getWhen().getTime())); + headers.add(""); + } + + if (!headers.isEmpty()) { + String header = Joiner.on("\n").join(headers); + expectedContentSideB = header + "\n" + expectedContentSideB; + } + + assertDiffForNewFile(diff, pushResult.getCommit(), path, expectedContentSideB); + } + + private void rebaseChangeOn(String changeId, ObjectId newParent) throws Exception { + RebaseInput rebaseInput = new RebaseInput(); + rebaseInput.base = newParent.getName(); + gApi.changes().id(changeId).current().rebase(rebaseInput); + } + + private ObjectId addCommit(ObjectId parentCommit, String filePath, String fileContent) + throws Exception { + ImmutableMap<String, String> files = ImmutableMap.of(filePath, fileContent); + return addCommit(parentCommit, files); + } + + private ObjectId addCommit(ObjectId parentCommit, ImmutableMap<String, String> files) + throws Exception { + testRepo.reset(parentCommit); + PushOneCommit push = + pushFactory.create(db, admin.getIdent(), testRepo, "Adjust files of repo", files); + PushOneCommit.Result result = push.to("refs/for/master"); + return result.getCommit(); + } + + private ObjectId addCommit(ObjectId parentCommit, String filePath, byte[] fileContent) + throws Exception { + testRepo.reset(parentCommit); + PushOneCommit.Result result = createEmptyChange(); + String changeId = result.getChangeId(); + gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(fileContent)); + gApi.changes().id(changeId).edit().publish(); + String currentRevision = gApi.changes().id(changeId).get().currentRevision; + GitUtil.fetch(testRepo, "refs/*:refs/*"); + return ObjectId.fromString(currentRevision); + } + + private ObjectId addCommitRemovingFiles(ObjectId parentCommit, String... removedFilePaths) + throws Exception { + testRepo.reset(parentCommit); + Map<String, String> files = + Arrays.stream(removedFilePaths) + .collect(Collectors.toMap(Function.identity(), path -> "Irrelevant content")); + PushOneCommit push = + pushFactory.create(db, admin.getIdent(), testRepo, "Remove files from repo", files); + PushOneCommit.Result result = push.rm("refs/for/master"); + return result.getCommit(); + } + + private ObjectId addCommitRenamingFile( + ObjectId parentCommit, String oldFilePath, String newFilePath) throws Exception { + testRepo.reset(parentCommit); + PushOneCommit.Result result = createEmptyChange(); + String changeId = result.getChangeId(); + gApi.changes().id(changeId).edit().renameFile(oldFilePath, newFilePath); + gApi.changes().id(changeId).edit().publish(); + String currentRevision = gApi.changes().id(changeId).get().currentRevision; + GitUtil.fetch(testRepo, "refs/*:refs/*"); + return ObjectId.fromString(currentRevision); + } + + private Result createEmptyChange() throws Exception { + PushOneCommit push = + pushFactory.create(db, admin.getIdent(), testRepo, "Test change", ImmutableMap.of()); + return push.to("refs/for/master"); + } + + private void addModifiedPatchSet( + String changeId, String filePath, Function<String, String> contentModification) + throws Exception { + try (BinaryResult content = gApi.changes().id(changeId).current().file(filePath).content()) { + String newContent = contentModification.apply(content.asString()); + gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(newContent)); + } + gApi.changes().id(changeId).edit().publish(); + } + + private static byte[] createRgbImage(int red, int green, int blue) throws IOException { + BufferedImage bufferedImage = new BufferedImage(10, 20, BufferedImage.TYPE_INT_RGB); + for (int x = 0; x < bufferedImage.getWidth(); x++) { + for (int y = 0; y < bufferedImage.getHeight(); y++) { + int rgb = (red << 16) + (green << 8) + blue; + bufferedImage.setRGB(x, y, rgb); + } + } + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ImageIO.write(bufferedImage, "png", byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java index 577634e..e92d255 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -15,6 +15,7 @@ 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; @@ -24,11 +25,11 @@ import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER; import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG; import static com.google.gerrit.reviewdb.client.Patch.MERGE_LIST; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.lib.Constants.HEAD; import static org.junit.Assert.fail; -import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -37,11 +38,16 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.TestProjectInput; +import com.google.gerrit.common.data.Permission; import com.google.gerrit.extensions.api.changes.ChangeApi; import com.google.gerrit.extensions.api.changes.CherryPickInput; import com.google.gerrit.extensions.api.changes.DraftApi; import com.google.gerrit.extensions.api.changes.DraftInput; +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.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput; import com.google.gerrit.extensions.api.changes.RevisionApi; @@ -54,19 +60,26 @@ import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeMessageInfo; import com.google.gerrit.extensions.common.CommentInfo; -import com.google.gerrit.extensions.common.DiffInfo; +import com.google.gerrit.extensions.common.CommitInfo; import com.google.gerrit.extensions.common.FileInfo; +import com.google.gerrit.extensions.common.GitPerson; import com.google.gerrit.extensions.common.LabelInfo; import com.google.gerrit.extensions.common.MergeableInfo; import com.google.gerrit.extensions.common.RevisionInfo; +import com.google.gerrit.extensions.common.WebLinkInfo; +import com.google.gerrit.extensions.registration.DynamicSet; +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.ETagView; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.extensions.webui.PatchSetWebLink; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.Branch.NameKey; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.server.change.GetRevisionActions; @@ -74,9 +87,9 @@ import com.google.gerrit.server.query.change.ChangeData; import com.google.inject.Inject; import java.io.ByteArrayOutputStream; +import java.sql.Timestamp; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; @@ -96,6 +109,7 @@ public class RevisionIT extends AbstractDaemonTest { @Inject private GetRevisionActions getRevisionActions; + @Inject private DynamicSet<PatchSetWebLink> patchSetLinks; @Test public void reviewTriplet() throws Exception { @@ -213,7 +227,7 @@ .stream() .filter(a -> a._accountId == user.id.get()) .findFirst(); - assertThat(crUser.isPresent()).isTrue(); + assertThat(crUser).isPresent(); assertThat(crUser.get().value).isEqualTo(0); revision(r).submit(); @@ -282,6 +296,15 @@ } @Test + public void voteNotAllowedWithoutPermission() throws Exception { + PushOneCommit.Result r = createChange(); + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("is restricted"); + gApi.changes().id(r.getChange().getId().get()).current().review(ReviewInput.approve()); + } + + @Test public void deleteDraft() throws Exception { PushOneCommit.Result r = createDraft(); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).delete(); @@ -326,6 +349,28 @@ } @Test + public void cherryPickSetChangeId() throws Exception { + PushOneCommit.Result r = pushTo("refs/for/master"); + CherryPickInput in = new CherryPickInput(); + in.destination = "foo"; + String id = "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbe3f"; + in.message = "it goes to foo branch\n\nChange-Id: " + id; + + gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput()); + ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId()); + + assertThat(orig.get().messages).hasSize(1); + ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in); + + ChangeInfo changeInfo = cherry.get(); + + // The cherry-pick honors the ChangeId specified in the input message: + RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision); + assertThat(revInfo).isNotNull(); + assertThat(revInfo.commit.message).endsWith(id + "\n"); + } + + @Test public void cherryPickwithNoTopic() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master"); CherryPickInput in = new CherryPickInput(); @@ -589,6 +634,163 @@ } @Test + public void cherryPickNotify() throws Exception { + createBranch(new NameKey(project, "branch-1")); + createBranch(new NameKey(project, "branch-2")); + createBranch(new NameKey(project, "branch-3")); + + // Creates a change for 'admin'. + PushOneCommit.Result result = createChange(); + String changeId = project.get() + "~master~" + result.getChangeId(); + + // 'user' cherry-picks the change to a new branch, the source change's author/committer('admin') + // will be added as a reviewer of the newly created change. + setApiUser(user); + CherryPickInput input = new CherryPickInput(); + input.message = "it goes to a new branch"; + + // Enable the notification. 'admin' as a reviewer should be notified. + input.destination = "branch-1"; + input.notify = NotifyHandling.ALL; + sender.clear(); + gApi.changes().id(changeId).current().cherryPick(input); + assertNotifyCc(admin); + + // Disable the notification. 'admin' as a reviewer should not be notified any more. + input.destination = "branch-2"; + input.notify = NotifyHandling.NONE; + sender.clear(); + gApi.changes().id(changeId).current().cherryPick(input); + assertThat(sender.getMessages()).hasSize(0); + + // Disable the notification. The user provided in the 'notifyDetails' should still be notified. + TestAccount userToNotify = accountCreator.user2(); + input.destination = "branch-3"; + input.notify = NotifyHandling.NONE; + input.notifyDetails = + ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email))); + sender.clear(); + gApi.changes().id(changeId).current().cherryPick(input); + assertNotifyTo(userToNotify); + } + + @Test + public void cherryPickToMergedChangeRevision() throws Exception { + createBranch(new NameKey(project, "foo")); + + PushOneCommit.Result dstChange = createChange(testRepo, "foo", SUBJECT, "b.txt", "b", "t"); + dstChange.assertOkStatus(); + + merge(dstChange); + + PushOneCommit.Result result = createChange(testRepo, "foo", SUBJECT, "b.txt", "c", "t"); + result.assertOkStatus(); + merge(result); + + PushOneCommit.Result srcChange = createChange(); + + CherryPickInput input = new CherryPickInput(); + input.destination = "foo"; + input.base = dstChange.getCommit().name(); + input.message = srcChange.getCommit().getFullMessage(); + ChangeInfo changeInfo = + gApi.changes().id(srcChange.getChangeId()).current().cherryPick(input).get(); + assertCherryPickResult(changeInfo, input, srcChange.getChangeId()); + } + + @Test + public void cherryPickToOpenChangeRevision() throws Exception { + createBranch(new NameKey(project, "foo")); + + PushOneCommit.Result dstChange = createChange(testRepo, "foo", SUBJECT, "b.txt", "b", "t"); + dstChange.assertOkStatus(); + + PushOneCommit.Result srcChange = createChange(); + + CherryPickInput input = new CherryPickInput(); + input.destination = "foo"; + input.base = dstChange.getCommit().name(); + input.message = srcChange.getCommit().getFullMessage(); + ChangeInfo changeInfo = + gApi.changes().id(srcChange.getChangeId()).current().cherryPick(input).get(); + assertCherryPickResult(changeInfo, input, srcChange.getChangeId()); + } + + @Test + public void cherryPickToNonVisibleChangeFails() throws Exception { + createBranch(new NameKey(project, "foo")); + + PushOneCommit.Result dstChange = createChange(testRepo, "foo", SUBJECT, "b.txt", "b", "t"); + dstChange.assertOkStatus(); + + gApi.changes().id(dstChange.getChangeId()).setPrivate(true, null); + + PushOneCommit.Result srcChange = createChange(); + + CherryPickInput input = new CherryPickInput(); + input.destination = "foo"; + input.base = dstChange.getCommit().name(); + input.message = srcChange.getCommit().getFullMessage(); + + setApiUser(user); + exception.expect(UnprocessableEntityException.class); + exception.expectMessage( + String.format("Commit %s does not exist on branch refs/heads/foo", input.base)); + gApi.changes().id(srcChange.getChangeId()).current().cherryPick(input).get(); + } + + @Test + public void cherryPickToAbandonedChangeFails() throws Exception { + PushOneCommit.Result change1 = createChange(); + PushOneCommit.Result change2 = createChange(); + gApi.changes().id(change2.getChangeId()).abandon(); + + CherryPickInput input = new CherryPickInput(); + input.destination = "master"; + input.base = change2.getCommit().name(); + input.message = change1.getCommit().getFullMessage(); + + exception.expect(ResourceConflictException.class); + exception.expectMessage( + String.format( + "Change %s with commit %s is %s", + change2.getChange().getId().get(), input.base, ChangeStatus.ABANDONED)); + gApi.changes().id(change1.getChangeId()).current().cherryPick(input); + } + + @Test + public void cherryPickWithInvalidBaseFails() throws Exception { + PushOneCommit.Result change1 = createChange(); + + CherryPickInput input = new CherryPickInput(); + input.destination = "master"; + input.base = "invalid-sha1"; + input.message = change1.getCommit().getFullMessage(); + + exception.expect(BadRequestException.class); + exception.expectMessage(String.format("Base %s doesn't represent a valid SHA-1", input.base)); + gApi.changes().id(change1.getChangeId()).current().cherryPick(input); + } + + @Test + public void cherryPickToCommitWithoutChangeId() throws Exception { + RevCommit commit1 = createNewCommitWithoutChangeId("refs/heads/foo", "a.txt", "content 1"); + + createNewCommitWithoutChangeId("refs/heads/foo", "a.txt", "content 2"); + + PushOneCommit.Result srcChange = createChange("subject", "b.txt", "b"); + srcChange.assertOkStatus(); + + CherryPickInput input = new CherryPickInput(); + input.destination = "foo"; + input.base = commit1.name(); + input.message = srcChange.getCommit().getFullMessage(); + ChangeInfo changeInfo = + gApi.changes().id(srcChange.getChangeId()).current().cherryPick(input).get(); + assertCherryPickResult(changeInfo, input, srcChange.getChangeId()); + } + + @Test public void canRebase() throws Exception { PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo); PushOneCommit.Result r1 = push.to("refs/for/master"); @@ -687,60 +889,38 @@ } @Test - public void diff() throws Exception { - PushOneCommit.Result r = createChange(); - assertDiffForNewFile(r, FILE_NAME, FILE_CONTENT); - assertDiffForNewFile(r, COMMIT_MSG, r.getCommit().getFullMessage()); - } - - @Test - public void diffDeletedFile() throws Exception { - pushFactory.create(db, admin.getIdent(), testRepo).to("refs/heads/master"); - PushOneCommit.Result r = - pushFactory.create(db, admin.getIdent(), testRepo).rm("refs/for/master"); - DiffInfo diff = - gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file(FILE_NAME).diff(); - assertThat(diff.metaA.lines).isEqualTo(1); - assertThat(diff.metaB).isNull(); - } - - @Test - public void diffOnMergeCommitChange() throws Exception { - PushOneCommit.Result r = createMergeCommitChange("refs/for/master"); - - DiffInfo diff; - - // automerge - diff = gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file("foo").diff(); - assertThat(diff.metaA.lines).isEqualTo(5); - assertThat(diff.metaB.lines).isEqualTo(1); - - diff = gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file("bar").diff(); - assertThat(diff.metaA.lines).isEqualTo(5); - assertThat(diff.metaB.lines).isEqualTo(1); - - // parent 1 - diff = gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file("bar").diff(1); - assertThat(diff.metaA.lines).isEqualTo(1); - assertThat(diff.metaB.lines).isEqualTo(1); - - // parent 2 - diff = gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).file("foo").diff(2); - assertThat(diff.metaA.lines).isEqualTo(1); - assertThat(diff.metaB.lines).isEqualTo(1); - } - - @Test public void description() throws Exception { PushOneCommit.Result r = createChange(); - assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description()) - .isEqualTo(""); + assertDescription(r, ""); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test"); - assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description()) - .isEqualTo("test"); + assertDescription(r, "test"); gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description(""); + assertDescription(r, ""); + } + + @Test + public void setDescriptionNotAllowedWithoutPermission() throws Exception { + PushOneCommit.Result r = createChange(); + assertDescription(r, ""); + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("edit description not permitted"); + gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test"); + } + + @Test + public void setDescriptionAllowedWithPermission() throws Exception { + PushOneCommit.Result r = createChange(); + assertDescription(r, ""); + grant(project, "refs/heads/master", Permission.OWNER, false, REGISTERED_USERS); + setApiUser(user); + gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description("test"); + assertDescription(r, "test"); + } + + private void assertDescription(PushOneCommit.Result r, String expected) throws Exception { assertThat(gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).description()) - .isEqualTo(""); + .isEqualTo(expected); } @Test @@ -768,6 +948,47 @@ assertThat(response.hasContent()).isFalse(); } + @Test + public void commit() throws Exception { + WebLinkInfo expectedWebLinkInfo = new WebLinkInfo("foo", "imageUrl", "url"); + patchSetLinks.add( + new PatchSetWebLink() { + @Override + public WebLinkInfo getPatchSetWebLink(String projectName, String commit) { + return expectedWebLinkInfo; + } + }); + + PushOneCommit.Result r = createChange(); + RevCommit c = r.getCommit(); + + CommitInfo commitInfo = gApi.changes().id(r.getChangeId()).current().commit(false); + assertThat(commitInfo.commit).isEqualTo(c.name()); + assertPersonIdent(commitInfo.author, c.getAuthorIdent()); + assertPersonIdent(commitInfo.committer, c.getCommitterIdent()); + assertThat(commitInfo.message).isEqualTo(c.getFullMessage()); + assertThat(commitInfo.subject).isEqualTo(c.getShortMessage()); + assertThat(commitInfo.parents).hasSize(1); + assertThat(Iterables.getOnlyElement(commitInfo.parents).commit) + .isEqualTo(c.getParent(0).name()); + assertThat(commitInfo.webLinks).isNull(); + + commitInfo = gApi.changes().id(r.getChangeId()).current().commit(true); + assertThat(commitInfo.webLinks).hasSize(1); + WebLinkInfo webLinkInfo = Iterables.getOnlyElement(commitInfo.webLinks); + assertThat(webLinkInfo.name).isEqualTo(expectedWebLinkInfo.name); + assertThat(webLinkInfo.imageUrl).isEqualTo(expectedWebLinkInfo.imageUrl); + assertThat(webLinkInfo.url).isEqualTo(expectedWebLinkInfo.url); + assertThat(webLinkInfo.target).isEqualTo(expectedWebLinkInfo.target); + } + + private void assertPersonIdent(GitPerson gitPerson, PersonIdent expectedIdent) { + assertThat(gitPerson.name).isEqualTo(expectedIdent.getName()); + assertThat(gitPerson.email).isEqualTo(expectedIdent.getEmailAddress()); + assertThat(gitPerson.date).isEqualTo(new Timestamp(expectedIdent.getWhen().getTime())); + assertThat(gitPerson.tz).isEqualTo(expectedIdent.getTimeZoneOffset()); + } + private void assertMergeable(String id, boolean expected) throws Exception { MergeableInfo m = gApi.changes().id(id).current().mergeable(); assertThat(m.mergeable).isEqualTo(expected); @@ -940,9 +1161,9 @@ recommend(r.getChangeId()); // check if it's blocked to delete a vote on a non-current patch set. + setApiUser(admin); exception.expect(MethodNotAllowedException.class); exception.expectMessage("Cannot access on non-current patch set"); - setApiUser(admin); gApi.changes() .id(r.getChangeId()) .revision(r.getCommit().getName()) @@ -982,6 +1203,16 @@ .containsExactlyElementsIn(ImmutableSet.of(admin.getId(), user.getId())); } + private static void assertCherryPickResult( + ChangeInfo changeInfo, CherryPickInput input, String srcChangeId) throws Exception { + assertThat(changeInfo.changeId).isEqualTo(srcChangeId); + assertThat(changeInfo.revisions.keySet()).containsExactly(changeInfo.currentRevision); + RevisionInfo revisionInfo = changeInfo.revisions.get(changeInfo.currentRevision); + assertThat(revisionInfo.commit.message).isEqualTo(input.message); + assertThat(revisionInfo.commit.parents).hasSize(1); + assertThat(revisionInfo.commit.parents.get(0).commit).isEqualTo(input.base); + } + private PushOneCommit.Result updateChange(PushOneCommit.Result r, String content) throws Exception { PushOneCommit push = @@ -1006,59 +1237,6 @@ return eTag; } - private void assertContent(PushOneCommit.Result pushResult, String path, String expectedContent) - throws Exception { - BinaryResult bin = - gApi.changes() - .id(pushResult.getChangeId()) - .revision(pushResult.getCommit().name()) - .file(path) - .content(); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - bin.writeTo(os); - String res = new String(os.toByteArray(), UTF_8); - assertThat(res).isEqualTo(expectedContent); - } - - private void assertDiffForNewFile( - PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception { - DiffInfo diff = - gApi.changes() - .id(pushResult.getChangeId()) - .revision(pushResult.getCommit().name()) - .file(path) - .diff(); - - List<String> headers = new ArrayList<>(); - if (path.equals(COMMIT_MSG)) { - RevCommit c = pushResult.getCommit(); - - RevCommit parentCommit = c.getParents()[0]; - String parentCommitId = - testRepo.getRevWalk().getObjectReader().abbreviate(parentCommit.getId(), 8).name(); - headers.add("Parent: " + parentCommitId + " (" + parentCommit.getShortMessage() + ")"); - - SimpleDateFormat dtfmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US); - PersonIdent author = c.getAuthorIdent(); - dtfmt.setTimeZone(author.getTimeZone()); - headers.add("Author: " + author.getName() + " <" + author.getEmailAddress() + ">"); - headers.add("AuthorDate: " + dtfmt.format(Long.valueOf(author.getWhen().getTime()))); - - PersonIdent committer = c.getCommitterIdent(); - dtfmt.setTimeZone(committer.getTimeZone()); - headers.add("Commit: " + committer.getName() + " <" + committer.getEmailAddress() + ">"); - headers.add("CommitDate: " + dtfmt.format(Long.valueOf(committer.getWhen().getTime()))); - headers.add(""); - } - - if (!headers.isEmpty()) { - String header = Joiner.on("\n").join(headers); - expectedContentSideB = header + "\n" + expectedContentSideB; - } - - assertDiffForNewFile(diff, pushResult.getCommit(), path, expectedContentSideB); - } - private PushOneCommit.Result createCherryPickableMerge( String parent1FileName, String parent2FileName) throws Exception { RevCommit initialCommit = getHead(repo());
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java index 11df473..c440d90 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -16,34 +16,51 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; -import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME; import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT; +import static com.google.gerrit.extensions.common.EditInfoSubject.assertThat; import static com.google.gerrit.extensions.common.RobotCommentInfoSubject.assertThatList; +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.AcceptanceTestRequestScope; +import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.PushOneCommit; 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.common.ChangeInfo; +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.common.RobotCommentInfo; import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.BinaryResult; +import com.google.gerrit.extensions.restapi.BinaryResultSubject; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import org.hamcrest.core.StringContains; +import java.util.Objects; +import java.util.Optional; import org.junit.Before; import org.junit.Test; public class RobotCommentsIT extends AbstractDaemonTest { + 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_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 String changeId; private FixReplacementInfo fixReplacementInfo; private FixSuggestionInfo fixSuggestionInfo; @@ -51,7 +68,14 @@ @Before public void setUp() throws Exception { - PushOneCommit.Result changeResult = createChange(); + PushOneCommit push = + pushFactory.create( + db, + admin.getIdent(), + testRepo, + "Provide files which can be used for fixes", + ImmutableMap.of(FILE_NAME, FILE_CONTENT, FILE_NAME2, FILE_CONTENT2)); + PushOneCommit.Result changeResult = push.to("refs/for/master"); changeId = changeResult.getChangeId(); fixReplacementInfo = createFixReplacementInfo(); @@ -61,7 +85,7 @@ @Test public void retrievingRobotCommentsBeforeAddingAnyDoesNotRaiseAnException() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); Map<String, List<RobotCommentInfo>> robotComments = gApi.changes().id(changeId).current().robotComments(); @@ -72,7 +96,7 @@ @Test public void addedRobotCommentsCanBeRetrieved() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); RobotCommentInput in = createRobotCommentInput(); addRobotComment(changeId, in); @@ -86,7 +110,7 @@ @Test public void addedRobotCommentsCanBeRetrievedByChange() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); RobotCommentInput in = createRobotCommentInput(); addRobotComment(changeId, in); @@ -109,7 +133,7 @@ @Test public void robotCommentsCanBeRetrievedAsList() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); RobotCommentInput robotCommentInput = createRobotCommentInput(); addRobotComment(changeId, robotCommentInput); @@ -124,7 +148,7 @@ @Test public void specificRobotCommentCanBeRetrieved() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); RobotCommentInput robotCommentInput = createRobotCommentInput(); addRobotComment(changeId, robotCommentInput); @@ -139,7 +163,7 @@ @Test public void robotCommentWithoutOptionalFieldsCanBeAdded() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); RobotCommentInput in = createRobotCommentInputWithMandatoryFields(); addRobotComment(changeId, in); @@ -151,8 +175,77 @@ } @Test + public void hugeRobotCommentIsRejected() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + int defaultSizeLimit = 1024 * 1024; + int sizeOfRest = 451; + fixReplacementInfo.replacement = getStringFor(defaultSizeLimit - sizeOfRest + 1); + + exception.expect(BadRequestException.class); + exception.expectMessage("limit"); + addRobotComment(changeId, withFixRobotCommentInput); + } + + @Test + public void reasonablyLargeRobotCommentIsAccepted() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + int defaultSizeLimit = 1024 * 1024; + int sizeOfRest = 451; + fixReplacementInfo.replacement = getStringFor(defaultSizeLimit - sizeOfRest); + + addRobotComment(changeId, withFixRobotCommentInput); + + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + assertThat(robotCommentInfos).hasSize(1); + } + + @Test + @GerritConfig(name = "change.robotCommentSizeLimit", value = "10k") + public void maximumAllowedSizeOfRobotCommentCanBeAdjusted() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + int sizeLimit = 10 * 1024; + fixReplacementInfo.replacement = getStringFor(sizeLimit); + + exception.expect(BadRequestException.class); + exception.expectMessage("limit"); + addRobotComment(changeId, withFixRobotCommentInput); + } + + @Test + @GerritConfig(name = "change.robotCommentSizeLimit", value = "0") + public void zeroForMaximumAllowedSizeOfRobotCommentRemovesRestriction() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + int defaultSizeLimit = 1024 * 1024; + fixReplacementInfo.replacement = getStringFor(defaultSizeLimit); + + addRobotComment(changeId, withFixRobotCommentInput); + + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + assertThat(robotCommentInfos).hasSize(1); + } + + @Test + @GerritConfig(name = "change.robotCommentSizeLimit", value = "-1") + public void negativeValueForMaximumAllowedSizeOfRobotCommentRemovesRestriction() + throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + int defaultSizeLimit = 1024 * 1024; + fixReplacementInfo.replacement = getStringFor(defaultSizeLimit); + + addRobotComment(changeId, withFixRobotCommentInput); + + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + assertThat(robotCommentInfos).hasSize(1); + } + + @Test public void addedFixSuggestionCanBeRetrieved() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); addRobotComment(changeId, withFixRobotCommentInput); List<RobotCommentInfo> robotCommentInfos = getRobotComments(); @@ -162,7 +255,7 @@ @Test public void fixIdIsGeneratedForFixSuggestion() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); addRobotComment(changeId, withFixRobotCommentInput); List<RobotCommentInfo> robotCommentInfos = getRobotComments(); @@ -177,7 +270,7 @@ @Test public void descriptionOfFixSuggestionIsAcceptedAsIs() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); addRobotComment(changeId, withFixRobotCommentInput); List<RobotCommentInfo> robotCommentInfos = getRobotComments(); @@ -191,7 +284,7 @@ @Test public void descriptionOfFixSuggestionIsMandatory() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); fixSuggestionInfo.description = null; @@ -205,7 +298,7 @@ @Test public void addedFixReplacementCanBeRetrieved() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); addRobotComment(changeId, withFixRobotCommentInput); List<RobotCommentInfo> robotCommentInfos = getRobotComments(); @@ -219,7 +312,7 @@ @Test public void fixReplacementsAreMandatory() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); fixSuggestionInfo.replacements = Collections.emptyList(); @@ -234,7 +327,7 @@ @Test public void pathOfFixReplacementIsAcceptedAsIs() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); addRobotComment(changeId, withFixRobotCommentInput); @@ -250,7 +343,7 @@ @Test public void pathOfFixReplacementIsMandatory() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); fixReplacementInfo.path = null; @@ -263,23 +356,8 @@ } @Test - public void pathOfFixReplacementMustReferToFileOfComment() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); - - fixReplacementInfo.path = "anotherFile.txt"; - - exception.expect(BadRequestException.class); - exception.expectMessage( - String.format( - "Replacements may only be specified " - + "for the file %s on which the robot comment was added", - withFixRobotCommentInput.path)); - addRobotComment(changeId, withFixRobotCommentInput); - } - - @Test public void rangeOfFixReplacementIsAcceptedAsIs() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); addRobotComment(changeId, withFixRobotCommentInput); @@ -295,7 +373,7 @@ @Test public void rangeOfFixReplacementIsMandatory() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); fixReplacementInfo.range = null; @@ -309,17 +387,121 @@ @Test public void rangeOfFixReplacementNeedsToBeValid() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); fixReplacementInfo.range = createRange(13, 9, 5, 10); exception.expect(BadRequestException.class); - exception.expectMessage(new StringContains("Range (13:9 - 5:10)")); + exception.expectMessage("Range (13:9 - 5:10)"); addRobotComment(changeId, withFixRobotCommentInput); } @Test + public void rangesOfFixReplacementsOfSameFixSuggestionForSameFileMayNotOverlap() + throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + 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); + withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo); + + exception.expect(BadRequestException.class); + exception.expectMessage("overlap"); + addRobotComment(changeId, withFixRobotCommentInput); + } + + @Test + public void rangesOfFixReplacementsOfSameFixSuggestionForDifferentFileMayOverlap() + throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + 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); + withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo); + + addRobotComment(changeId, withFixRobotCommentInput); + + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + assertThatList(robotCommentInfos).onlyElement().fixSuggestions().hasSize(1); + } + + @Test + public void rangesOfFixReplacementsOfDifferentFixSuggestionsForSameFileMayOverlap() + throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + 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); + + withFixRobotCommentInput.fixSuggestions = + ImmutableList.of(fixSuggestionInfo1, fixSuggestionInfo2); + + addRobotComment(changeId, withFixRobotCommentInput); + + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + assertThatList(robotCommentInfos).onlyElement().fixSuggestions().hasSize(2); + } + + @Test + public void fixReplacementsDoNotNeedToBeOrderedAccordingToRange() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + 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); + withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo); + + addRobotComment(changeId, withFixRobotCommentInput); + + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + assertThatList(robotCommentInfos).onlyElement().onlyFixSuggestion().replacements().hasSize(3); + } + + @Test public void replacementStringOfFixReplacementIsAcceptedAsIs() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); addRobotComment(changeId, withFixRobotCommentInput); @@ -335,7 +517,7 @@ @Test public void replacementStringOfFixReplacementIsMandatory() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); fixReplacementInfo.replacement = null; @@ -349,13 +531,490 @@ } @Test + public void fixWithinALineCanBeApplied() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + fixReplacementInfo.path = FILE_NAME; + fixReplacementInfo.replacement = "Modified content"; + fixReplacementInfo.range = createRange(3, 1, 3, 3); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + List<String> fixIds = getFixIds(robotCommentInfos); + 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 fixSpanningMultipleLinesCanBeApplied() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + fixReplacementInfo.path = FILE_NAME; + fixReplacementInfo.replacement = "Modified content\n5"; + fixReplacementInfo.range = createRange(3, 2, 5, 3); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + List<String> fixIds = getFixIds(robotCommentInfos); + 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 fixWithTwoCloseReplacementsOnSameFileCanBeApplied() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + 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); + withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + List<String> fixIds = getFixIds(robotCommentInfos); + 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 twoFixesOnSameFileCanBeApplied() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + 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); + + RobotCommentInput robotCommentInput1 = createRobotCommentInput(fixSuggestionInfo1); + RobotCommentInput robotCommentInput2 = createRobotCommentInput(fixSuggestionInfo2); + addRobotComment(changeId, robotCommentInput1); + addRobotComment(changeId, robotCommentInput2); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + List<String> fixIds = getFixIds(robotCommentInfos); + 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 twoConflictingFixesOnSameFileCannotBeApplied() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + 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); + + RobotCommentInput robotCommentInput1 = createRobotCommentInput(fixSuggestionInfo1); + RobotCommentInput robotCommentInput2 = createRobotCommentInput(fixSuggestionInfo2); + addRobotComment(changeId, robotCommentInput1); + addRobotComment(changeId, robotCommentInput2); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + List<String> fixIds = getFixIds(robotCommentInfos); + gApi.changes().id(changeId).current().applyFix(fixIds.get(0)); + exception.expect(ResourceConflictException.class); + exception.expectMessage("merge"); + gApi.changes().id(changeId).current().applyFix(fixIds.get(1)); + } + + @Test + public void twoFixesOfSameRobotCommentCanBeApplied() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + 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); + + withFixRobotCommentInput.fixSuggestions = + ImmutableList.of(fixSuggestionInfo1, fixSuggestionInfo2); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + List<String> fixIds = getFixIds(robotCommentInfos); + 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 fixReferringToDifferentFileThanRobotCommentCanBeApplied() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + fixReplacementInfo.path = FILE_NAME2; + fixReplacementInfo.range = createRange(2, 0, 3, 0); + fixReplacementInfo.replacement = "Modified content\n"; + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + List<String> fixIds = getFixIds(robotCommentInfos); + 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 fixInvolvingTwoFilesCanBeApplied() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + 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); + withFixRobotCommentInput.fixSuggestions = ImmutableList.of(fixSuggestionInfo); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + List<String> fixIds = getFixIds(robotCommentInfos); + 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 fixReferringToNonExistentFileCannotBeApplied() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + fixReplacementInfo.path = "a_non_existent_file.txt"; + fixReplacementInfo.range = createRange(1, 0, 2, 0); + fixReplacementInfo.replacement = "Modified content\n"; + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + List<String> fixIds = getFixIds(robotCommentInfos); + String fixId = Iterables.getOnlyElement(fixIds); + + exception.expect(ResourceNotFoundException.class); + gApi.changes().id(changeId).current().applyFix(fixId); + } + + @Test + public void fixOnPreviousPatchSetWithoutChangeEditCannotBeApplied() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + fixReplacementInfo.path = FILE_NAME; + fixReplacementInfo.replacement = "Modified content"; + fixReplacementInfo.range = createRange(3, 1, 3, 3); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + // Remember patch set and add another one. + String previousRevision = gApi.changes().id(changeId).get().currentRevision; + amendChange(changeId); + + List<String> fixIds = getFixIds(robotCommentInfos); + String fixId = Iterables.getOnlyElement(fixIds); + + exception.expect(ResourceConflictException.class); + exception.expectMessage("current"); + gApi.changes().id(changeId).revision(previousRevision).applyFix(fixId); + } + + @Test + public void fixOnPreviousPatchSetWithExistingChangeEditCanBeApplied() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + // 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); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + // Remember patch set and add another one. + String previousRevision = gApi.changes().id(changeId).get().currentRevision; + amendChange(changeId); + + List<String> fixIds = getFixIds(robotCommentInfos); + 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 fixOnCurrentPatchSetWithChangeEditOnPreviousPatchSetCannotBeApplied() + throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + // 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); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + List<String> fixIds = getFixIds(robotCommentInfos); + String fixId = Iterables.getOnlyElement(fixIds); + + exception.expect(ResourceConflictException.class); + exception.expectMessage("based"); + gApi.changes().id(changeId).current().applyFix(fixId); + } + + @Test + public void fixDoesNotModifyCommitMessageOfChangeEdit() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + String changeEditCommitMessage = "This is the commit message of the change edit.\n"; + gApi.changes().id(changeId).edit().modifyCommitMessage(changeEditCommitMessage); + + fixReplacementInfo.path = FILE_NAME; + fixReplacementInfo.replacement = "Modified content"; + fixReplacementInfo.range = createRange(3, 1, 3, 3); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + List<String> fixIds = getFixIds(robotCommentInfos); + String fixId = Iterables.getOnlyElement(fixIds); + + gApi.changes().id(changeId).current().applyFix(fixId); + + String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage(); + assertThat(commitMessage).isEqualTo(changeEditCommitMessage); + } + + @Test + public void applyingFixTwiceIsIdempotent() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + fixReplacementInfo.path = FILE_NAME; + fixReplacementInfo.replacement = "Modified content"; + fixReplacementInfo.range = createRange(3, 1, 3, 3); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + List<String> fixIds = getFixIds(robotCommentInfos); + 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 nonExistentFixCannotBeApplied() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + fixReplacementInfo.path = FILE_NAME; + fixReplacementInfo.replacement = "Modified content"; + fixReplacementInfo.range = createRange(3, 1, 3, 3); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + List<String> fixIds = getFixIds(robotCommentInfos); + String fixId = Iterables.getOnlyElement(fixIds); + String nonExistentFixId = fixId + "_non-existent"; + + exception.expect(ResourceNotFoundException.class); + gApi.changes().id(changeId).current().applyFix(nonExistentFixId); + } + + @Test + public void applyingFixReturnsEditInfoForCreatedChangeEdit() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + fixReplacementInfo.path = FILE_NAME; + fixReplacementInfo.replacement = "Modified content"; + fixReplacementInfo.range = createRange(3, 1, 3, 3); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + List<String> fixIds = getFixIds(robotCommentInfos); + 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 applyingFixOnTopOfChangeEditReturnsEditInfoForUpdatedChangeEdit() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + gApi.changes().id(changeId).edit().create(); + + fixReplacementInfo.path = FILE_NAME; + fixReplacementInfo.replacement = "Modified content"; + fixReplacementInfo.range = createRange(3, 1, 3, 3); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + List<String> fixIds = getFixIds(robotCommentInfos); + 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 createdChangeEditIsBasedOnCurrentPatchSet() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + String currentRevision = gApi.changes().id(changeId).get().currentRevision; + + fixReplacementInfo.path = FILE_NAME; + fixReplacementInfo.replacement = "Modified content"; + fixReplacementInfo.range = createRange(3, 1, 3, 3); + + addRobotComment(changeId, withFixRobotCommentInput); + List<RobotCommentInfo> robotCommentInfos = getRobotComments(); + + List<String> fixIds = getFixIds(robotCommentInfos); + String fixId = Iterables.getOnlyElement(fixIds); + + EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(fixId); + + assertThat(editInfo).baseRevision().isEqualTo(currentRevision); + } + + @Test public void robotCommentsNotSupportedWithoutNoteDb() throws Exception { - assume().that(notesMigration.enabled()).isFalse(); + assume().that(notesMigration.readChanges()).isFalse(); RobotCommentInput in = createRobotCommentInput(); ReviewInput reviewInput = new ReviewInput(); Map<String, List<RobotCommentInput>> robotComments = new HashMap<>(); - robotComments.put(FILE_NAME, Collections.singletonList(in)); + robotComments.put(in.path, ImmutableList.of(in)); reviewInput.robotComments = robotComments; reviewInput.message = "comment test"; @@ -366,7 +1025,7 @@ @Test public void queryChangesWithUnresolvedCommentCount() throws Exception { - assume().that(notesMigration.enabled()).isTrue(); + assume().that(notesMigration.readChanges()).isTrue(); PushOneCommit.Result r1 = createChange(); PushOneCommit.Result r2 = @@ -389,7 +1048,7 @@ } } - private RobotCommentInput createRobotCommentInputWithMandatoryFields() { + private static RobotCommentInput createRobotCommentInputWithMandatoryFields() { RobotCommentInput in = new RobotCommentInput(); in.robotId = "happyRobot"; in.robotRunId = "1"; @@ -399,7 +1058,8 @@ return in; } - private RobotCommentInput createRobotCommentInput(FixSuggestionInfo... fixSuggestionInfos) { + private static RobotCommentInput createRobotCommentInput( + FixSuggestionInfo... fixSuggestionInfos) { RobotCommentInput in = createRobotCommentInputWithMandatoryFields(); in.url = "http://www.happy-robot.com"; in.properties = new HashMap<>(); @@ -409,7 +1069,8 @@ return in; } - private FixSuggestionInfo createFixSuggestionInfo(FixReplacementInfo... fixReplacementInfos) { + 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."; @@ -417,15 +1078,15 @@ return newFixSuggestionInfo; } - private FixReplacementInfo createFixReplacementInfo() { + private static FixReplacementInfo createFixReplacementInfo() { FixReplacementInfo newFixReplacementInfo = new FixReplacementInfo(); newFixReplacementInfo.path = FILE_NAME; newFixReplacementInfo.replacement = "some replacement code"; - newFixReplacementInfo.range = createRange(3, 12, 15, 4); + newFixReplacementInfo.range = createRange(3, 9, 8, 4); return newFixReplacementInfo; } - private Comment.Range createRange( + private static Comment.Range createRange( int startLine, int startCharacter, int endLine, int endCharacter) { Comment.Range range = new Comment.Range(); range.startLine = startLine; @@ -439,8 +1100,7 @@ throws Exception { ReviewInput reviewInput = new ReviewInput(); reviewInput.robotComments = - Collections.singletonMap( - robotCommentInput.path, Collections.singletonList(robotCommentInput)); + Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput)); reviewInput.message = "robot comment test"; gApi.changes().id(targetChangeId).current().review(reviewInput); } @@ -470,4 +1130,22 @@ assertThat(c.path).isNull(); } } + + 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<RobotCommentInfo> robotComments) { + assertThatList(robotComments).isNotNull(); + return robotComments + .stream() + .map(robotCommentInfo -> robotCommentInfo.fixSuggestions) + .filter(Objects::nonNull) + .flatMap(List::stream) + .map(fixSuggestionInfo -> fixSuggestionInfo.fixId) + .collect(toList()); + } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java index 9cb0b31..4447c78 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -671,7 +671,7 @@ TestRepository<InMemoryRepository> userTestRepo = cloneProject(p, user); // Block default permission - block(Permission.ADD_PATCH_SET, REGISTERED_USERS, "refs/for/*", p); + block(p, "refs/for/*", Permission.ADD_PATCH_SET, REGISTERED_USERS); // Create change as user PushOneCommit push = pushFactory.create(db, user.getIdent(), userTestRepo);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java index 31ca9df..773ad44 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -15,21 +15,25 @@ package com.google.gerrit.acceptance.git; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.acceptance.GitUtil.assertPushOk; import static com.google.gerrit.acceptance.GitUtil.assertPushRejected; import static com.google.gerrit.acceptance.GitUtil.pushHead; +import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME; import static com.google.gerrit.common.FooterConstants.CHANGE_ID; import static com.google.gerrit.extensions.common.EditInfoSubject.assertThat; import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS; import static com.google.gerrit.server.project.Util.category; import static com.google.gerrit.server.project.Util.value; import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.toList; 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.Streams; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.PushOneCommit; @@ -37,16 +41,20 @@ import com.google.gerrit.acceptance.TestProjectInput; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.Permission; +import com.google.gerrit.extensions.api.changes.DraftInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.client.ChangeStatus; +import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.client.ProjectWatchInfo; import com.google.gerrit.extensions.client.ReviewerState; +import com.google.gerrit.extensions.client.Side; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeMessageInfo; +import com.google.gerrit.extensions.common.CommentInfo; import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.extensions.common.LabelInfo; import com.google.gerrit.extensions.common.RevisionInfo; @@ -61,12 +69,14 @@ import com.google.gerrit.testutil.TestTimeUtil; import java.util.ArrayList; import java.util.Collection; +import java.util.Comparator; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.regex.Pattern; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.ObjectId; @@ -75,6 +85,7 @@ import org.eclipse.jgit.transport.PushResult; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.RemoteRefUpdate; +import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -108,7 +119,15 @@ Util.allow( cfg, Permission.forLabel(patchSetLock.getName()), 0, 1, anonymousUsers, "refs/heads/*"); saveProjectConfig(cfg); - grant(Permission.LABEL + "Patch-Set-Lock", project, "refs/heads/*"); + grant(project, "refs/heads/*", Permission.LABEL + "Patch-Set-Lock"); + } + + @After + public void tearDown() throws Exception { + setApiUser(admin); + GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences(); + prefs.publishCommentsOnPush = false; + gApi.accounts().id(admin.id.get()).setPreferences(prefs); } protected void selectProtocol(Protocol p) throws Exception { @@ -228,7 +247,7 @@ @Test public void pushForMasterWithNotify() throws Exception { // create a user that watches the project - TestAccount user3 = accounts.create("user3", "user3@example.com", "User3"); + TestAccount user3 = accountCreator.create("user3", "user3@example.com", "User3"); List<ProjectWatchInfo> projectsToWatch = new ArrayList<>(); ProjectWatchInfo pwi = new ProjectWatchInfo(); pwi.project = project.get(); @@ -238,7 +257,7 @@ setApiUser(user3); gApi.accounts().self().setWatchedProjects(projectsToWatch); - TestAccount user2 = accounts.user2(); + TestAccount user2 = accountCreator.user2(); String pushSpec = "refs/for/master%reviewer=" + user.email + ",cc=" + user2.email; sender.clear(); @@ -304,10 +323,9 @@ String topic = "my/topic"; PushOneCommit.Result r = pushTo("refs/for/master/" + topic + "%cc=" + user.email); r.assertOkStatus(); - r.assertChange(Change.Status.NEW, topic); + r.assertChange(Change.Status.NEW, topic, ImmutableList.of(), ImmutableList.of(user)); // cc several users - TestAccount user2 = accounts.create("another-user", "another.user@example.com", "Another User"); r = pushTo( "refs/for/master/" @@ -317,9 +335,14 @@ + ",cc=" + user.email + ",cc=" - + user2.email); + + accountCreator.user2().email); r.assertOkStatus(); - r.assertChange(Change.Status.NEW, topic); + // Check that admin isn't CC'd as they own the change + r.assertChange( + Change.Status.NEW, + topic, + ImmutableList.of(), + ImmutableList.of(user, accountCreator.user2())); // cc non-existing user String nonExistingEmail = "non.existing@example.com"; @@ -345,7 +368,8 @@ r.assertChange(Change.Status.NEW, topic, user); // add several reviewers - TestAccount user2 = accounts.create("another-user", "another.user@example.com", "Another User"); + TestAccount user2 = + accountCreator.create("another-user", "another.user@example.com", "Another User"); r = pushTo( "refs/for/master/" @@ -376,6 +400,70 @@ } @Test + public void pushPrivateChange() throws Exception { + // Push a private change. + PushOneCommit.Result r = pushTo("refs/for/master%private"); + r.assertOkStatus(); + assertThat(r.getChange().change().isPrivate()).isTrue(); + + // Pushing a new patch set without --private doesn't remove the privacy flag from the change. + r = amendChange(r.getChangeId(), "refs/for/master"); + r.assertOkStatus(); + assertThat(r.getChange().change().isPrivate()).isTrue(); + + // Remove the privacy flag from the change. + r = amendChange(r.getChangeId(), "refs/for/master%remove-private"); + r.assertOkStatus(); + assertThat(r.getChange().change().isPrivate()).isFalse(); + + // Normal push: privacy flag is not added back. + r = amendChange(r.getChangeId(), "refs/for/master"); + r.assertOkStatus(); + assertThat(r.getChange().change().isPrivate()).isFalse(); + + // Make the change private again. + r = pushTo("refs/for/master%private"); + r.assertOkStatus(); + assertThat(r.getChange().change().isPrivate()).isTrue(); + + // Can't use --private and --remove-private together. + r = pushTo("refs/for/master%private,remove-private"); + r.assertErrorStatus(); + } + + @Test + public void pushWorkInProgressChange() throws Exception { + // Push a work-in-progress change. + PushOneCommit.Result r = pushTo("refs/for/master%wip"); + r.assertOkStatus(); + assertThat(r.getChange().change().isWorkInProgress()).isTrue(); + + // Pushing a new patch set without --wip doesn't remove the wip flag from the change. + r = amendChange(r.getChangeId(), "refs/for/master"); + r.assertOkStatus(); + assertThat(r.getChange().change().isWorkInProgress()).isTrue(); + + // Remove the wip flag from the change. + r = amendChange(r.getChangeId(), "refs/for/master%ready"); + r.assertOkStatus(); + assertThat(r.getChange().change().isWorkInProgress()).isFalse(); + + // Normal push: wip flag is not added back. + r = amendChange(r.getChangeId(), "refs/for/master"); + r.assertOkStatus(); + assertThat(r.getChange().change().isWorkInProgress()).isFalse(); + + // Make the change work-in-progress again. + r = pushTo("refs/for/master%wip"); + r.assertOkStatus(); + assertThat(r.getChange().change().isWorkInProgress()).isTrue(); + + // Can't use --wip and --ready together. + r = pushTo("refs/for/master%wip,ready"); + r.assertErrorStatus(); + } + + @Test public void pushForMasterAsDraft() throws Exception { // create draft by pushing to 'refs/drafts/' PushOneCommit.Result r = pushTo("refs/drafts/master"); @@ -849,7 +937,7 @@ @Test public void pushSameCommitTwiceUsingMagicBranchBaseOption() throws Exception { - grant(Permission.PUSH, project, "refs/heads/master"); + grant(project, "refs/heads/master", Permission.PUSH); PushOneCommit.Result rBase = pushTo("refs/heads/master"); rBase.assertOkStatus(); @@ -864,8 +952,10 @@ PushResult pr = GitUtil.pushHead(testRepo, "refs/for/foo%base=" + rBase.getCommit().name(), false, false); - assertThat(pr.getMessages()).contains("changes: new: 1, refs: 1, done"); + // BatchUpdate implementations differ in how they hook into progress monitors. We mostly just + // care that there is a new change. + assertThat(pr.getMessages()).containsMatch("changes: new: 1,( refs: 1)? done"); assertTwoChangesWithSameRevision(r); } @@ -985,7 +1075,7 @@ private void testPushWithoutChangeId() throws Exception { RevCommit c = createCommit(testRepo, "Message without Change-Id"); - assertThat(GitUtil.getChangeId(testRepo, c).isPresent()).isFalse(); + assertThat(GitUtil.getChangeId(testRepo, c)).isEmpty(); pushForReviewRejected(testRepo, "missing Change-Id in commit message footer"); ProjectConfig config = projectCache.checkedGet(project).getConfig(); @@ -1271,7 +1361,7 @@ @Test public void createChangeForMergedCommit() throws Exception { String master = "refs/heads/master"; - grant(Permission.PUSH, project, master, true); + grant(project, master, Permission.PUSH, true); // Update master with a direct push. RevCommit c1 = testRepo.commit().message("Non-change 1").create(); @@ -1370,7 +1460,7 @@ @Test public void mergedOptionWithExistingChangeInsertsPatchSet() throws Exception { String master = "refs/heads/master"; - grant(Permission.PUSH, project, master, true); + grant(project, master, Permission.PUSH, true); PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); @@ -1403,6 +1493,193 @@ assertThat(info.status).isEqualTo(ChangeStatus.NEW); } + @Test + public void publishCommentsOnPushPublishesDraftsOnAllRevisions() throws Exception { + PushOneCommit.Result r = createChange(); + String rev1 = r.getCommit().name(); + CommentInfo c1 = addDraft(r.getChangeId(), rev1, newDraft(FILE_NAME, 1, "comment1")); + CommentInfo c2 = addDraft(r.getChangeId(), rev1, newDraft(FILE_NAME, 1, "comment2")); + + r = amendChange(r.getChangeId()); + String rev2 = r.getCommit().name(); + CommentInfo c3 = addDraft(r.getChangeId(), rev2, newDraft(FILE_NAME, 1, "comment3")); + + assertThat(getPublishedComments(r.getChangeId())).isEmpty(); + + gApi.changes().id(r.getChangeId()).addReviewer(user.email); + sender.clear(); + amendChange(r.getChangeId(), "refs/for/master%publish-comments"); + + Collection<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"); + assertThat(getLastMessage(r.getChangeId())).isEqualTo("Uploaded patch set 3.\n\n(3 comments)"); + + List<String> messages = + sender + .getMessages() + .stream() + .map(m -> m.body()) + .sorted(Comparator.comparingInt(m -> m.contains("reexamine") ? 0 : 1)) + .collect(toList()); + assertThat(messages).hasSize(2); + + assertThat(messages.get(0)).contains("Gerrit-MessageType: newpatchset"); + assertThat(messages.get(0)).contains("I'd like you to reexamine a change"); + assertThat(messages.get(0)).doesNotContain("Uploaded patch set 3"); + + assertThat(messages.get(1)).contains("Gerrit-MessageType: comment"); + assertThat(messages.get(1)) + .containsMatch( + Pattern.compile( + // A little weird that the comment email contains this text, but it's actually + // what's in the ChangeMessage. Really we should fuse the emails into one, but until + // then, this test documents the current behavior. + "Uploaded patch set 3\\.\n" + + "\n" + + "\\(3 comments\\)\\n.*" + + "PS1, Line 1:.*" + + "comment1\\n.*" + + "PS1, Line 1:.*" + + "comment2\\n.*" + + "PS2, Line 1:.*" + + "comment3\\n", + Pattern.DOTALL)); + } + + @Test + public void publishCommentsOnPushWithMessage() throws Exception { + PushOneCommit.Result r = createChange(); + String rev = r.getCommit().name(); + addDraft(r.getChangeId(), rev, newDraft(FILE_NAME, 1, "comment1")); + + r = amendChange(r.getChangeId(), "refs/for/master%publish-comments,m=The_message"); + + Collection<CommentInfo> comments = getPublishedComments(r.getChangeId()); + assertThat(comments.stream().map(c -> c.message)).containsExactly("comment1"); + assertThat(getLastMessage(r.getChangeId())) + .isEqualTo("Uploaded patch set 2.\n\n(1 comment)\n\nThe message"); + } + + @Test + public void publishCommentsOnPushPublishesDraftsOnMultipleChanges() throws Exception { + ObjectId initialHead = testRepo.getRepository().resolve("HEAD"); + List<RevCommit> commits = createChanges(2, "refs/for/master"); + String id1 = byCommit(commits.get(0)).change().getKey().get(); + String id2 = byCommit(commits.get(1)).change().getKey().get(); + CommentInfo c1 = addDraft(id1, commits.get(0).name(), newDraft(FILE_NAME, 1, "comment1")); + CommentInfo c2 = addDraft(id2, commits.get(1).name(), newDraft(FILE_NAME, 1, "comment2")); + + assertThat(getPublishedComments(id1)).isEmpty(); + assertThat(getPublishedComments(id2)).isEmpty(); + + amendChanges(initialHead, commits, "refs/for/master%publish-comments"); + + Collection<CommentInfo> cs1 = getPublishedComments(id1); + assertThat(cs1.stream().map(c -> c.message)).containsExactly("comment1"); + assertThat(cs1.stream().map(c -> c.id)).containsExactly(c1.id); + assertThat(getLastMessage(id1)) + .isEqualTo("Uploaded patch set 2: Commit message was updated.\n\n(1 comment)"); + + Collection<CommentInfo> cs2 = getPublishedComments(id2); + assertThat(cs2.stream().map(c -> c.message)).containsExactly("comment2"); + assertThat(cs2.stream().map(c -> c.id)).containsExactly(c2.id); + assertThat(getLastMessage(id2)) + .isEqualTo("Uploaded patch set 2: Commit message was updated.\n\n(1 comment)"); + } + + @Test + public void publishCommentsOnPushOnlyPublishesDraftsOnUpdatedChanges() throws Exception { + PushOneCommit.Result r1 = createChange(); + PushOneCommit.Result r2 = createChange(); + String id1 = r1.getChangeId(); + String id2 = r2.getChangeId(); + addDraft(id1, r1.getCommit().name(), newDraft(FILE_NAME, 1, "comment1")); + CommentInfo c2 = addDraft(id2, r2.getCommit().name(), newDraft(FILE_NAME, 1, "comment2")); + + assertThat(getPublishedComments(id1)).isEmpty(); + assertThat(getPublishedComments(id2)).isEmpty(); + + r2 = amendChange(id2, "refs/for/master%publish-comments"); + + assertThat(getPublishedComments(id1)).isEmpty(); + assertThat(gApi.changes().id(id1).drafts()).hasSize(1); + + Collection<CommentInfo> cs2 = getPublishedComments(id2); + assertThat(cs2.stream().map(c -> c.message)).containsExactly("comment2"); + assertThat(cs2.stream().map(c -> c.id)).containsExactly(c2.id); + + assertThat(getLastMessage(id1)).doesNotMatch("[Cc]omment"); + assertThat(getLastMessage(id2)).isEqualTo("Uploaded patch set 2.\n\n(1 comment)"); + } + + @Test + public void publishCommentsOnPushWithPreference() throws Exception { + PushOneCommit.Result r = createChange(); + addDraft(r.getChangeId(), r.getCommit().name(), newDraft(FILE_NAME, 1, "comment1")); + r = amendChange(r.getChangeId()); + + assertThat(getPublishedComments(r.getChangeId())).isEmpty(); + + GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences(); + prefs.publishCommentsOnPush = true; + gApi.accounts().id(admin.id.get()).setPreferences(prefs); + + r = amendChange(r.getChangeId()); + assertThat(getPublishedComments(r.getChangeId()).stream().map(c -> c.message)) + .containsExactly("comment1"); + } + + @Test + public void publishCommentsOnPushOverridingPreference() throws Exception { + PushOneCommit.Result r = createChange(); + addDraft(r.getChangeId(), r.getCommit().name(), newDraft(FILE_NAME, 1, "comment1")); + + GeneralPreferencesInfo prefs = gApi.accounts().id(admin.id.get()).getPreferences(); + prefs.publishCommentsOnPush = true; + gApi.accounts().id(admin.id.get()).setPreferences(prefs); + + r = amendChange(r.getChangeId(), "refs/for/master%no-publish-comments"); + + assertThat(getPublishedComments(r.getChangeId())).isEmpty(); + } + + private DraftInput newDraft(String path, int line, String message) { + DraftInput d = new DraftInput(); + d.path = path; + d.side = Side.REVISION; + d.line = line; + d.message = message; + d.unresolved = true; + return d; + } + + private CommentInfo addDraft(String changeId, String revId, DraftInput in) throws Exception { + return gApi.changes().id(changeId).revision(revId).createDraft(in).get(); + } + + private Collection<CommentInfo> getPublishedComments(String changeId) throws Exception { + return gApi.changes() + .id(changeId) + .comments() + .values() + .stream() + .flatMap(cs -> cs.stream()) + .collect(toList()); + } + + private String getLastMessage(String changeId) throws Exception { + return Streams.findLast( + gApi.changes() + .id(changeId) + .get(EnumSet.of(ListChangesOption.MESSAGES)) + .messages + .stream() + .map(m -> m.message)) + .get(); + } + private void assertThatUserIsOnlyReviewer(ChangeInfo ci, TestAccount reviewer) { assertThat(ci.reviewers).isNotNull(); assertThat(ci.reviewers.keySet()).containsExactly(ReviewerState.REVIEWER);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java index 42463c7..d8b82e3 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractSubmoduleSubscription.java
@@ -87,8 +87,8 @@ SubmitType submitType) throws Exception { Project.NameKey project = createProject(name, parent, createEmptyCommit, submitType); - grant(Permission.PUSH, project, "refs/heads/*"); - grant(Permission.SUBMIT, project, "refs/for/refs/heads/*"); + grant(project, "refs/heads/*", Permission.PUSH); + grant(project, "refs/for/refs/heads/*", Permission.SUBMIT); return cloneProject(project); }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java index f2dc8d5..527f37d 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/DraftChangeBlockedIT.java
@@ -28,7 +28,7 @@ @Before public void setUp() throws Exception { - block(Permission.PUSH, ANONYMOUS_USERS, "refs/drafts/*"); + block("refs/drafts/*", Permission.PUSH, ANONYMOUS_USERS); } @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ForcePushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ForcePushIT.java index 6aaf12c..17fb08ab 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ForcePushIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/ForcePushIT.java
@@ -49,7 +49,7 @@ @Test public void forcePushAllowed() throws Exception { ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId(); - grant(Permission.PUSH, project, "refs/*", true); + grant(project, "refs/*", Permission.PUSH, true); PushOneCommit push1 = pushFactory.create(db, admin.getIdent(), testRepo, "change1", "a.txt", "content"); PushOneCommit.Result r1 = push1.to("refs/heads/master");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java index b900cc7..02a19c6 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -18,12 +18,12 @@ import static com.google.common.truth.Truth.assertWithMessage; import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static java.util.stream.Collectors.toList; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AcceptanceTestRequestScope; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; -import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.AccessSection; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.Permission; @@ -33,27 +33,22 @@ import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.config.AnonymousCowardName; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.git.ReceiveCommitsAdvertiseRefsHook; -import com.google.gerrit.server.git.SearchingChangeCacheImpl; -import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.notedb.ChangeNoteUtil; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; -import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.Util; import com.google.gerrit.server.query.change.ChangeData; -import com.google.gerrit.testutil.DisabledReviewDb; import com.google.gerrit.testutil.TestChanges; import com.google.inject.Inject; -import com.google.inject.Provider; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.LsRemoteCommand; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; @@ -65,16 +60,8 @@ @NoHttpd public class RefAdvertisementIT extends AbstractDaemonTest { - @Inject private ProjectControl.GenericFactory projectControlFactory; - - @Inject @Nullable private SearchingChangeCacheImpl changeCache; - - @Inject private TagCache tagCache; - - @Inject private Provider<CurrentUser> userProvider; - + @Inject private VisibleRefFilter.Factory refFilterFactory; @Inject private ChangeNoteUtil noteUtil; - @Inject @AnonymousCowardName private String anonymousCowardName; private AccountGroup.UUID admins; @@ -203,7 +190,7 @@ @Test public void uploadPackSubsetOfBranchesVisibleIncludingHead() throws Exception { allow(Permission.READ, REGISTERED_USERS, "refs/heads/master"); - deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); + deny("refs/heads/branch", Permission.READ, REGISTERED_USERS); setApiUser(user); assertUploadPackRefs( @@ -218,7 +205,7 @@ @Test public void uploadPackSubsetOfBranchesVisibleNotIncludingHead() throws Exception { - deny(Permission.READ, REGISTERED_USERS, "refs/heads/master"); + deny("refs/heads/master", Permission.READ, REGISTERED_USERS); allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); setApiUser(user); @@ -237,7 +224,6 @@ @Test public void uploadPackSubsetOfBranchesVisibleWithEdit() throws Exception { allow(Permission.READ, REGISTERED_USERS, "refs/heads/master"); - deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); Change c = notesFactory.createChecked(db, project, c1.getId()).getChange(); String changeId = c.getKey().get(); @@ -262,10 +248,43 @@ } @Test + public void uploadPackSubsetOfBranchesAndEditsVisibleWithViewPrivateChanges() throws Exception { + allow(Permission.READ, REGISTERED_USERS, "refs/heads/master"); + allow(Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS, "refs/*"); + + Change change1 = notesFactory.createChecked(db, project, c1.getId()).getChange(); + String changeId1 = change1.getKey().get(); + Change change2 = notesFactory.createChecked(db, project, c2.getId()).getChange(); + String changeId2 = change2.getKey().get(); + + // Admin's edit on change1 is visible. + setApiUser(admin); + gApi.changes().id(changeId1).edit().create(); + + // Admin's edit on change2 is not visible since user cannot see the change. + gApi.changes().id(changeId2).edit().create(); + + // User's edit is visible. + setApiUser(user); + gApi.changes().id(changeId1).edit().create(); + + assertUploadPackRefs( + "HEAD", + r1 + "1", + r1 + "meta", + r3 + "1", + r3 + "meta", + "refs/heads/master", + "refs/tags/master-tag", + "refs/users/00/1000000/edit-" + c1.getId() + "/1", + "refs/users/01/1000001/edit-" + c1.getId() + "/1"); + } + + @Test public void uploadPackSubsetOfRefsVisibleWithAccessDatabase() throws Exception { allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); try { - deny(Permission.READ, REGISTERED_USERS, "refs/heads/master"); + deny("refs/heads/master", Permission.READ, REGISTERED_USERS); allow(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); String changeId = c1.change().getKey().get(); @@ -351,7 +370,7 @@ try (Repository repo = repoManager.openRepository(project)) { assertRefs( repo, - new VisibleRefFilter(tagCache, notesFactory, null, repo, projectControl(), db, true), + refFilterFactory.create(projectCache.get(project), repo), // Can't use stored values from the index so DB must be enabled. false, "HEAD", @@ -375,12 +394,12 @@ assume().that(notesMigration.readChangeSequence()).isTrue(); try (Repository repo = repoManager.openRepository(allProjects)) { setApiUser(user); - assertRefs(repo, newFilter(db, repo, allProjects), true); + assertRefs(repo, newFilter(repo, allProjects), true); allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); try { setApiUser(user); - assertRefs(repo, newFilter(db, repo, allProjects), true, "refs/sequences/changes"); + assertRefs(repo, newFilter(repo, allProjects), true, "refs/sequences/changes"); } finally { removeGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); } @@ -405,7 +424,7 @@ @Test public void receivePackRespectsVisibilityOfOpenChanges() throws Exception { allow(Permission.READ, REGISTERED_USERS, "refs/heads/master"); - deny(Permission.READ, REGISTERED_USERS, "refs/heads/branch"); + deny("refs/heads/branch", Permission.READ, REGISTERED_USERS); setApiUser(user); assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(c3, 1)); @@ -470,6 +489,46 @@ assertThat(getReceivePackRefs().additionalHaves()).containsExactly(obj(c4, 1)); } + @Test + public void advertisedReferencesOmitPrivateChangesOfOtherUsers() throws Exception { + allow(Permission.READ, REGISTERED_USERS, "refs/heads/master"); + + TestRepository<?> userTestRepository = cloneProject(project, user); + try (Git git = userTestRepository.git()) { + LsRemoteCommand lsRemoteCommand = git.lsRemote(); + String change3RefName = c3.currentPatchSet().getRefName(); + + List<String> initialRefNames = + lsRemoteCommand.call().stream().map(Ref::getName).collect(toList()); + assertWithMessage("Precondition violated").that(initialRefNames).contains(change3RefName); + + gApi.changes().id(c3.getId().get()).setPrivate(true, null); + + List<String> refNames = lsRemoteCommand.call().stream().map(Ref::getName).collect(toList()); + assertThat(refNames).doesNotContain(change3RefName); + } + } + + @Test + public void advertisedReferencesIncludePrivateChangesWhenAllRefsMayBeRead() throws Exception { + allow(Permission.READ, REGISTERED_USERS, "refs/*"); + + TestRepository<?> userTestRepository = cloneProject(project, user); + try (Git git = userTestRepository.git()) { + LsRemoteCommand lsRemoteCommand = git.lsRemote(); + String change3RefName = c3.currentPatchSet().getRefName(); + + List<String> initialRefNames = + lsRemoteCommand.call().stream().map(Ref::getName).collect(toList()); + assertWithMessage("Precondition violated").that(initialRefNames).contains(change3RefName); + + gApi.changes().id(c3.getId().get()).setPrivate(true, null); + + List<String> refNames = lsRemoteCommand.call().stream().map(Ref::getName).collect(toList()); + assertThat(refNames).contains(change3RefName); + } + } + /** * Assert that refs seen by a non-admin user match expected. * @@ -481,17 +540,7 @@ private void assertUploadPackRefs(String... expectedWithMeta) throws Exception { try (Repository repo = repoManager.openRepository(project)) { assertRefs( - repo, - new VisibleRefFilter( - tagCache, - notesFactory, - changeCache, - repo, - projectControl(), - new DisabledReviewDb(), - true), - true, - expectedWithMeta); + repo, refFilterFactory.create(projectCache.get(project), repo), true, expectedWithMeta); } } @@ -500,7 +549,7 @@ throws Exception { List<String> expected = new ArrayList<>(expectedWithMeta.length); for (String r : expectedWithMeta) { - if (notesMigration.writeChanges() || !r.endsWith(RefNames.META_SUFFIX)) { + if (notesMigration.commitChangeWrites() || !r.endsWith(RefNames.META_SUFFIX)) { expected.add(r); } } @@ -527,20 +576,8 @@ } } - private ProjectControl projectControl() throws Exception { - return projectControlFactory.controlFor(project, userProvider.get()); - } - - private VisibleRefFilter newFilter(ReviewDb db, Repository repo, Project.NameKey project) - throws Exception { - return new VisibleRefFilter( - tagCache, - notesFactory, - null, - repo, - projectControlFactory.controlFor(project, userProvider.get()), - db, - true); + private VisibleRefFilter newFilter(Repository repo, Project.NameKey project) { + return refFilterFactory.create(projectCache.get(project), repo); } private static ObjectId obj(ChangeData cd, int psNum) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java index 2f9d501..74412ab 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmitOnPushIT.java
@@ -49,7 +49,7 @@ @Test public void submitOnPush() throws Exception { - grant(Permission.SUBMIT, project, "refs/for/refs/heads/master"); + grant(project, "refs/for/refs/heads/master", Permission.SUBMIT); PushOneCommit.Result r = pushTo("refs/for/master%submit"); r.assertOkStatus(); r.assertChange(Change.Status.MERGED, null, admin); @@ -59,9 +59,9 @@ @Test public void submitOnPushWithTag() throws Exception { - grant(Permission.SUBMIT, project, "refs/for/refs/heads/master"); - grant(Permission.CREATE, project, "refs/tags/*"); - grant(Permission.PUSH, project, "refs/tags/*"); + grant(project, "refs/for/refs/heads/master", Permission.SUBMIT); + grant(project, "refs/tags/*", Permission.CREATE); + grant(project, "refs/tags/*", Permission.PUSH); PushOneCommit.Tag tag = new PushOneCommit.Tag("v1.0"); PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo); push.setTag(tag); @@ -75,8 +75,8 @@ @Test public void submitOnPushWithAnnotatedTag() throws Exception { - grant(Permission.SUBMIT, project, "refs/for/refs/heads/master"); - grant(Permission.PUSH, project, "refs/tags/*"); + grant(project, "refs/for/refs/heads/master", Permission.SUBMIT); + grant(project, "refs/tags/*", Permission.PUSH); PushOneCommit.AnnotatedTag tag = new PushOneCommit.AnnotatedTag("v1.0", "annotation", admin.getIdent()); PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo); @@ -91,7 +91,7 @@ @Test public void submitOnPushToRefsMetaConfig() throws Exception { - grant(Permission.SUBMIT, project, "refs/for/refs/meta/config"); + grant(project, "refs/for/refs/meta/config", Permission.SUBMIT); git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call(); testRepo.reset(RefNames.REFS_CONFIG); @@ -109,7 +109,7 @@ push("refs/heads/master", "one change", "a.txt", "some content"); testRepo.reset(objectId); - grant(Permission.SUBMIT, project, "refs/for/refs/heads/master"); + grant(project, "refs/for/refs/heads/master", Permission.SUBMIT); PushOneCommit.Result r = push("refs/for/master%submit", "other change", "a.txt", "other content"); r.assertErrorStatus(); @@ -125,7 +125,7 @@ push(master, "one change", "a.txt", "some content"); testRepo.reset(objectId); - grant(Permission.SUBMIT, project, "refs/for/refs/heads/master"); + grant(project, "refs/for/refs/heads/master", Permission.SUBMIT); PushOneCommit.Result r = push("refs/for/master%submit", "other change", "b.txt", "other content"); r.assertOkStatus(); @@ -138,7 +138,7 @@ PushOneCommit.Result r = push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content"); - grant(Permission.SUBMIT, project, "refs/for/refs/heads/master"); + grant(project, "refs/for/refs/heads/master", Permission.SUBMIT); r = push( "refs/for/master%submit", @@ -154,7 +154,7 @@ @Test public void submitOnPushNotAllowed_Error() throws Exception { PushOneCommit.Result r = pushTo("refs/for/master%submit"); - r.assertErrorStatus("submit not allowed"); + r.assertErrorStatus("update by submit not permitted"); } @Test @@ -166,7 +166,7 @@ push( "refs/for/master%submit", PushOneCommit.SUBJECT, "a.txt", "other content", r.getChangeId()); - r.assertErrorStatus("submit not allowed"); + r.assertErrorStatus("update by submit not permitted"); } @Test @@ -184,7 +184,7 @@ @Test public void mergeOnPushToBranch() throws Exception { - grant(Permission.PUSH, project, "refs/heads/master"); + grant(project, "refs/heads/master", Permission.PUSH); PushOneCommit.Result r = push("refs/for/master", PushOneCommit.SUBJECT, "a.txt", "some content"); r.assertOkStatus(); @@ -206,7 +206,7 @@ @Test public void mergeOnPushToBranchWithNewPatchset() throws Exception { - grant(Permission.PUSH, project, "refs/heads/master"); + grant(project, "refs/heads/master", Permission.PUSH); PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); RevCommit c1 = r.getCommit(); @@ -241,7 +241,7 @@ @Test public void mergeOnPushToBranchWithOldPatchset() throws Exception { - grant(Permission.PUSH, project, "refs/heads/master"); + grant(project, "refs/heads/master", Permission.PUSH); PushOneCommit.Result r = pushTo("refs/for/master"); r.assertOkStatus(); RevCommit c1 = r.getCommit(); @@ -268,7 +268,7 @@ @Test public void mergeMultipleOnPushToBranchWithNewPatchset() throws Exception { - grant(Permission.PUSH, project, "refs/heads/master"); + grant(project, "refs/heads/master", Permission.PUSH); // Create 2 changes. ObjectId initialHead = getRemoteHead();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java index 6941765..d64d67f 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -16,20 +16,25 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.acceptance.GitUtil.getChangeId; +import com.google.common.collect.ImmutableList; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.change.Submit.TestSubmitInput; import com.google.gerrit.testutil.ConfigSuite; +import java.util.ArrayDeque; import java.util.Map; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.RefSpec; import org.junit.Test; @@ -740,4 +745,69 @@ expectToHaveSubmoduleState(repoA, "master", "project-b", repoB, "master"); expectToHaveSubmoduleState(repoA, "dev", "project-b", repoB, "dev"); } + + @Test + public void retrySubmitAfterTornTopicOnLockFailure() throws Exception { + assume().that(notesMigration.fuseUpdates()).isTrue(); + + TestRepository<?> superRepo = createProjectWithPush("super-project"); + TestRepository<?> sub1 = createProjectWithPush("sub1"); + TestRepository<?> sub2 = createProjectWithPush("sub2"); + + allowMatchingSubmoduleSubscription( + "sub1", "refs/heads/master", "super-project", "refs/heads/master"); + allowMatchingSubmoduleSubscription( + "sub2", "refs/heads/master", "super-project", "refs/heads/master"); + + Config config = new Config(); + prepareSubmoduleConfigEntry(config, "sub1", "master"); + prepareSubmoduleConfigEntry(config, "sub2", "master"); + pushSubmoduleConfig(superRepo, "master", config); + + ObjectId superPreviousId = pushChangeTo(superRepo, "master"); + + String topic = "same-topic"; + ObjectId sub1Id = pushChangeTo(sub1, "refs/for/master", "some message", topic); + ObjectId sub2Id = pushChangeTo(sub2, "refs/for/master", "some message", topic); + + String changeId1 = getChangeId(sub1, sub1Id).get(); + String changeId2 = getChangeId(sub2, sub2Id).get(); + approve(changeId1); + approve(changeId2); + + TestSubmitInput input = new TestSubmitInput(); + input.generateLockFailures = + new ArrayDeque<>( + ImmutableList.of( + false, // Change 1, attempt 1: success + true, // Change 2, attempt 1: lock failure + false, // Change 1, attempt 2: success + false, // Change 2, attempt 2: success + false)); // Leftover value to check total number of calls. + gApi.changes().id(changeId1).current().submit(input); + + assertThat(info(changeId1).status).isEqualTo(ChangeStatus.MERGED); + assertThat(info(changeId2).status).isEqualTo(ChangeStatus.MERGED); + + sub1.git().fetch().call(); + RevWalk rw1 = sub1.getRevWalk(); + RevCommit master1 = rw1.parseCommit(getRemoteHead(name("sub1"), "master")); + RevCommit change1Ps = parseCurrentRevision(rw1, changeId1); + assertThat(rw1.isMergedInto(change1Ps, master1)).isTrue(); + + sub2.git().fetch().call(); + RevWalk rw2 = sub2.getRevWalk(); + RevCommit master2 = rw2.parseCommit(getRemoteHead(name("sub2"), "master")); + RevCommit change2Ps = parseCurrentRevision(rw2, changeId2); + assertThat(rw2.isMergedInto(change2Ps, master2)).isTrue(); + + assertThat(input.generateLockFailures).containsExactly(false); + + expectToHaveSubmoduleState(superRepo, "master", "sub1", sub1, "master"); + expectToHaveSubmoduleState(superRepo, "master", "sub2", sub2, "master"); + + assertWithMessage("submodule subscription update should have made one commit") + .that(superRepo.getRepository().resolve("origin/master^")) + .isEqualTo(superPreviousId); + } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD index f405e19..3f45711c 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/BUILD
@@ -4,4 +4,12 @@ srcs = glob(["*IT.java"]), group = "pgm", labels = ["pgm"], + deps = [":util"], +) + +java_library( + name = "util", + testonly = 1, + srcs = ["IndexUpgradeController.java"], + deps = ["//gerrit-acceptance-tests:lib"], )
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/IndexUpgradeController.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/IndexUpgradeController.java new file mode 100644 index 0000000..9cdcb40 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/IndexUpgradeController.java
@@ -0,0 +1,121 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.pgm; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.server.index.OnlineUpgradeListener; +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +class IndexUpgradeController implements OnlineUpgradeListener { + @AutoValue + abstract static class UpgradeAttempt { + static UpgradeAttempt create(String name, int oldVersion, int newVersion) { + return new AutoValue_IndexUpgradeController_UpgradeAttempt(name, oldVersion, newVersion); + } + + abstract String name(); + + abstract int oldVersion(); + + abstract int newVersion(); + } + + private final int numExpected; + private final CountDownLatch readyToStart; + private final CountDownLatch started; + private final CountDownLatch finished; + + private final List<UpgradeAttempt> startedAttempts; + private final List<UpgradeAttempt> succeededAttempts; + private final List<UpgradeAttempt> failedAttempts; + + IndexUpgradeController(int numExpected) { + this.numExpected = numExpected; + readyToStart = new CountDownLatch(1); + started = new CountDownLatch(numExpected); + finished = new CountDownLatch(numExpected); + startedAttempts = new ArrayList<>(); + succeededAttempts = new ArrayList<>(); + failedAttempts = new ArrayList<>(); + } + + Module module() { + return new AbstractModule() { + @Override + public void configure() { + DynamicSet.bind(binder(), OnlineUpgradeListener.class) + .toInstance(IndexUpgradeController.this); + } + }; + } + + @Override + public synchronized void onStart(String name, int oldVersion, int newVersion) { + UpgradeAttempt a = UpgradeAttempt.create(name, oldVersion, newVersion); + try { + readyToStart.await(); + } catch (InterruptedException e) { + throw new AssertionError("interrupted waiting to start " + a, e); + } + checkState( + started.getCount() > 0, "already started %s upgrades, can't start %s", numExpected, a); + startedAttempts.add(a); + started.countDown(); + } + + @Override + public synchronized void onSuccess(String name, int oldVersion, int newVersion) { + finish(UpgradeAttempt.create(name, oldVersion, newVersion), succeededAttempts); + } + + @Override + public synchronized void onFailure(String name, int oldVersion, int newVersion) { + finish(UpgradeAttempt.create(name, oldVersion, newVersion), failedAttempts); + } + + private synchronized void finish(UpgradeAttempt a, List<UpgradeAttempt> out) { + checkState(readyToStart.getCount() == 0, "shouldn't be finishing upgrade before starting"); + checkState( + finished.getCount() > 0, "already finished %s upgrades, can't finish %s", numExpected, a); + out.add(a); + finished.countDown(); + } + + void runUpgrades() throws Exception { + readyToStart.countDown(); + started.await(); + finished.await(); + } + + synchronized ImmutableList<UpgradeAttempt> getStartedAttempts() { + return ImmutableList.copyOf(startedAttempts); + } + + synchronized ImmutableList<UpgradeAttempt> getSucceededAttempts() { + return ImmutableList.copyOf(succeededAttempts); + } + + synchronized ImmutableList<UpgradeAttempt> getFailedAttempts() { + return ImmutableList.copyOf(failedAttempts); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNoteDbIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNoteDbIT.java deleted file mode 100644 index e00058d..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/RebuildNoteDbIT.java +++ /dev/null
@@ -1,68 +0,0 @@ -// 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.acceptance.pgm; - -import static com.google.common.truth.Truth.assertThat; -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.io.Files; -import com.google.gerrit.launcher.GerritLauncher; -import com.google.gerrit.server.notedb.ConfigNotesMigration; -import com.google.gerrit.testutil.TempFileUtil; -import java.io.File; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -public class RebuildNoteDbIT { - private File sitePath; - - @Before - public void createTempDirectory() throws Exception { - sitePath = TempFileUtil.createTempDirectory(); - } - - @After - public void destroySite() throws Exception { - if (sitePath != null) { - TempFileUtil.cleanup(); - } - } - - @Test - public void rebuildEmptySite() throws Exception { - initSite(); - Files.append( - ConfigNotesMigration.allEnabledConfig().toText(), - new File(sitePath.toString(), "etc/gerrit.config"), - UTF_8); - runGerrit("RebuildNoteDb", "-d", sitePath.toString(), "--show-stack-trace"); - } - - private void initSite() throws Exception { - runGerrit( - "init", - "-d", - sitePath.getPath(), - "--batch", - "--no-auto-start", - "--skip-plugins", - "--show-stack-trace"); - } - - private static void runGerrit(String... args) throws Exception { - assertThat(GerritLauncher.mainImpl(args)).isEqualTo(0); - } -}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java index 79bbba2..94250e6 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/ReindexIT.java
@@ -14,48 +14,154 @@ package com.google.gerrit.acceptance.pgm; +import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; -import com.google.gerrit.launcher.GerritLauncher; -import com.google.gerrit.testutil.TempFileUtil; -import java.io.File; -import org.junit.After; -import org.junit.Before; +import com.google.common.collect.ImmutableSet; +import com.google.common.io.MoreFiles; +import com.google.common.io.RecursiveDeleteOption; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.StandaloneSiteTest; +import com.google.gerrit.acceptance.pgm.IndexUpgradeController.UpgradeAttempt; +import com.google.gerrit.extensions.api.GerritApi; +import com.google.gerrit.extensions.common.ChangeInput; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.index.GerritIndexStatus; +import com.google.gerrit.server.index.change.ChangeIndexCollection; +import com.google.gerrit.server.index.change.ChangeSchemaDefinitions; +import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.inject.Provider; +import java.nio.file.Files; +import java.util.Set; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; import org.junit.Test; -public class ReindexIT { - private File sitePath; +@NoHttpd +public class ReindexIT extends StandaloneSiteTest { + private static final String CHANGES = ChangeSchemaDefinitions.NAME; - @Before - public void createTempDirectory() throws Exception { - sitePath = TempFileUtil.createTempDirectory(); - } + private Project.NameKey project; + private String changeId; - @After - public void destroySite() throws Exception { - if (sitePath != null) { - TempFileUtil.cleanup(); + @Test + public void reindexFromScratch() throws Exception { + 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"); + assertReady(ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion()); + + try (ServerContext ctx = startServer()) { + GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class); + assertThat(gApi.changes().query("message:Test").get().stream().map(c -> c.changeId)) + .containsExactly(changeId); } } @Test - public void reindexEmptySite() throws Exception { - initSite(); - runGerrit("reindex", "-d", sitePath.toString(), "--show-stack-trace"); + public void onlineUpgradeChanges() throws Exception { + int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion(); + int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion(); + + // Before storing any changes, switch back to the previous version. + GerritIndexStatus status = new GerritIndexStatus(sitePaths); + status.setReady(CHANGES, currVersion, false); + status.setReady(CHANGES, prevVersion, true); + status.save(); + assertReady(prevVersion); + + setOnlineUpgradeConfig(false); + setUpChange(); + setOnlineUpgradeConfig(true); + + IndexUpgradeController u = new IndexUpgradeController(1); + try (ServerContext ctx = startServer(u.module())) { + assertSearchVersion(ctx, prevVersion); + assertWriteVersions(ctx, prevVersion, currVersion); + + // Updating and searching old schema version works. + Provider<InternalChangeQuery> queryProvider = + ctx.getInjector().getProvider(InternalChangeQuery.class); + assertThat(queryProvider.get().byKey(new Change.Key(changeId))).hasSize(1); + assertThat(queryProvider.get().byTopicOpen("topic1")).isEmpty(); + + GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class); + gApi.changes().id(changeId).topic("topic1"); + assertThat(queryProvider.get().byTopicOpen("topic1")).hasSize(1); + + u.runUpgrades(); + assertThat(u.getStartedAttempts()) + .containsExactly(UpgradeAttempt.create(CHANGES, prevVersion, currVersion)); + assertThat(u.getSucceededAttempts()) + .containsExactly(UpgradeAttempt.create(CHANGES, prevVersion, currVersion)); + assertThat(u.getFailedAttempts()).isEmpty(); + + assertReady(currVersion); + assertSearchVersion(ctx, currVersion); + assertWriteVersions(ctx, currVersion); + + // Updating and searching new schema version works. + assertThat(queryProvider.get().byTopicOpen("topic1")).hasSize(1); + assertThat(queryProvider.get().byTopicOpen("topic2")).isEmpty(); + gApi.changes().id(changeId).topic("topic2"); + assertThat(queryProvider.get().byTopicOpen("topic1")).isEmpty(); + assertThat(queryProvider.get().byTopicOpen("topic2")).hasSize(1); + } } - private void initSite() throws Exception { - runGerrit( - "init", - "-d", - sitePath.getPath(), - "--batch", - "--no-auto-start", - "--skip-plugins", - "--show-stack-trace"); + private void setUpChange() throws Exception { + project = new Project.NameKey("project"); + try (ServerContext ctx = startServer()) { + GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class); + gApi.projects().create(project.get()); + + ChangeInput in = new ChangeInput(project.get(), "master", "Test change"); + in.newBranch = true; + changeId = gApi.changes().create(in).info().changeId; + } } - private static void runGerrit(String... args) throws Exception { - assertThat(GerritLauncher.mainImpl(args)).isEqualTo(0); + private void setOnlineUpgradeConfig(boolean enable) throws Exception { + FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect()); + cfg.load(); + cfg.setBoolean("index", null, "onlineUpgrade", enable); + cfg.save(); + } + + private void assertSearchVersion(ServerContext ctx, int expected) { + assertThat( + ctx.getInjector() + .getInstance(ChangeIndexCollection.class) + .getSearchIndex() + .getSchema() + .getVersion()) + .named("search version") + .isEqualTo(expected); + } + + private void assertWriteVersions(ServerContext ctx, Integer... expected) { + assertThat( + ctx.getInjector() + .getInstance(ChangeIndexCollection.class) + .getWriteIndexes() + .stream() + .map(i -> i.getSchema().getVersion())) + .named("write versions") + .containsExactlyElementsIn(ImmutableSet.copyOf(expected)); + } + + private void assertReady(int expectedReady) throws Exception { + Set<Integer> allVersions = ChangeSchemaDefinitions.INSTANCE.getSchemas().keySet(); + GerritIndexStatus status = new GerritIndexStatus(sitePaths); + assertThat( + allVersions.stream().collect(toImmutableMap(v -> v, v -> status.getReady(CHANGES, v)))) + .named("ready state for index versions") + .isEqualTo(allVersions.stream().collect(toImmutableMap(v -> v, v -> v == expectedReady))); } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java new file mode 100644 index 0000000..6ba5b07 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
@@ -0,0 +1,216 @@ +// 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.acceptance.pgm; + +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.ImmutableMap; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.StandaloneSiteTest; +import com.google.gerrit.extensions.api.GerritApi; +import com.google.gerrit.extensions.common.ChangeInput; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.index.GerritIndexStatus; +import com.google.gerrit.server.index.change.ChangeIndexCollection; +import com.google.gerrit.server.index.change.ChangeSchemaDefinitions; +import com.google.gerrit.server.notedb.ConfigNotesMigration; +import com.google.gerrit.server.notedb.NoteDbChangeState; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; +import com.google.gerrit.server.notedb.NoteDbChangeState.RefState; +import com.google.gerrit.server.notedb.NotesMigrationState; +import com.google.gerrit.server.schema.ReviewDbFactory; +import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.Key; +import com.google.inject.TypeLiteral; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for NoteDb migrations where the entry point is through a program, {@code + * migrate-to-note-db} or {@code daemon}. + * + * <p><strong>Note:</strong> These tests are very slow due to the repeated daemon startup. Prefer + * adding tests to {@link com.google.gerrit.acceptance.server.notedb.OnlineNoteDbMigrationIT} if + * possible. + */ +@NoHttpd +public class StandaloneNoteDbMigrationIT extends StandaloneSiteTest { + private StoredConfig gerritConfig; + + private Project.NameKey project; + private Change.Id changeId; + + @Before + public void setUp() throws Exception { + gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect()); + } + + @Test + public void rebuildOneChangeTrialMode() throws Exception { + assertNotesMigrationState(NotesMigrationState.REVIEW_DB); + setUpOneChange(); + + migrate(); + assertNotesMigrationState(NotesMigrationState.READ_WRITE_NO_SEQUENCE); + + try (ServerContext ctx = startServer()) { + GitRepositoryManager repoManager = ctx.getInjector().getInstance(GitRepositoryManager.class); + ObjectId metaId; + try (Repository repo = repoManager.openRepository(project)) { + Ref ref = repo.exactRef(RefNames.changeMetaRef(changeId)); + assertThat(ref).isNotNull(); + metaId = ref.getObjectId(); + } + + try (ReviewDb db = openUnderlyingReviewDb(ctx)) { + Change c = db.changes().get(changeId); + assertThat(c).isNotNull(); + NoteDbChangeState state = NoteDbChangeState.parse(c); + assertThat(state).isNotNull(); + assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB); + assertThat(state.getRefState()).hasValue(RefState.create(metaId, ImmutableMap.of())); + } + } + } + + @Test + public void migrateOneChange() throws Exception { + assertNotesMigrationState(NotesMigrationState.REVIEW_DB); + setUpOneChange(); + + migrate("--trial", "false"); + assertNotesMigrationState(NotesMigrationState.NOTE_DB_UNFUSED); + + try (ServerContext ctx = startServer()) { + GitRepositoryManager repoManager = ctx.getInjector().getInstance(GitRepositoryManager.class); + try (Repository repo = repoManager.openRepository(project)) { + assertThat(repo.exactRef(RefNames.changeMetaRef(changeId))).isNotNull(); + } + + try (ReviewDb db = openUnderlyingReviewDb(ctx)) { + Change c = db.changes().get(changeId); + assertThat(c).isNotNull(); + NoteDbChangeState state = NoteDbChangeState.parse(c); + assertThat(state).isNotNull(); + assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.NOTE_DB); + assertThat(state.getRefState()).isEmpty(); + + ChangeInput in = new ChangeInput(project.get(), "master", "NoteDb-only change"); + in.newBranch = true; + GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class); + Change.Id id2 = new Change.Id(gApi.changes().create(in).info()._number); + assertThat(db.changes().get(id2)).isNull(); + } + } + } + + @Test + public void migrationWithReindex() throws Exception { + assertNotesMigrationState(NotesMigrationState.REVIEW_DB); + setUpOneChange(); + + int version = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion(); + GerritIndexStatus status = new GerritIndexStatus(sitePaths); + assertThat(status.getReady(ChangeSchemaDefinitions.NAME, version)).isTrue(); + status.setReady(ChangeSchemaDefinitions.NAME, version, false); + status.save(); + assertServerStartupFails(); + + migrate("--trial", "false"); + assertNotesMigrationState(NotesMigrationState.NOTE_DB_UNFUSED); + + status = new GerritIndexStatus(sitePaths); + assertThat(status.getReady(ChangeSchemaDefinitions.NAME, version)).isTrue(); + } + + @Test + public void onlineMigrationViaDaemon() throws Exception { + assertNotesMigrationState(NotesMigrationState.REVIEW_DB); + int prevVersion = ChangeSchemaDefinitions.INSTANCE.getPrevious().getVersion(); + int currVersion = ChangeSchemaDefinitions.INSTANCE.getLatest().getVersion(); + + // Before storing any changes, switch back to the previous version. + GerritIndexStatus status = new GerritIndexStatus(sitePaths); + status.setReady(ChangeSchemaDefinitions.NAME, currVersion, false); + status.setReady(ChangeSchemaDefinitions.NAME, prevVersion, true); + status.save(); + + setOnlineUpgradeConfig(false); + setUpOneChange(); + setOnlineUpgradeConfig(true); + + IndexUpgradeController u = new IndexUpgradeController(1); + try (ServerContext ctx = startServer(u.module(), "--migrate-to-note-db", "true")) { + ChangeIndexCollection indexes = ctx.getInjector().getInstance(ChangeIndexCollection.class); + assertThat(indexes.getSearchIndex().getSchema().getVersion()).isEqualTo(prevVersion); + + // Index schema upgrades happen after NoteDb migration, so waiting for those to complete + // should be sufficient. + u.runUpgrades(); + + assertThat(indexes.getSearchIndex().getSchema().getVersion()).isEqualTo(currVersion); + assertNotesMigrationState(NotesMigrationState.NOTE_DB_UNFUSED); + } + } + + private void setUpOneChange() throws Exception { + project = new Project.NameKey("project"); + try (ServerContext ctx = startServer()) { + GerritApi gApi = ctx.getInjector().getInstance(GerritApi.class); + gApi.projects().create("project"); + + ChangeInput in = new ChangeInput(project.get(), "master", "Test change"); + in.newBranch = true; + changeId = new Change.Id(gApi.changes().create(in).info()._number); + } + } + + private void migrate(String... additionalArgs) throws Exception { + runGerrit( + ImmutableList.of( + "migrate-to-note-db", "-d", sitePaths.site_path.toString(), "--show-stack-trace"), + ImmutableList.copyOf(additionalArgs)); + } + + private void assertNotesMigrationState(NotesMigrationState expected) throws Exception { + gerritConfig.load(); + assertThat(NotesMigrationState.forNotesMigration(new ConfigNotesMigration(gerritConfig))) + .hasValue(expected); + } + + private ReviewDb openUnderlyingReviewDb(ServerContext ctx) throws Exception { + return ctx.getInjector() + .getInstance(Key.get(new TypeLiteral<SchemaFactory<ReviewDb>>() {}, ReviewDbFactory.class)) + .open(); + } + + private void setOnlineUpgradeConfig(boolean enable) throws Exception { + gerritConfig.load(); + gerritConfig.setBoolean("index", null, "onlineUpgrade", enable); + gerritConfig.save(); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CheckAccessIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CheckAccessIT.java new file mode 100644 index 0000000..53c89c5 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/CheckAccessIT.java
@@ -0,0 +1,139 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.extensions.api.config.AccessCheckInfo; +import com.google.gerrit.extensions.api.config.AccessCheckInput; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.group.SystemGroupBackend; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; + +public class CheckAccessIT extends AbstractDaemonTest { + + private Project.NameKey normalProject; + private Project.NameKey secretProject; + private Project.NameKey secretRefProject; + private TestAccount privilegedUser; + private AccountGroup privilegedGroup; + + @Before + public void setUp() throws Exception { + normalProject = createProject("normal"); + secretProject = createProject("secret"); + secretRefProject = createProject("secretRef"); + privilegedGroup = groupCache.get(new AccountGroup.NameKey(createGroup("privilegedGroup"))); + + privilegedUser = accountCreator.create("privilegedUser", "snowden@nsa.gov", "Ed Snowden"); + gApi.groups().id(privilegedGroup.getGroupUUID().get()).addMembers(privilegedUser.username); + + assertThat(gApi.groups().id(privilegedGroup.getGroupUUID().get()).members().get(0).email) + .contains("snowden"); + + grant(secretProject, "refs/*", Permission.READ, false, privilegedGroup.getGroupUUID()); + block(secretProject, "refs/*", Permission.READ, SystemGroupBackend.REGISTERED_USERS); + + // deny/grant/block arg ordering is screwy. + deny(secretRefProject, "refs/*", Permission.READ, SystemGroupBackend.ANONYMOUS_USERS); + grant( + secretRefProject, + "refs/heads/secret/*", + Permission.READ, + false, + privilegedGroup.getGroupUUID()); + block( + secretRefProject, + "refs/heads/secret/*", + Permission.READ, + SystemGroupBackend.REGISTERED_USERS); + grant( + secretRefProject, + "refs/heads/*", + Permission.READ, + false, + SystemGroupBackend.REGISTERED_USERS); + } + + @Test + public void invalidInputs() { + List<AccessCheckInput> inputs = + ImmutableList.of( + new AccessCheckInput(), + new AccessCheckInput(user.email, null, null), + new AccessCheckInput(null, normalProject.toString(), null), + new AccessCheckInput("doesnotexist@invalid.com", normalProject.toString(), null)); + for (AccessCheckInput input : inputs) { + try { + gApi.config().server().checkAccess(input); + fail(String.format("want RestApiException for %s", newGson().toJson(input))); + } catch (RestApiException e) { + + } + } + } + + @Test + public void accessible() { + Map<AccessCheckInput, Integer> inputs = + ImmutableMap.of( + new AccessCheckInput(user.email, normalProject.get(), null), 200, + new AccessCheckInput(user.email, secretProject.get(), null), 403, + new AccessCheckInput(user.email, "nonexistent", null), 404, + new AccessCheckInput(privilegedUser.email, normalProject.get(), null), 200, + new AccessCheckInput(privilegedUser.email, secretProject.get(), null), 200); + + for (Map.Entry<AccessCheckInput, Integer> entry : inputs.entrySet()) { + String in = newGson().toJson(entry.getKey()); + AccessCheckInfo info = null; + + try { + info = gApi.config().server().checkAccess(entry.getKey()); + } catch (RestApiException e) { + fail(String.format("check.check(%s): exception %s", in, e)); + } + + int want = entry.getValue(); + if (want != info.status) { + fail(String.format("check.access(%s) = %d, want %d", in, info.status, want)); + } + + switch (want) { + case 403: + assertThat(info.message).contains("cannot see"); + break; + case 404: + assertThat(info.message).contains("does not exist"); + break; + case 200: + assertThat(info.message).isNull(); + break; + default: + fail(String.format("unknown code %d", want)); + } + } + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java index df44366..2fe9dcd 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -16,64 +16,88 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.acceptance.GitUtil.fetch; -import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME; +import static com.google.gerrit.acceptance.GitUtil.pushHead; +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.account.externalids.ExternalId.SCHEME_UUID; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toList; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.junit.Assert.fail; import com.github.rholder.retry.BlockStrategy; import com.github.rholder.retry.Retryer; import com.github.rholder.retry.RetryerBuilder; import com.github.rholder.retry.StopStrategies; +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.PushOneCommit; import com.google.gerrit.acceptance.RestResponse; import com.google.gerrit.acceptance.Sandboxed; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.Permission; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInput; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInput.CheckAccountExternalIdsInput; import com.google.gerrit.extensions.common.AccountExternalIdInfo; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.metrics.MetricMaker; +import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.RefNames; -import com.google.gerrit.server.account.ExternalId; -import com.google.gerrit.server.account.ExternalIds; -import com.google.gerrit.server.account.ExternalIdsUpdate; +import com.google.gerrit.server.account.externalids.DisabledExternalIdCache; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIdReader; +import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate.RefsMetaExternalIdsUpdate; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.git.LockFailureException; import com.google.gson.reflect.TypeToken; +import com.google.gwtorm.server.OrmDuplicateKeyException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.eclipse.jgit.api.errors.TransportException; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.notes.NoteMap; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.PushResult; +import org.eclipse.jgit.transport.RemoteRefUpdate; +import org.eclipse.jgit.transport.RemoteRefUpdate.Status; +import org.eclipse.jgit.util.MutableInteger; import org.junit.Test; @Sandboxed public class ExternalIdIT extends AbstractDaemonTest { @Inject private AllUsersName allUsers; - @Inject private ExternalIdsUpdate.Server extIdsUpdate; - @Inject private ExternalIds externalIds; + @Inject private ExternalIdReader externalIdReader; + @Inject private MetricMaker metricMaker; @Test - public void getExternalIDs() throws Exception { + public void getExternalIds() throws Exception { Collection<ExternalId> expectedIds = accountCache.get(user.getId()).getExternalIds(); - - List<AccountExternalIdInfo> expectedIdInfos = new ArrayList<>(); - for (ExternalId id : expectedIds) { - AccountExternalIdInfo info = new AccountExternalIdInfo(); - info.identity = id.key().get(); - info.emailAddress = id.email(); - info.canDelete = !id.isScheme(SCHEME_USERNAME) ? true : null; - info.trusted = true; - expectedIdInfos.add(info); - } + List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds); RestResponse response = userRestSession.get("/accounts/self/external.ids"); response.assertOK(); @@ -89,7 +113,35 @@ } @Test - public void deleteExternalIDs() throws Exception { + public void getExternalIdsOfOtherUserNotAllowed() throws Exception { + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("access database not permitted"); + gApi.accounts().id(admin.id.get()).getExternalIds(); + } + + @Test + public void getExternalIdsOfOtherUserWithAccessDatabase() throws Exception { + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + + Collection<ExternalId> expectedIds = accountCache.get(admin.getId()).getExternalIds(); + List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds); + + RestResponse response = userRestSession.get("/accounts/" + admin.id + "/external.ids"); + response.assertOK(); + + List<AccountExternalIdInfo> results = + newGson() + .fromJson( + response.getReader(), new TypeToken<List<AccountExternalIdInfo>>() {}.getType()); + + Collections.sort(expectedIdInfos); + Collections.sort(results); + assertThat(results).containsExactlyElementsIn(expectedIdInfos); + } + + @Test + public void deleteExternalIds() throws Exception { setApiUser(user); List<AccountExternalIdInfo> externalIds = gApi.accounts().self().getExternalIds(); @@ -115,7 +167,70 @@ } @Test - public void deleteExternalIDs_Conflict() throws Exception { + public void deleteExternalIdsOfOtherUserNotAllowed() throws Exception { + List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds(); + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("access database not permitted"); + gApi.accounts() + .id(admin.id.get()) + .deleteExternalIds(extIds.stream().map(e -> e.identity).collect(toList())); + } + + @Test + public void deleteExternalIdOfOtherUserUnderOwnAccount_UnprocessableEntity() throws Exception { + List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds(); + setApiUser(user); + exception.expect(UnprocessableEntityException.class); + exception.expectMessage(String.format("External id %s does not exist", extIds.get(0).identity)); + gApi.accounts() + .self() + .deleteExternalIds(extIds.stream().map(e -> e.identity).collect(toList())); + } + + @Test + public void deleteExternalIdsOfOtherUserWithAccessDatabase() throws Exception { + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + + List<AccountExternalIdInfo> externalIds = gApi.accounts().self().getExternalIds(); + + List<String> toDelete = new ArrayList<>(); + List<AccountExternalIdInfo> expectedIds = new ArrayList<>(); + for (AccountExternalIdInfo id : externalIds) { + if (id.canDelete != null && id.canDelete) { + toDelete.add(id.identity); + continue; + } + expectedIds.add(id); + } + + assertThat(toDelete).hasSize(1); + + setApiUser(user); + RestResponse response = + userRestSession.post("/accounts/" + admin.id + "/external.ids:delete", toDelete); + response.assertNoContent(); + List<AccountExternalIdInfo> results = gApi.accounts().id(admin.id.get()).getExternalIds(); + // The external ID in WebSession will not be set for tests, resulting that + // "mailto:user@example.com" can be deleted while "username:user" can't. + assertThat(results).hasSize(1); + assertThat(results).containsExactlyElementsIn(expectedIds); + } + + @Test + public void deleteExternalIdOfPreferredEmail() throws Exception { + String preferredEmail = gApi.accounts().self().get().email; + assertThat(preferredEmail).isNotNull(); + + gApi.accounts() + .self() + .deleteExternalIds( + ImmutableList.of(ExternalId.Key.create(SCHEME_MAILTO, preferredEmail).get())); + assertThat(gApi.accounts().self().get().email).isNull(); + } + + @Test + public void deleteExternalIds_Conflict() throws Exception { List<String> toDelete = new ArrayList<>(); String externalIdStr = "username:" + user.username; toDelete.add(externalIdStr); @@ -126,7 +241,7 @@ } @Test - public void deleteExternalIDs_UnprocessableEntity() throws Exception { + public void deleteExternalIds_UnprocessableEntity() throws Exception { List<String> toDelete = new ArrayList<>(); String externalIdStr = "mailto:user@domain.com"; toDelete.add(externalIdStr); @@ -160,20 +275,397 @@ @Test public void pushToExternalIdsBranch() throws Exception { - grant(Permission.READ, allUsers, RefNames.REFS_EXTERNAL_IDS); - grant(Permission.PUSH, allUsers, RefNames.REFS_EXTERNAL_IDS); - TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); - fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":externalIds"); - allUsersRepo.reset("externalIds"); - PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo); - push.to(RefNames.REFS_EXTERNAL_IDS) - .assertErrorStatus("not allowed to update " + RefNames.REFS_EXTERNAL_IDS); + fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS); + allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS); + + // different case email is allowed + ExternalId newExtId = createExternalIdWithOtherCaseEmail("foo:bar"); + addExtId(allUsersRepo, newExtId); + allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS); + + List<AccountExternalIdInfo> extIdsBefore = gApi.accounts().self().getExternalIds(); + + allowPushOfExternalIds(); + PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS); + assertThat(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS).getStatus()).isEqualTo(Status.OK); + + List<AccountExternalIdInfo> extIdsAfter = gApi.accounts().self().getExternalIds(); + assertThat(extIdsAfter) + .containsExactlyElementsIn( + Iterables.concat(extIdsBefore, ImmutableSet.of(toExternalIdInfo(newExtId)))); + } + + @Test + public void pushToExternalIdsBranchRejectsExternalIdWithoutAccountId() throws Exception { + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS); + allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS); + + insertExternalIdWithoutAccountId( + allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar"); + allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS); + + allowPushOfExternalIds(); + PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS); + assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs"); + } + + @Test + public void pushToExternalIdsBranchRejectsExternalIdWithKeyThatDoesntMatchTheNoteId() + throws Exception { + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS); + allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS); + + insertExternalIdWithKeyThatDoesntMatchNoteId( + allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar"); + allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS); + + allowPushOfExternalIds(); + PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS); + assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs"); + } + + @Test + public void pushToExternalIdsBranchRejectsExternalIdWithInvalidConfig() throws Exception { + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS); + allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS); + + insertExternalIdWithInvalidConfig( + allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar"); + allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS); + + allowPushOfExternalIds(); + PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS); + assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs"); + } + + @Test + public void pushToExternalIdsBranchRejectsExternalIdWithEmptyNote() throws Exception { + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS); + allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS); + + insertExternalIdWithEmptyNote( + allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar"); + allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS); + + allowPushOfExternalIds(); + PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS); + assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs"); + } + + @Test + public void pushToExternalIdsBranchRejectsExternalIdForNonExistingAccount() throws Exception { + testPushToExternalIdsBranchRejectsInvalidExternalId( + createExternalIdForNonExistingAccount("foo:bar")); + } + + @Test + public void pushToExternalIdsBranchRejectsExternalIdWithInvalidEmail() throws Exception { + testPushToExternalIdsBranchRejectsInvalidExternalId( + createExternalIdWithInvalidEmail("foo:bar")); + } + + @Test + public void pushToExternalIdsBranchRejectsDuplicateEmails() throws Exception { + testPushToExternalIdsBranchRejectsInvalidExternalId( + createExternalIdWithDuplicateEmail("foo:bar")); + } + + @Test + public void pushToExternalIdsBranchRejectsBadPassword() throws Exception { + testPushToExternalIdsBranchRejectsInvalidExternalId(createExternalIdWithBadPassword("foo")); + } + + private void testPushToExternalIdsBranchRejectsInvalidExternalId(ExternalId invalidExtId) + throws Exception { + TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers); + fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS); + allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS); + + addExtId(allUsersRepo, invalidExtId); + allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS); + + allowPushOfExternalIds(); + PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS); + assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs"); + } + + @Test + public void readExternalIdsWhenInvalidExternalIdsExist() throws Exception { + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + resetCurrentApiUser(); + + insertValidExternalIds(); + insertInvalidButParsableExternalIds(); + + Set<ExternalId> parseableExtIds = externalIds.all(); + + insertNonParsableExternalIds(); + + Set<ExternalId> extIds = externalIds.all(); + assertThat(extIds).containsExactlyElementsIn(parseableExtIds); + + for (ExternalId parseableExtId : parseableExtIds) { + ExternalId extId = externalIds.get(parseableExtId.key()); + assertThat(extId).isEqualTo(parseableExtId); + } + } + + @Test + public void checkConsistency() throws Exception { + allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE); + resetCurrentApiUser(); + + insertValidExternalIds(); + + ConsistencyCheckInput input = new ConsistencyCheckInput(); + input.checkAccountExternalIds = new CheckAccountExternalIdsInput(); + ConsistencyCheckInfo checkInfo = gApi.config().server().checkConsistency(input); + assertThat(checkInfo.checkAccountExternalIdsResult.problems).isEmpty(); + + Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>(); + expectedProblems.addAll(insertInvalidButParsableExternalIds()); + expectedProblems.addAll(insertNonParsableExternalIds()); + + checkInfo = gApi.config().server().checkConsistency(input); + assertThat(checkInfo.checkAccountExternalIdsResult.problems).hasSize(expectedProblems.size()); + assertThat(checkInfo.checkAccountExternalIdsResult.problems) + .containsExactlyElementsIn(expectedProblems); + } + + @Test + public void checkConsistencyNotAllowed() throws Exception { + exception.expect(AuthException.class); + exception.expectMessage("access database not permitted"); + gApi.config().server().checkConsistency(new ConsistencyCheckInput()); + } + + private ConsistencyProblemInfo consistencyError(String message) { + return new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, message); + } + + private void insertValidExternalIds() throws IOException, ConfigInvalidException, OrmException { + MutableInteger i = new MutableInteger(); + String scheme = "valid"; + ExternalIdsUpdate u = extIdsUpdate.create(); + + // create valid external IDs + u.insert( + ExternalId.createWithPassword( + ExternalId.Key.parse(nextId(scheme, i)), + admin.id, + "admin.other@example.com", + "secret-password")); + u.insert(createExternalIdWithOtherCaseEmail(nextId(scheme, i))); + } + + private Set<ConsistencyProblemInfo> insertInvalidButParsableExternalIds() + throws IOException, ConfigInvalidException, OrmException { + MutableInteger i = new MutableInteger(); + String scheme = "invalid"; + ExternalIdsUpdate u = extIdsUpdate.create(); + + Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>(); + ExternalId extIdForNonExistingAccount = + createExternalIdForNonExistingAccount(nextId(scheme, i)); + u.insert(extIdForNonExistingAccount); + expectedProblems.add( + consistencyError( + "External ID '" + + extIdForNonExistingAccount.key().get() + + "' belongs to account that doesn't exist: " + + extIdForNonExistingAccount.accountId().get())); + + ExternalId extIdWithInvalidEmail = createExternalIdWithInvalidEmail(nextId(scheme, i)); + u.insert(extIdWithInvalidEmail); + expectedProblems.add( + consistencyError( + "External ID '" + + extIdWithInvalidEmail.key().get() + + "' has an invalid email: " + + extIdWithInvalidEmail.email())); + + ExternalId extIdWithDuplicateEmail = createExternalIdWithDuplicateEmail(nextId(scheme, i)); + u.insert(extIdWithDuplicateEmail); + expectedProblems.add( + consistencyError( + "Email '" + + extIdWithDuplicateEmail.email() + + "' is not unique, it's used by the following external IDs: '" + + extIdWithDuplicateEmail.key().get() + + "', 'mailto:" + + extIdWithDuplicateEmail.email() + + "'")); + + ExternalId extIdWithBadPassword = createExternalIdWithBadPassword("admin-username"); + u.insert(extIdWithBadPassword); + expectedProblems.add( + consistencyError( + "External ID '" + + extIdWithBadPassword.key().get() + + "' has an invalid password: unrecognized algorithm")); + + return expectedProblems; + } + + private Set<ConsistencyProblemInfo> insertNonParsableExternalIds() throws IOException { + MutableInteger i = new MutableInteger(); + String scheme = "corrupt"; + + Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>(); + try (Repository repo = repoManager.openRepository(allUsers); + RevWalk rw = new RevWalk(repo)) { + String externalId = nextId(scheme, i); + String noteId = insertExternalIdWithoutAccountId(repo, rw, externalId); + expectedProblems.add( + consistencyError( + "Invalid external ID config for note '" + + noteId + + "': Value for 'externalId." + + externalId + + ".accountId' is missing, expected account ID")); + + externalId = nextId(scheme, i); + noteId = insertExternalIdWithKeyThatDoesntMatchNoteId(repo, rw, externalId); + expectedProblems.add( + consistencyError( + "Invalid external ID config for note '" + + noteId + + "': SHA1 of external ID '" + + externalId + + "' does not match note ID '" + + noteId + + "'")); + + noteId = insertExternalIdWithInvalidConfig(repo, rw, nextId(scheme, i)); + expectedProblems.add( + consistencyError( + "Invalid external ID config for note '" + noteId + "': Invalid line in config file")); + + noteId = insertExternalIdWithEmptyNote(repo, rw, nextId(scheme, i)); + expectedProblems.add( + consistencyError( + "Invalid external ID config for note '" + + noteId + + "': Expected exactly 1 'externalId' section, found 0")); + } + + return expectedProblems; + } + + private ExternalId createExternalIdWithOtherCaseEmail(String externalId) { + return ExternalId.createWithPassword( + ExternalId.Key.parse(externalId), admin.id, admin.email.toUpperCase(Locale.US), "password"); + } + + private String insertExternalIdWithoutAccountId(Repository repo, RevWalk rw, String externalId) + throws IOException { + ObjectId rev = ExternalIdReader.readRevision(repo); + NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev); + + ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id); + + try (ObjectInserter ins = repo.newObjectInserter()) { + ObjectId noteId = extId.key().sha1(); + Config c = new Config(); + extId.writeToConfig(c); + c.unset("externalId", extId.key().get(), "accountId"); + byte[] raw = c.toText().getBytes(UTF_8); + ObjectId dataBlob = ins.insert(OBJ_BLOB, raw); + noteMap.set(noteId, dataBlob); + + ExternalIdsUpdate.commit( + repo, rw, ins, rev, noteMap, "Add external ID", admin.getIdent(), admin.getIdent()); + return noteId.getName(); + } + } + + private String insertExternalIdWithKeyThatDoesntMatchNoteId( + Repository repo, RevWalk rw, String externalId) throws IOException { + ObjectId rev = ExternalIdReader.readRevision(repo); + NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev); + + ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id); + + try (ObjectInserter ins = repo.newObjectInserter()) { + ObjectId noteId = ExternalId.Key.parse(externalId + "x").sha1(); + Config c = new Config(); + extId.writeToConfig(c); + byte[] raw = c.toText().getBytes(UTF_8); + ObjectId dataBlob = ins.insert(OBJ_BLOB, raw); + noteMap.set(noteId, dataBlob); + + ExternalIdsUpdate.commit( + repo, rw, ins, rev, noteMap, "Add external ID", admin.getIdent(), admin.getIdent()); + return noteId.getName(); + } + } + + private String insertExternalIdWithInvalidConfig(Repository repo, RevWalk rw, String externalId) + throws IOException { + ObjectId rev = ExternalIdReader.readRevision(repo); + NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev); + + try (ObjectInserter ins = repo.newObjectInserter()) { + ObjectId noteId = ExternalId.Key.parse(externalId).sha1(); + byte[] raw = "bad-config".getBytes(UTF_8); + ObjectId dataBlob = ins.insert(OBJ_BLOB, raw); + noteMap.set(noteId, dataBlob); + + ExternalIdsUpdate.commit( + repo, rw, ins, rev, noteMap, "Add external ID", admin.getIdent(), admin.getIdent()); + return noteId.getName(); + } + } + + private String insertExternalIdWithEmptyNote(Repository repo, RevWalk rw, String externalId) + throws IOException { + ObjectId rev = ExternalIdReader.readRevision(repo); + NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev); + + try (ObjectInserter ins = repo.newObjectInserter()) { + ObjectId noteId = ExternalId.Key.parse(externalId).sha1(); + byte[] raw = "".getBytes(UTF_8); + ObjectId dataBlob = ins.insert(OBJ_BLOB, raw); + noteMap.set(noteId, dataBlob); + + ExternalIdsUpdate.commit( + repo, rw, ins, rev, noteMap, "Add external ID", admin.getIdent(), admin.getIdent()); + return noteId.getName(); + } + } + + private ExternalId createExternalIdForNonExistingAccount(String externalId) { + return ExternalId.create(ExternalId.Key.parse(externalId), new Account.Id(1)); + } + + private ExternalId createExternalIdWithInvalidEmail(String externalId) { + return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), admin.id, "invalid-email"); + } + + private ExternalId createExternalIdWithDuplicateEmail(String externalId) { + return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), admin.id, admin.email); + } + + private ExternalId createExternalIdWithBadPassword(String username) { + return ExternalId.create( + ExternalId.Key.create(SCHEME_USERNAME, username), + admin.id, + null, + "non-hashed-password-is-not-allowed"); + } + + private static String nextId(String scheme, MutableInteger i) { + return scheme + ":foo" + ++i.value; } @Test public void retryOnLockFailure() throws Exception { - Retryer<Void> retryer = + Retryer<RefsMetaExternalIdsUpdate> retryer = ExternalIdsUpdate.retryerBuilder() .withBlockStrategy( new BlockStrategy() { @@ -193,12 +685,15 @@ repoManager, accountCache, allUsers, + metricMaker, + externalIds, + new DisabledExternalIdCache(), serverIdent.get(), serverIdent.get(), () -> { if (!doneBgUpdate.getAndSet(true)) { try { - extIdsUpdate.create().insert(db, ExternalId.create(barId, admin.id)); + extIdsUpdate.create().insert(ExternalId.create(barId, admin.id)); } catch (IOException | ConfigInvalidException | OrmException e) { // Ignore, the successful insertion of the external ID is asserted later } @@ -206,7 +701,7 @@ }, retryer); assertThat(doneBgUpdate.get()).isFalse(); - update.insert(db, ExternalId.create(fooId, admin.id)); + update.insert(ExternalId.create(fooId, admin.id)); assertThat(doneBgUpdate.get()).isTrue(); assertThat(externalIds.get(fooId)).isNotNull(); @@ -226,24 +721,27 @@ repoManager, accountCache, allUsers, + metricMaker, + externalIds, + new DisabledExternalIdCache(), serverIdent.get(), serverIdent.get(), () -> { try { extIdsUpdate .create() - .insert(db, ExternalId.create(extIdsKeys[bgCounter.getAndAdd(1)], admin.id)); + .insert(ExternalId.create(extIdsKeys[bgCounter.getAndAdd(1)], admin.id)); } catch (IOException | ConfigInvalidException | OrmException e) { // Ignore, the successful insertion of the external ID is asserted later } }, - RetryerBuilder.<Void>newBuilder() + RetryerBuilder.<RefsMetaExternalIdsUpdate>newBuilder() .retryIfException(e -> e instanceof LockFailureException) .withStopStrategy(StopStrategies.stopAfterAttempt(extIdsKeys.length)) .build()); assertThat(bgCounter.get()).isEqualTo(0); try { - update.insert(db, ExternalId.create(ExternalId.Key.create("abc", "abc"), admin.id)); + update.insert(ExternalId.create(ExternalId.Key.create("abc", "abc"), admin.id)); fail("expected LockFailureException"); } catch (LockFailureException e) { // Ignore, expected @@ -253,4 +751,130 @@ assertThat(externalIds.get(extIdKey)).isNotNull(); } } + + @Test + public void readExternalIdWithAccountIdThatCanBeExpressedInKiB() throws Exception { + ExternalId.Key extIdKey = ExternalId.Key.parse("foo:bar"); + Account.Id accountId = new Account.Id(1024 * 100); + extIdsUpdate.create().insert(ExternalId.create(extIdKey, accountId)); + ExternalId extId = externalIds.get(extIdKey); + assertThat(extId.accountId()).isEqualTo(accountId); + } + + @Test + public void checkNoReloadAfterUpdate() throws Exception { + Set<ExternalId> expectedExtIds = new HashSet<>(externalIds.byAccount(admin.id)); + externalIdReader.setFailOnLoad(true); + + // insert external ID + ExternalId extId = ExternalId.create("foo", "bar", admin.id); + extIdsUpdate.create().insert(extId); + expectedExtIds.add(extId); + assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds); + + // update external ID + expectedExtIds.remove(extId); + extId = ExternalId.createWithEmail("foo", "bar", admin.id, "foo.bar@example.com"); + extIdsUpdate.create().upsert(extId); + expectedExtIds.add(extId); + assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds); + + // delete external ID + extIdsUpdate.create().delete(extId); + expectedExtIds.remove(extId); + assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds); + } + + @Test + public void byAccountFailIfReadingExternalIdsFails() throws Exception { + externalIdReader.setFailOnLoad(true); + + // update external ID branch so that external IDs need to be reloaded + insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id)); + + exception.expect(IOException.class); + externalIds.byAccount(admin.id); + } + + @Test + public void byEmailFailIfReadingExternalIdsFails() throws Exception { + externalIdReader.setFailOnLoad(true); + + // update external ID branch so that external IDs need to be reloaded + insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id)); + + exception.expect(IOException.class); + externalIds.byEmail(admin.email); + } + + @Test + public void byAccountUpdateExternalIdsBehindGerritsBack() throws Exception { + Set<ExternalId> expectedExternalIds = new HashSet<>(externalIds.byAccount(admin.id)); + ExternalId newExtId = ExternalId.create("foo", "bar", admin.id); + insertExtIdBehindGerritsBack(newExtId); + expectedExternalIds.add(newExtId); + assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExternalIds); + } + + private void insertExtIdBehindGerritsBack(ExternalId extId) throws Exception { + try (Repository repo = repoManager.openRepository(allUsers); + RevWalk rw = new RevWalk(repo); + ObjectInserter ins = repo.newObjectInserter()) { + ObjectId rev = ExternalIdReader.readRevision(repo); + NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev); + ExternalIdsUpdate.insert(rw, ins, noteMap, extId); + ExternalIdsUpdate.commit( + repo, rw, ins, rev, noteMap, "insert new ID", serverIdent.get(), serverIdent.get()); + } + } + + private void addExtId(TestRepository<?> testRepo, ExternalId... extIds) + throws IOException, OrmDuplicateKeyException, ConfigInvalidException { + ObjectId rev = ExternalIdReader.readRevision(testRepo.getRepository()); + + try (ObjectInserter ins = testRepo.getRepository().newObjectInserter()) { + NoteMap noteMap = ExternalIdReader.readNoteMap(testRepo.getRevWalk(), rev); + for (ExternalId extId : extIds) { + ExternalIdsUpdate.insert(testRepo.getRevWalk(), ins, noteMap, extId); + } + + ExternalIdsUpdate.commit( + testRepo.getRepository(), + testRepo.getRevWalk(), + ins, + rev, + noteMap, + "Add external ID", + admin.getIdent(), + admin.getIdent()); + } + } + + private List<AccountExternalIdInfo> toExternalIdInfos(Collection<ExternalId> extIds) { + return extIds.stream().map(this::toExternalIdInfo).collect(toList()); + } + + private AccountExternalIdInfo toExternalIdInfo(ExternalId extId) { + AccountExternalIdInfo info = new AccountExternalIdInfo(); + info.identity = extId.key().get(); + info.emailAddress = extId.email(); + info.canDelete = !extId.isScheme(SCHEME_USERNAME) ? true : null; + info.trusted = + extId.isScheme(SCHEME_MAILTO) + || extId.isScheme(SCHEME_UUID) + || extId.isScheme(SCHEME_USERNAME) + ? true + : null; + return info; + } + + private void allowPushOfExternalIds() throws IOException, ConfigInvalidException { + grant(allUsers, RefNames.REFS_EXTERNAL_IDS, Permission.READ); + grant(allUsers, RefNames.REFS_EXTERNAL_IDS, Permission.PUSH); + } + + private void assertRefUpdateFailure(RemoteRefUpdate update, String msg) { + assertThat(update.getStatus()).isEqualTo(Status.REJECTED_OTHER_REASON); + assertThat(update.getMessage()).isEqualTo(msg); + } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java index c69391c..05e5f99 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -39,8 +39,10 @@ import com.google.gerrit.extensions.api.changes.RevisionApi; import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.api.groups.GroupInput; +import com.google.gerrit.extensions.client.ListChangesOption; 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.CommentInfo; import com.google.gerrit.extensions.common.GroupInfo; @@ -62,6 +64,7 @@ import com.google.gerrit.server.project.Util; import com.google.gerrit.server.query.change.ChangeData; import com.google.inject.Inject; +import java.util.EnumSet; import org.apache.http.Header; import org.apache.http.message.BasicHeader; import org.junit.After; @@ -84,7 +87,7 @@ @Before public void setUp() throws Exception { anonRestSession = new RestSession(server, null); - admin2 = accounts.admin2(); + admin2 = accountCreator.admin2(); GroupInput gi = new GroupInput(); gi.name = name("New-Group"); gi.members = ImmutableList.of(user.id.toString()); @@ -311,7 +314,7 @@ in.label("Code-Review", 1); exception.expect(UnprocessableEntityException.class); - exception.expectMessage("on_behalf_of account " + user.id + " cannot see destination ref"); + exception.expectMessage("on_behalf_of account " + user.id + " cannot see change"); revision.review(in); } @@ -319,7 +322,7 @@ @Test public void voteOnBehalfOfInvisibleUserNotAllowed() throws Exception { allowCodeReviewOnBehalfOf(); - setApiUser(accounts.user2()); + setApiUser(accountCreator.user2()); assertThat(accountControlFactory.get().canSee(user.id)).isFalse(); PushOneCommit.Result r = createChange(); @@ -375,7 +378,7 @@ SubmitInput in = new SubmitInput(); in.onBehalfOf = admin2.email; exception.expect(AuthException.class); - exception.expectMessage("submit on behalf of not permitted"); + exception.expectMessage("submit as not permitted"); gApi.changes().id(project.get() + "~master~" + r.getChangeId()).current().submit(in); } @@ -390,7 +393,7 @@ SubmitInput in = new SubmitInput(); in.onBehalfOf = user.email; exception.expect(UnprocessableEntityException.class); - exception.expectMessage("on_behalf_of account " + user.id + " cannot see destination ref"); + exception.expectMessage("on_behalf_of account " + user.id + " cannot see change"); gApi.changes().id(changeId).current().submit(in); } @@ -398,7 +401,7 @@ @Test public void submitOnBehalfOfInvisibleUserNotAllowed() throws Exception { allowSubmitOnBehalfOf(); - setApiUser(accounts.user2()); + setApiUser(accountCreator.user2()); assertThat(accountControlFactory.get().canSee(user.id)).isFalse(); PushOneCommit.Result r = createChange(); @@ -499,7 +502,7 @@ // X-Gerrit-RunAs user (user2). allowRunAs(); allowCodeReviewOnBehalfOf(); - TestAccount user2 = accounts.user2(); + TestAccount user2 = accountCreator.user2(); PushOneCommit.Result r = createChange(); ReviewInput in = new ReviewInput(); @@ -529,6 +532,28 @@ assertThat(m.getRealAuthor()).isEqualTo(admin.id); // not user2 } + @Test + public void changeMessageCreatedOnBehalfOfHasRealUser() throws Exception { + allowCodeReviewOnBehalfOf(); + + PushOneCommit.Result r = createChange(); + ReviewInput in = new ReviewInput(); + in.onBehalfOf = user.id.toString(); + in.message = "Message on behalf of"; + in.label("Code-Review", 1); + + setApiUser(accountCreator.user2()); + gApi.changes().id(r.getChangeId()).revision(r.getPatchSetId().getId()).review(in); + + ChangeInfo info = + gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.MESSAGES)); + assertThat(info.messages).hasSize(2); + + ChangeMessageInfo changeMessageInfo = Iterables.getLast(info.messages); + assertThat(changeMessageInfo.realAuthor).isNotNull(); + assertThat(changeMessageInfo.realAuthor._accountId).isEqualTo(accountCreator.user2().id.get()); + } + private void allowCodeReviewOnBehalfOf() throws Exception { ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); LabelType codeReviewType = Util.codeReview();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java index 9378591..7de9d70 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/PutUsernameIT.java
@@ -18,23 +18,16 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.RestResponse; -import com.google.gerrit.common.TimeUtil; -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.PutUsername; -import com.google.gwtorm.server.SchemaFactory; -import com.google.inject.Inject; -import java.util.Collections; import org.junit.Test; public class PutUsernameIT extends AbstractDaemonTest { - @Inject private SchemaFactory<ReviewDb> reviewDbProvider; - @Test public void set() throws Exception { PutUsername.Input in = new PutUsername.Input(); in.username = "myUsername"; - RestResponse r = adminRestSession.put("/accounts/" + createUser().get() + "/username", in); + RestResponse r = + adminRestSession.put("/accounts/" + accountCreator.create().id.get() + "/username", in); r.assertOK(); assertThat(newGson().fromJson(r.getReader(), String.class)).isEqualTo(in.username); } @@ -43,7 +36,9 @@ public void setExisting_Conflict() throws Exception { PutUsername.Input in = new PutUsername.Input(); in.username = admin.username; - adminRestSession.put("/accounts/" + createUser().get() + "/username", in).assertConflict(); + adminRestSession + .put("/accounts/" + accountCreator.create().id.get() + "/username", in) + .assertConflict(); } @Test @@ -57,13 +52,4 @@ public void delete_MethodNotAllowed() throws Exception { adminRestSession.put("/accounts/" + admin.username + "/username").assertMethodNotAllowed(); } - - private Account.Id createUser() throws Exception { - try (ReviewDb db = reviewDbProvider.open()) { - Account.Id id = new Account.Id(db.nextAccountId()); - Account a = new Account(id, TimeUtil.nowTs()); - db.accounts().insert(Collections.singleton(a)); - return id; - } - } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java index 94fa99b..0cb5e81 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -17,6 +17,7 @@ import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assert_; +import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION; import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS; @@ -32,6 +33,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestAccount; @@ -40,6 +42,7 @@ import com.google.gerrit.common.data.Permission; import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.api.projects.BranchInput; +import com.google.gerrit.extensions.api.projects.ConfigInput; import com.google.gerrit.extensions.api.projects.ProjectInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.InheritableBoolean; @@ -65,6 +68,7 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.change.RevisionResource; import com.google.gerrit.server.change.Submit; +import com.google.gerrit.server.change.Submit.TestSubmitInput; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.git.validators.OnSubmitValidationListener; import com.google.gerrit.server.notedb.ChangeNotes; @@ -79,6 +83,7 @@ import com.google.inject.Inject; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -95,6 +100,8 @@ import org.eclipse.jgit.lib.Repository; 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.After; import org.junit.Before; import org.junit.Test; @@ -112,8 +119,6 @@ @Inject private IdentifiedUser.GenericFactory userFactory; - @Inject private BatchUpdate.Factory updateFactory; - @Inject private DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners; private RegistrationHandle onSubmitValidatorHandle; @@ -306,7 +311,7 @@ public void submitNoPermission() throws Exception { // create project where submit is blocked Project.NameKey p = createProject("p"); - block(Permission.SUBMIT, REGISTERED_USERS, "refs/*", p); + block(p, "refs/*", Permission.SUBMIT, REGISTERED_USERS); TestRepository<InMemoryRepository> repo = cloneProject(p, admin); PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo); @@ -480,6 +485,43 @@ } @Test + public void submitReusingOldTopic() throws Exception { + assume().that(isSubmitWholeTopicEnabled()).isTrue(); + + String topic = "test-topic"; + PushOneCommit.Result change1 = createChange("Change 1", "a.txt", "content", topic); + PushOneCommit.Result change2 = createChange("Change 2", "a.txt", "content", topic); + String id1 = change1.getChangeId(); + String id2 = change2.getChangeId(); + approve(id1); + approve(id2); + assertSubmittedTogether(id1, ImmutableList.of(id1, id2)); + assertSubmittedTogether(id2, ImmutableList.of(id1, id2)); + submit(id2); + + String expectedTopic = name(topic); + change1.assertChange(Change.Status.MERGED, expectedTopic, admin); + change2.assertChange(Change.Status.MERGED, expectedTopic, admin); + assertSubmittedTogether(id1, ImmutableList.of(id1, id2)); + assertSubmittedTogether(id2, ImmutableList.of(id1, id2)); + + PushOneCommit.Result change3 = createChange("Change 3", "c.txt", "content", topic); + String id3 = change3.getChangeId(); + approve(id3); + assertSubmittedTogether(id3, ImmutableList.of()); + submit(id3); + + change3.assertChange(Change.Status.MERGED, expectedTopic, admin); + assertSubmittedTogether(id3, ImmutableList.of()); + } + + private void assertSubmittedTogether(String changeId, Iterable<String> expected) + throws Exception { + assertThat(gApi.changes().id(changeId).submittedTogether().stream().map(i -> i.changeId)) + .containsExactlyElementsIn(expected); + } + + @Test public void submitDraftChange() throws Exception { PushOneCommit.Result draft = createDraftChange(); Change.Id num = draft.getChange().getId(); @@ -494,6 +536,20 @@ } @Test + public void submitWorkInProgressChange() throws Exception { + PushOneCommit.Result change = createWorkInProgressChange(); + Change.Id num = change.getChange().getId(); + submitWithConflict( + change.getChangeId(), + "Failed to submit 1 change due to the following problems:\n" + + "Change " + + num + + ": Change " + + num + + " is work in progress"); + } + + @Test public void submitDraftPatchSet() throws Exception { PushOneCommit.Result change = createChange(); PushOneCommit.Result draft = amendChangeAsDraft(change.getChangeId()); @@ -756,11 +812,16 @@ new OnSubmitValidationListener() { @Override public void preBranchUpdate(Arguments args) throws ValidationException { - assertThat(args.getCommands().keySet()).contains("refs/heads/master"); - try (RevWalk rw = args.newRevWalk()) { - rw.parseBody(rw.parseCommit(args.getCommands().get("refs/heads/master").getNewId())); + String master = "refs/heads/master"; + assertThat(args.getCommands()).containsKey(master); + ReceiveCommand cmd = args.getCommands().get(master); + ObjectId newMasterId = cmd.getNewId(); + try (Repository repo = repoManager.openRepository(args.getProject())) { + assertThat(repo.exactRef(master).getObjectId()).isEqualTo(cmd.getOldId()); + assertThat(args.getRef(master)).hasValue(newMasterId); + args.getRevWalk().parseBody(args.getRevWalk().parseCommit(newMasterId)); } catch (IOException e) { - assertThat(e).isNull(); + throw new AssertionError("failed checking new ref value", e); } projectsCalled.add(args.getProject().get()); if (projectsCalled.size() == 2) { @@ -783,10 +844,181 @@ } } + @Test + public void submitWithCommitAndItsMergeCommitTogether() throws Exception { + assume().that(isSubmitWholeTopicEnabled()).isTrue(); + + RevCommit initialHead = getRemoteHead(); + + // Create a stable branch and bootstrap it. + gApi.projects().name(project.get()).branch("stable").create(new BranchInput()); + PushOneCommit push = + pushFactory.create(db, user.getIdent(), testRepo, "initial commit", "a.txt", "a"); + PushOneCommit.Result change = push.to("refs/heads/stable"); + + RevCommit stable = getRemoteHead(project, "stable"); + RevCommit master = getRemoteHead(project, "master"); + + assertThat(master).isEqualTo(initialHead); + assertThat(stable).isEqualTo(change.getCommit()); + + testRepo.git().fetch().call(); + testRepo.git().branchCreate().setName("stable").setStartPoint(stable).call(); + testRepo.git().branchCreate().setName("master").setStartPoint(master).call(); + + // Create a fix in stable branch. + testRepo.reset(stable); + RevCommit fix = + testRepo + .commit() + .parent(stable) + .message("small fix") + .add("b.txt", "b") + .insertChangeId() + .create(); + testRepo.branch("refs/heads/stable").update(fix); + testRepo + .git() + .push() + .setRefSpecs(new RefSpec("refs/heads/stable:refs/for/stable/" + name("topic"))) + .call(); + + // Merge the fix into master. + testRepo.reset(master); + RevCommit merge = + testRepo + .commit() + .parent(master) + .parent(fix) + .message("Merge stable into master") + .insertChangeId() + .create(); + testRepo.branch("refs/heads/master").update(merge); + testRepo + .git() + .push() + .setRefSpecs(new RefSpec("refs/heads/master:refs/for/master/" + name("topic"))) + .call(); + + // Submit together. + String fixId = GitUtil.getChangeId(testRepo, fix).get(); + String mergeId = GitUtil.getChangeId(testRepo, merge).get(); + approve(fixId); + approve(mergeId); + submit(mergeId); + assertMerged(fixId); + assertMerged(mergeId); + testRepo.git().fetch().call(); + RevWalk rw = testRepo.getRevWalk(); + master = rw.parseCommit(getRemoteHead(project, "master")); + assertThat(rw.isMergedInto(merge, master)).isTrue(); + assertThat(rw.isMergedInto(fix, master)).isTrue(); + } + + @Test + public void retrySubmitSingleChangeOnLockFailure() throws Exception { + assume().that(notesMigration.fuseUpdates()).isTrue(); + + PushOneCommit.Result change = createChange(); + String id = change.getChangeId(); + approve(id); + + TestSubmitInput input = new TestSubmitInput(); + input.generateLockFailures = + new ArrayDeque<>( + ImmutableList.of( + true, // Attempt 1: lock failure + false, // Attempt 2: success + false)); // Leftover value to check total number of calls. + submit(id, input); + assertMerged(id); + + testRepo.git().fetch().call(); + RevWalk rw = testRepo.getRevWalk(); + RevCommit master = rw.parseCommit(getRemoteHead(project, "master")); + RevCommit patchSet = parseCurrentRevision(rw, change); + assertThat(rw.isMergedInto(patchSet, master)).isTrue(); + + assertThat(input.generateLockFailures).containsExactly(false); + } + + @Test + public void retrySubmitAfterTornTopicOnLockFailure() throws Exception { + assume().that(notesMigration.fuseUpdates()).isTrue(); + assume().that(isSubmitWholeTopicEnabled()).isTrue(); + + String topic = "test-topic"; + + TestRepository<?> repoA = createProjectWithPush("project-a", null, getSubmitType()); + TestRepository<?> repoB = createProjectWithPush("project-b", null, getSubmitType()); + + PushOneCommit.Result change1 = + createChange(repoA, "master", "Change 1", "a.txt", "content", topic); + PushOneCommit.Result change2 = + createChange(repoB, "master", "Change 2", "b.txt", "content", topic); + + approve(change1.getChangeId()); + approve(change2.getChangeId()); + + TestSubmitInput input = new TestSubmitInput(); + input.generateLockFailures = + new ArrayDeque<>( + ImmutableList.of( + false, // Change 1, attempt 1: success + true, // Change 2, attempt 1: lock failure + false, // Change 1, attempt 2: success + false, // Change 2, attempt 2: success + false)); // Leftover value to check total number of calls. + submit(change2.getChangeId(), input); + + String expectedTopic = name(topic); + change1.assertChange(Change.Status.MERGED, expectedTopic, admin); + change2.assertChange(Change.Status.MERGED, expectedTopic, admin); + + repoA.git().fetch().call(); + RevWalk rwA = repoA.getRevWalk(); + RevCommit masterA = rwA.parseCommit(getRemoteHead(name("project-a"), "master")); + RevCommit change1Ps = parseCurrentRevision(rwA, change1); + assertThat(rwA.isMergedInto(change1Ps, masterA)).isTrue(); + + repoB.git().fetch().call(); + RevWalk rwB = repoB.getRevWalk(); + RevCommit masterB = rwB.parseCommit(getRemoteHead(name("project-b"), "master")); + RevCommit change2Ps = parseCurrentRevision(rwB, change2); + assertThat(rwB.isMergedInto(change2Ps, masterB)).isTrue(); + + assertThat(input.generateLockFailures).containsExactly(false); + } + + @Test + public void authorAndCommitDateAreEqual() throws Exception { + assume().that(getSubmitType()).isNotEqualTo(SubmitType.FAST_FORWARD_ONLY); + + ConfigInput ci = new ConfigInput(); + ci.matchAuthorToCommitterDate = InheritableBoolean.TRUE; + gApi.projects().name(project.get()).config(ci); + + RevCommit initialHead = getRemoteHead(); + testRepo.reset(initialHead); + PushOneCommit.Result change = createChange("Change 1", "b", "b"); + + testRepo.reset(initialHead); + PushOneCommit.Result change2 = createChange("Change 2", "c", "c"); + + if (getSubmitType() == SubmitType.MERGE_IF_NECESSARY + || getSubmitType() == SubmitType.REBASE_IF_NECESSARY) { + // Merge another change so that change2 is not a fast-forward + submit(change.getChangeId()); + } + + submit(change2.getChangeId()); + assertAuthorAndCommitDateEquals(getRemoteHead()); + } + private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Exception { for (PushOneCommit.Result change : changes) { try (BatchUpdate bu = - updateFactory.create(db, project, userFactory.create(admin.id), TimeUtil.nowTs())) { + batchUpdateFactory.create(db, project, userFactory.create(admin.id), TimeUtil.nowTs())) { bu.addOp( change.getChange().getId(), new BatchUpdateOp() { @@ -936,6 +1168,12 @@ assertThat(actual.getTimeZone()).isEqualTo(expected.getTimeZone()); } + protected void assertAuthorAndCommitDateEquals(RevCommit commit) { + assertThat(commit.getAuthorIdent().getWhen()).isEqualTo(commit.getCommitterIdent().getWhen()); + assertThat(commit.getAuthorIdent().getTimeZone()) + .isEqualTo(commit.getCommitterIdent().getTimeZone()); + } + protected void assertSubmitter(String changeId, int psId) throws Exception { assertSubmitter(changeId, psId, admin); }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java index 0250db1..b4d8557 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByMerge.java
@@ -18,11 +18,9 @@ import static com.google.common.truth.TruthJUnit.assume; import com.google.common.collect.Iterables; -import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestProjectInput; import com.google.gerrit.extensions.api.changes.SubmitInput; -import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.common.ChangeInfo; @@ -34,7 +32,6 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.transport.RefSpec; import org.junit.Test; public abstract class AbstractSubmitByMerge extends AbstractSubmit { @@ -130,6 +127,9 @@ @Test public void repairChangeStateAfterFailure() throws Exception { + // In NoteDb-only mode, repo and meta updates are atomic (at least in InMemoryRepository). + assume().that(notesMigration.disableChangeReviewDb()).isFalse(); + RevCommit initialHead = getRemoteHead(); PushOneCommit.Result change = createChange("Change 1", "a.txt", "content"); submit(change.getChangeId()); @@ -138,10 +138,11 @@ testRepo.reset(initialHead); PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content"); Change.Id id2 = change2.getChange().getId(); - SubmitInput failAfterRefUpdates = new TestSubmitInput(new SubmitInput(), true); + TestSubmitInput failInput = new TestSubmitInput(); + failInput.failAfterRefUpdates = true; submit( change2.getChangeId(), - failAfterRefUpdates, + failInput, ResourceConflictException.class, "Failing after ref updates"); @@ -177,75 +178,4 @@ assertThat(repo.exactRef("refs/heads/master").getObjectId()).isEqualTo(tip); } } - - @Test - public void submitWithCommitAndItsMergeCommitTogether() throws Exception { - assume().that(isSubmitWholeTopicEnabled()).isTrue(); - - RevCommit initialHead = getRemoteHead(); - - // Create a stable branch and bootstrap it. - gApi.projects().name(project.get()).branch("stable").create(new BranchInput()); - PushOneCommit push = - pushFactory.create(db, user.getIdent(), testRepo, "initial commit", "a.txt", "a"); - PushOneCommit.Result change = push.to("refs/heads/stable"); - - RevCommit stable = getRemoteHead(project, "stable"); - RevCommit master = getRemoteHead(project, "master"); - - assertThat(master).isEqualTo(initialHead); - assertThat(stable).isEqualTo(change.getCommit()); - - testRepo.git().fetch().call(); - testRepo.git().branchCreate().setName("stable").setStartPoint(stable).call(); - testRepo.git().branchCreate().setName("master").setStartPoint(master).call(); - - // Create a fix in stable branch. - testRepo.reset(stable); - RevCommit fix = - testRepo - .commit() - .parent(stable) - .message("small fix") - .add("b.txt", "b") - .insertChangeId() - .create(); - testRepo.branch("refs/heads/stable").update(fix); - testRepo - .git() - .push() - .setRefSpecs(new RefSpec("refs/heads/stable:refs/for/stable/" + name("topic"))) - .call(); - - // Merge the fix into master. - testRepo.reset(master); - RevCommit merge = - testRepo - .commit() - .parent(master) - .parent(fix) - .message("Merge stable into master") - .insertChangeId() - .create(); - testRepo.branch("refs/heads/master").update(merge); - testRepo - .git() - .push() - .setRefSpecs(new RefSpec("refs/heads/master:refs/for/master/" + name("topic"))) - .call(); - - // Submit together. - String fixId = GitUtil.getChangeId(testRepo, fix).get(); - String mergeId = GitUtil.getChangeId(testRepo, merge).get(); - approve(fixId); - approve(mergeId); - submit(mergeId); - assertMerged(fixId); - assertMerged(mergeId); - testRepo.git().fetch().call(); - RevWalk rw = testRepo.getRevWalk(); - master = rw.parseCommit(getRemoteHead(project, "master")); - assertThat(rw.isMergedInto(merge, master)).isTrue(); - assertThat(rw.isMergedInto(fix, master)).isTrue(); - } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java index d8aa35c..5dfc76d 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmitByRebase.java
@@ -15,6 +15,7 @@ package com.google.gerrit.acceptance.rest.change; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.acceptance.GitUtil.getChangeId; import static com.google.gerrit.acceptance.GitUtil.pushHead; import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; @@ -25,7 +26,6 @@ import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.acceptance.TestProjectInput; import com.google.gerrit.common.data.Permission; -import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.client.SubmitType; @@ -244,6 +244,9 @@ @Test public void repairChangeStateAfterFailure() throws Exception { + // In NoteDb-only mode, repo and meta updates are atomic (at least in InMemoryRepository). + assume().that(notesMigration.disableChangeReviewDb()).isFalse(); + RevCommit initialHead = getRemoteHead(); PushOneCommit.Result change = createChange("Change 1", "a.txt", "content"); submit(change.getChangeId()); @@ -252,10 +255,11 @@ testRepo.reset(initialHead); PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content"); Change.Id id2 = change2.getChange().getId(); - SubmitInput failAfterRefUpdates = new TestSubmitInput(new SubmitInput(), true); + TestSubmitInput failInput = new TestSubmitInput(); + failInput.failAfterRefUpdates = true; submit( change2.getChangeId(), - failAfterRefUpdates, + failInput, ResourceConflictException.class, "Failing after ref updates"); RevCommit headAfterFailedSubmit = getRemoteHead();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AssigneeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AssigneeIT.java index 35ba1a2..a905d38 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AssigneeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AssigneeIT.java
@@ -16,19 +16,26 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; import static java.util.concurrent.TimeUnit.SECONDS; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.Sandboxed; +import com.google.gerrit.common.data.Permission; import com.google.gerrit.extensions.api.changes.AssigneeInput; import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.common.AccountInfo; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.testutil.FakeEmailSender.Message; import com.google.gerrit.testutil.TestTimeUtil; import java.util.Iterator; import java.util.List; +import org.eclipse.jgit.transport.RefSpec; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -124,6 +131,43 @@ assertThat(deleteAssignee(r)).isNull(); } + @Test + @Sandboxed + public void setAssigneeToInactiveUser() throws Exception { + PushOneCommit.Result r = createChange(); + gApi.accounts().id(user.getId().get()).setActive(false); + exception.expect(UnprocessableEntityException.class); + exception.expectMessage("is not active"); + setAssignee(r, user.email); + } + + @Test + public void setAssigneeForNonVisibleChange() throws Exception { + git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call(); + testRepo.reset(RefNames.REFS_CONFIG); + PushOneCommit.Result r = createChange("refs/for/refs/meta/config"); + exception.expect(AuthException.class); + exception.expectMessage("read not permitted"); + setAssignee(r, user.email); + } + + @Test + public void setAssigneeNotAllowedWithoutPermission() throws Exception { + PushOneCommit.Result r = createChange(); + setApiUser(user); + exception.expect(AuthException.class); + exception.expectMessage("not permitted"); + setAssignee(r, user.email); + } + + @Test + public void setAssigneeAllowedWithPermission() throws Exception { + PushOneCommit.Result r = createChange(); + grant(project, "refs/heads/master", Permission.EDIT_ASSIGNEE, false, REGISTERED_USERS); + setApiUser(user); + assertThat(setAssignee(r, user.email)._accountId).isEqualTo(user.getId().get()); + } + private AccountInfo getAssignee(PushOneCommit.Result r) throws Exception { return gApi.changes().id(r.getChange().getId().get()).getAssignee(); }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java new file mode 100644 index 0000000..a2ad7fc --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeIdIT.java
@@ -0,0 +1,115 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.change; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.extensions.api.changes.ChangeApi; +import org.junit.Test; + +public class ChangeIdIT extends AbstractDaemonTest { + + @Test + public void projectChangeNumberReturnsChange() throws Exception { + PushOneCommit.Result c = createChange(); + RestResponse res = adminRestSession.get(changeDetail(getProjectChangeNumber(c.getChangeId()))); + res.assertOK(); + } + + @Test + public void wrongProjectChangeNumberReturnsNotFound() throws Exception { + PushOneCommit.Result c = createChange(); + RestResponse res1 = + adminRestSession.get( + changeDetail("unknown/project~" + getNumericChangeId(c.getChangeId()))); + res1.assertNotFound(); + + RestResponse res2 = adminRestSession.get(project.get() + "~" + Integer.MAX_VALUE); + res2.assertNotFound(); + + // Try a non-numeric change number + RestResponse res3 = adminRestSession.get(project.get() + "~some-id"); + res3.assertNotFound(); + } + + @Test + public void changeNumberReturnsChange() throws Exception { + PushOneCommit.Result c = createChange(); + RestResponse res = adminRestSession.get(changeDetail(getNumericChangeId(c.getChangeId()))); + res.assertOK(); + } + + @Test + public void wrongChangeNumberReturnsNotFound() throws Exception { + RestResponse res = adminRestSession.get(changeDetail(String.valueOf(Integer.MAX_VALUE))); + res.assertNotFound(); + } + + @Test + public void tripletChangeIdReturnsChange() throws Exception { + PushOneCommit.Result c = createChange(); + RestResponse res = adminRestSession.get(changeDetail(getTriplet(c.getChangeId()))); + res.assertOK(); + } + + @Test + public void wrongTripletChangeIdReturnsNotFound() throws Exception { + PushOneCommit.Result c = createChange(); + RestResponse res1 = adminRestSession.get(changeDetail("unknown~master~" + c.getChangeId())); + res1.assertNotFound(); + + RestResponse res2 = + adminRestSession.get(changeDetail(project.get() + "~unknown~" + c.getChangeId())); + res2.assertNotFound(); + + RestResponse res3 = adminRestSession.get(changeDetail(project.get() + "~master~I1234567890")); + res3.assertNotFound(); + } + + @Test + public void changeIdReturnsChange() throws Exception { + PushOneCommit.Result c = createChange(); + RestResponse res = adminRestSession.get(changeDetail(c.getChangeId())); + res.assertOK(); + } + + @Test + public void wrongChangeIdReturnsNotFound() throws Exception { + RestResponse res = adminRestSession.get(changeDetail("I1234567890")); + res.assertNotFound(); + } + + private static String changeDetail(String changeId) { + return "/changes/" + changeId + "/detail"; + } + + /** Convert a changeId (I0...01) to project~changeNumber (project~00001) */ + private String getProjectChangeNumber(String changeId) throws Exception { + ChangeApi cApi = gApi.changes().id(changeId); + return cApi.get().project + "~" + cApi.get()._number; + } + + /** Convert a changeId (I0...01) to a triplet (project~branch~I0...01) */ + private String getTriplet(String changeId) throws Exception { + ChangeApi cApi = gApi.changes().id(changeId); + return cApi.get().project + "~" + cApi.get().branch + "~" + changeId; + } + + /** Convert a changeId (I0...01) to a numeric changeId (00001) */ + private String getNumericChangeId(String changeId) throws Exception { + return Integer.toString(gApi.changes().id(changeId).get()._number); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java index 0710e6c..2c238f0 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeOwnerIT.java
@@ -33,7 +33,7 @@ @Before public void setUp() throws Exception { setApiUser(user); - user2 = accounts.user2(); + user2 = accountCreator.user2(); } @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java new file mode 100644 index 0000000..0b004ec --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersByEmailIT.java
@@ -0,0 +1,346 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.change; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; + +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.NoHttpd; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.extensions.api.changes.AddReviewerInput; +import com.google.gerrit.extensions.api.changes.AddReviewerResult; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.api.projects.ConfigInput; +import com.google.gerrit.extensions.client.InheritableBoolean; +import com.google.gerrit.extensions.client.ListChangesOption; +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.server.mail.Address; +import com.google.gerrit.testutil.FakeEmailSender.Message; +import java.util.EnumSet; +import java.util.List; +import org.junit.Before; +import org.junit.Test; + +@NoHttpd +public class ChangeReviewersByEmailIT extends AbstractDaemonTest { + + @Before + public void setUp() throws Exception { + ConfigInput conf = new ConfigInput(); + conf.enableReviewerByEmail = InheritableBoolean.TRUE; + gApi.projects().name(project.get()).config(conf); + } + + @Test + public void addByEmail() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com"); + + for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) { + PushOneCommit.Result r = createChange(); + + AddReviewerInput input = new AddReviewerInput(); + input.reviewer = toRfcAddressString(acc); + input.state = state; + gApi.changes().id(r.getChangeId()).addReviewer(input); + + ChangeInfo info = + gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS)); + assertThat(info.reviewers).isEqualTo(ImmutableMap.of(state, ImmutableList.of(acc))); + // All reviewers added by email should be removable + assertThat(info.removableReviewers).isEqualTo(ImmutableList.of(acc)); + } + } + + @Test + public void addByEmailAndById() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + AccountInfo byEmail = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com"); + AccountInfo byId = new AccountInfo(user.id.get()); + + for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) { + PushOneCommit.Result r = createChange(); + + AddReviewerInput inputByEmail = new AddReviewerInput(); + inputByEmail.reviewer = toRfcAddressString(byEmail); + inputByEmail.state = state; + gApi.changes().id(r.getChangeId()).addReviewer(inputByEmail); + + AddReviewerInput inputById = new AddReviewerInput(); + inputById.reviewer = user.email; + inputById.state = state; + gApi.changes().id(r.getChangeId()).addReviewer(inputById); + + ChangeInfo info = + gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS)); + assertThat(info.reviewers).isEqualTo(ImmutableMap.of(state, ImmutableList.of(byId, byEmail))); + // All reviewers (both by id and by email) should be removable + assertThat(info.removableReviewers).isEqualTo(ImmutableList.of(byId, byEmail)); + } + } + + @Test + public void removeByEmail() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com"); + + for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) { + PushOneCommit.Result r = createChange(); + + AddReviewerInput addInput = new AddReviewerInput(); + addInput.reviewer = toRfcAddressString(acc); + addInput.state = state; + gApi.changes().id(r.getChangeId()).addReviewer(addInput); + + gApi.changes().id(r.getChangeId()).reviewer(acc.email).remove(); + + ChangeInfo info = + gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS)); + assertThat(info.reviewers).isEmpty(); + } + } + + @Test + public void convertFromCCToReviewer() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com"); + + PushOneCommit.Result r = createChange(); + + AddReviewerInput addInput = new AddReviewerInput(); + addInput.reviewer = toRfcAddressString(acc); + addInput.state = ReviewerState.CC; + gApi.changes().id(r.getChangeId()).addReviewer(addInput); + + AddReviewerInput modifyInput = new AddReviewerInput(); + modifyInput.reviewer = addInput.reviewer; + modifyInput.state = ReviewerState.REVIEWER; + gApi.changes().id(r.getChangeId()).addReviewer(modifyInput); + + ChangeInfo info = + gApi.changes().id(r.getChangeId()).get(EnumSet.of(ListChangesOption.DETAILED_LABELS)); + assertThat(info.reviewers) + .isEqualTo(ImmutableMap.of(ReviewerState.REVIEWER, ImmutableList.of(acc))); + } + + @Test + public void addedReviewersGetNotified() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com"); + + for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) { + PushOneCommit.Result r = createChange(); + + AddReviewerInput input = new AddReviewerInput(); + input.reviewer = toRfcAddressString(acc); + input.state = state; + gApi.changes().id(r.getChangeId()).addReviewer(input); + + List<Message> messages = sender.getMessages(); + assertThat(messages).hasSize(1); + assertThat(messages.get(0).rcpt()).containsExactly(Address.parse(input.reviewer)); + sender.clear(); + } + } + + @Test + public void removingReviewerTriggersNotification() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com"); + + for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) { + PushOneCommit.Result r = createChange(); + + AddReviewerInput addInput = new AddReviewerInput(); + addInput.reviewer = toRfcAddressString(acc); + addInput.state = state; + gApi.changes().id(r.getChangeId()).addReviewer(addInput); + + // Review change as user + ReviewInput reviewInput = new ReviewInput(); + reviewInput.message = "I have a comment"; + setApiUser(user); + revision(r).review(reviewInput); + setApiUser(admin); + + sender.clear(); + + // Delete as admin + gApi.changes().id(r.getChangeId()).reviewer(addInput.reviewer).remove(); + + List<Message> messages = sender.getMessages(); + assertThat(messages).hasSize(1); + assertThat(messages.get(0).rcpt()) + .containsExactly(Address.parse(addInput.reviewer), user.emailAddress); + sender.clear(); + } + } + + @Test + public void reviewerAndCCReceiveRegularNotification() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com"); + + for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) { + PushOneCommit.Result r = createChange(); + + AddReviewerInput input = new AddReviewerInput(); + input.reviewer = toRfcAddressString(acc); + input.state = state; + gApi.changes().id(r.getChangeId()).addReviewer(input); + sender.clear(); + + gApi.changes() + .id(r.getChangeId()) + .revision(r.getCommit().name()) + .review(ReviewInput.approve()); + + assertNotifyCc(Address.parse(input.reviewer)); + } + } + + @Test + public void reviewerAndCCReceiveSameEmail() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + PushOneCommit.Result r = createChange(); + for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) { + for (int i = 0; i < 10; i++) { + AddReviewerInput input = new AddReviewerInput(); + input.reviewer = String.format("%s-%s@gerritcodereview.com", state, i); + input.state = state; + gApi.changes().id(r.getChangeId()).addReviewer(input); + } + } + + // Also add user as a regular reviewer + AddReviewerInput input = new AddReviewerInput(); + input.reviewer = user.email; + input.state = ReviewerState.REVIEWER; + gApi.changes().id(r.getChangeId()).addReviewer(input); + + sender.clear(); + gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(ReviewInput.approve()); + // Assert that only one email was sent out to everyone + assertThat(sender.getMessages()).hasSize(1); + } + + @Test + public void addingMultipleReviewersAndCCsAtOnceSendsOnlyOneEmail() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + PushOneCommit.Result r = createChange(); + 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); + } + } + assertThat(reviewInput.reviewers).hasSize(20); + + sender.clear(); + gApi.changes().id(r.getChangeId()).revision(r.getCommit().name()).review(reviewInput); + assertThat(sender.getMessages()).hasSize(1); + } + + @Test + public void rejectMissingEmail() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + PushOneCommit.Result r = createChange(); + + AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer(""); + assertThat(result.error).isEqualTo(" is not a valid user identifier"); + assertThat(result.reviewers).isNull(); + } + + @Test + public void rejectMalformedEmail() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + PushOneCommit.Result r = createChange(); + + AddReviewerResult result = gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@"); + assertThat(result.error).isEqualTo("Foo Bar <foo.bar@ is not a valid user identifier"); + assertThat(result.reviewers).isNull(); + } + + @Test + public void rejectOnNonPublicChange() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + PushOneCommit.Result r = createDraftChange(); + + AddReviewerResult result = + gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@gerritcodereview.com>"); + assertThat(result.error) + .isEqualTo( + "Foo Bar <foo.bar@gerritcodereview.com> does not have permission to see this change"); + assertThat(result.reviewers).isNull(); + } + + @Test + public void rejectWhenFeatureIsDisabled() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + + ConfigInput conf = new ConfigInput(); + conf.enableReviewerByEmail = InheritableBoolean.FALSE; + gApi.projects().name(project.get()).config(conf); + + PushOneCommit.Result r = createChange(); + + AddReviewerResult result = + gApi.changes().id(r.getChangeId()).addReviewer("Foo Bar <foo.bar@gerritcodereview.com>"); + assertThat(result.error) + .isEqualTo( + "Foo Bar <foo.bar@gerritcodereview.com> does not identify a registered user or group"); + assertThat(result.reviewers).isNull(); + } + + @Test + public void reviewersByEmailAreServedFromIndex() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + AccountInfo acc = new AccountInfo("Foo Bar", "foo.bar@gerritcodereview.com"); + + for (ReviewerState state : ImmutableList.of(ReviewerState.CC, ReviewerState.REVIEWER)) { + PushOneCommit.Result r = createChange(); + + AddReviewerInput input = new AddReviewerInput(); + input.reviewer = toRfcAddressString(acc); + input.state = state; + gApi.changes().id(r.getChangeId()).addReviewer(input); + + notesMigration.setFailOnLoad(true); + try { + ChangeInfo info = + Iterables.getOnlyElement( + gApi.changes() + .query(r.getChangeId()) + .withOption(ListChangesOption.DETAILED_LABELS) + .get()); + assertThat(info.reviewers).isEqualTo(ImmutableMap.of(state, ImmutableList.of(acc))); + } finally { + notesMigration.setFailOnLoad(false); + } + } + } + + private static String toRfcAddressString(AccountInfo info) { + return (new Address(info.name, info.email)).toString(); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java index 66966c3..c7c02b2 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -16,18 +16,24 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; +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 static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_OK; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.RestResponse; import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.extensions.api.changes.AddReviewerInput; import com.google.gerrit.extensions.api.changes.AddReviewerResult; +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.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.ReviewResult; import com.google.gerrit.extensions.client.ReviewerState; @@ -42,6 +48,7 @@ import com.google.gson.stream.JsonReader; import java.util.ArrayList; import java.util.Collection; +import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -192,7 +199,7 @@ // CC a group that overlaps with some existing reviewers and CCed accounts. TestAccount reviewer = - accounts.create(name("reviewer"), "addCcGroup-reviewer@example.com", "Reviewer"); + accountCreator.create(name("reviewer"), "addCcGroup-reviewer@example.com", "Reviewer"); result = addReviewer(changeId, reviewer.username); assertThat(result.error).isNull(); sender.clear(); @@ -418,7 +425,7 @@ @Test public void reviewAndAddReviewers() throws Exception { - TestAccount observer = accounts.user2(); + TestAccount observer = accountCreator.user2(); PushOneCommit.Result r = createChange(); ReviewInput input = ReviewInput.approve().reviewer(user.email).reviewer(observer.email, CC, false); @@ -473,7 +480,7 @@ .id(mediumGroup) .addMembers(usernames.subList(0, mediumGroupSize).toArray(new String[mediumGroupSize])); - TestAccount observer = accounts.user2(); + TestAccount observer = accountCreator.user2(); PushOneCommit.Result r = createChange(); // Attempt to add overly large group as reviewers. @@ -603,9 +610,12 @@ @Test public void addOverlappingGroups() throws Exception { String emailPrefix = "addOverlappingGroups-"; - TestAccount user1 = accounts.create(name("user1"), emailPrefix + "user1@example.com", "User1"); - TestAccount user2 = accounts.create(name("user2"), emailPrefix + "user2@example.com", "User2"); - TestAccount user3 = accounts.create(name("user3"), emailPrefix + "user3@example.com", "User3"); + TestAccount user1 = + accountCreator.create(name("user1"), emailPrefix + "user1@example.com", "User1"); + TestAccount user2 = + accountCreator.create(name("user2"), emailPrefix + "user2@example.com", "User2"); + TestAccount user3 = + accountCreator.create(name("user3"), emailPrefix + "user3@example.com", "User3"); String group1 = createGroup("group1"); String group2 = createGroup("group2"); gApi.groups().id(group1).addMembers(user1.username, user2.username); @@ -655,6 +665,64 @@ assertThat(reviewerResult.ccs).hasSize(1); } + @Test + public void removingReviewerRemovesTheirVote() throws Exception { + String crLabel = "Code-Review"; + PushOneCommit.Result r = createChange(); + ReviewInput input = ReviewInput.approve().reviewer(admin.email); + ReviewResult addResult = review(r.getChangeId(), r.getCommit().name(), input); + assertThat(addResult.reviewers).isNotNull(); + assertThat(addResult.reviewers).hasSize(1); + + Map<String, LabelInfo> changeLabels = getChangeLabels(r.getChangeId()); + assertThat(changeLabels.get(crLabel).all).hasSize(1); + + RestResponse deleteResult = deleteReviewer(r.getChangeId(), admin); + deleteResult.assertNoContent(); + + changeLabels = getChangeLabels(r.getChangeId()); + assertThat(changeLabels.get(crLabel).all).isNull(); + + // Check that the vote is gone even after the reviewer is added back + addReviewer(r.getChangeId(), admin.email); + changeLabels = getChangeLabels(r.getChangeId()); + assertThat(changeLabels.get(crLabel).all).isNull(); + } + + @Test + public void notifyDetailsWorkOnPostReview() throws Exception { + PushOneCommit.Result r = createChange(); + TestAccount userToNotify = createAccounts(1, "notify-details-post-review").get(0); + + ReviewInput reviewInput = new ReviewInput(); + reviewInput.reviewer(user.email, ReviewerState.REVIEWER, true); + reviewInput.notify = NotifyHandling.NONE; + reviewInput.notifyDetails = + ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email))); + + sender.clear(); + gApi.changes().id(r.getChangeId()).current().review(reviewInput); + assertThat(sender.getMessages()).hasSize(1); + assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.emailAddress); + } + + @Test + public void notifyDetailsWorkOnPostReviewers() throws Exception { + PushOneCommit.Result r = createChange(); + TestAccount userToNotify = createAccounts(1, "notify-details-post-reviewers").get(0); + + AddReviewerInput addReviewer = new AddReviewerInput(); + addReviewer.reviewer = user.email; + addReviewer.notify = NotifyHandling.NONE; + addReviewer.notifyDetails = + ImmutableMap.of(RecipientType.TO, new NotifyInfo(ImmutableList.of(userToNotify.email))); + + sender.clear(); + gApi.changes().id(r.getChangeId()).addReviewer(addReviewer); + assertThat(sender.getMessages()).hasSize(1); + assertThat(sender.getMessages().get(0).rcpt()).containsExactly(userToNotify.emailAddress); + } + private AddReviewerResult addReviewer(String changeId, String reviewer) throws Exception { return addReviewer(changeId, reviewer, SC_OK); } @@ -731,8 +799,13 @@ List<TestAccount> result = new ArrayList<>(n); for (int i = 0; i < n; i++) { result.add( - accounts.create(name("u" + i), emailPrefix + "-" + i + "@example.com", "Full Name " + i)); + accountCreator.create( + name("u" + i), emailPrefix + "-" + i + "@example.com", "Full Name " + i)); } return result; } + + private Map<String, LabelInfo> getChangeLabels(String changeId) throws Exception { + return gApi.changes().id(changeId).get(EnumSet.of(DETAILED_LABELS)).labels; + } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CorsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CorsIT.java index 4f2d2bd..bd9c98d 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CorsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CorsIT.java
@@ -18,18 +18,36 @@ import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; +import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_MAX_AGE; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD; +import static com.google.common.net.HttpHeaders.AUTHORIZATION; +import static com.google.common.net.HttpHeaders.CONTENT_TYPE; import static com.google.common.net.HttpHeaders.ORIGIN; +import static com.google.common.net.HttpHeaders.VARY; import static com.google.common.truth.Truth.assertThat; +import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.truth.StringSubject; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.PushOneCommit.Result; import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.server.UrlEncoded; import com.google.gerrit.testutil.ConfigSuite; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.stream.Stream; import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.fluent.Executor; import org.apache.http.client.fluent.Request; +import org.apache.http.cookie.Cookie; +import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.message.BasicHeader; import org.eclipse.jgit.lib.Config; import org.junit.Test; @@ -38,6 +56,7 @@ @ConfigSuite.Default public static Config allowExampleDotCom() { Config cfg = new Config(); + cfg.setString("auth", null, "type", "DEVELOPMENT_BECOME_ANY_ACCOUNT"); cfg.setStringList( "site", null, @@ -47,14 +66,29 @@ } @Test - public void origin() throws Exception { + public void missingOriginIsAllowedWithNoCorsResponseHeaders() throws Exception { Result change = createChange(); - String url = "/changes/" + change.getChangeId() + "/detail"; RestResponse r = adminRestSession.get(url); r.assertOK(); - assertThat(r.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isNull(); - assertThat(r.getHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS)).isNull(); + + String allowOrigin = r.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN); + String allowCred = r.getHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS); + String maxAge = r.getHeader(ACCESS_CONTROL_MAX_AGE); + String allowMethods = r.getHeader(ACCESS_CONTROL_ALLOW_METHODS); + String allowHeaders = r.getHeader(ACCESS_CONTROL_ALLOW_HEADERS); + + assertThat(allowOrigin).named(ACCESS_CONTROL_ALLOW_ORIGIN).isNull(); + assertThat(allowCred).named(ACCESS_CONTROL_ALLOW_CREDENTIALS).isNull(); + assertThat(maxAge).named(ACCESS_CONTROL_MAX_AGE).isNull(); + assertThat(allowMethods).named(ACCESS_CONTROL_ALLOW_METHODS).isNull(); + assertThat(allowHeaders).named(ACCESS_CONTROL_ALLOW_HEADERS).isNull(); + } + + @Test + public void origins() throws Exception { + Result change = createChange(); + String url = "/changes/" + change.getChangeId() + "/detail"; check(url, true, "http://example.com"); check(url, true, "https://sub.example.com"); @@ -65,14 +99,26 @@ } @Test - public void putWithOriginRefused() throws Exception { + public void putWithServerOriginAcceptedWithNoCorsResponseHeaders() throws Exception { + Result change = createChange(); + String origin = adminRestSession.url(); + RestResponse r = + adminRestSession.putWithHeader( + "/changes/" + change.getChangeId() + "/topic", new BasicHeader(ORIGIN, origin), "A"); + r.assertOK(); + checkCors(r, false, origin); + checkTopic(change, "A"); + } + + @Test + public void putWithOtherOriginAccepted() throws Exception { Result change = createChange(); String origin = "http://example.com"; RestResponse r = adminRestSession.putWithHeader( "/changes/" + change.getChangeId() + "/topic", new BasicHeader(ORIGIN, origin), "A"); r.assertOK(); - checkCors(r, false, origin); + checkCors(r, true, origin); } @Test @@ -88,71 +134,153 @@ RestResponse res = adminRestSession.execute(req); res.assertOK(); + + String vary = res.getHeader(VARY); + assertThat(vary).named(VARY).isNotNull(); + assertThat(Splitter.on(", ").splitToList(vary)) + .containsExactly(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); checkCors(res, true, origin); } @Test public void preflightBadOrigin() throws Exception { Result change = createChange(); - Request req = Request.Options(adminRestSession.url() + "/a/changes/" + change.getChangeId() + "/detail"); req.addHeader(ORIGIN, "http://evil.attacker"); req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, "GET"); - adminRestSession.execute(req).assertBadRequest(); } @Test public void preflightBadMethod() throws Exception { Result change = createChange(); - - for (String method : new String[] {"POST", "PUT", "DELETE", "PATCH"}) { - Request req = - Request.Options( - adminRestSession.url() + "/a/changes/" + change.getChangeId() + "/detail"); - req.addHeader(ORIGIN, "http://example.com"); - req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, method); - adminRestSession.execute(req).assertBadRequest(); - } + Request req = + Request.Options(adminRestSession.url() + "/a/changes/" + change.getChangeId() + "/detail"); + req.addHeader(ORIGIN, "http://example.com"); + req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, "CALL"); + adminRestSession.execute(req).assertBadRequest(); } @Test public void preflightBadHeader() throws Exception { Result change = createChange(); - Request req = Request.Options(adminRestSession.url() + "/a/changes/" + change.getChangeId() + "/detail"); req.addHeader(ORIGIN, "http://example.com"); req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, "GET"); - req.addHeader(ACCESS_CONTROL_REQUEST_HEADERS, "X-Gerrit-Auth"); - + req.addHeader(ACCESS_CONTROL_REQUEST_HEADERS, "X-Secret-Auth-Token"); adminRestSession.execute(req).assertBadRequest(); } - private RestResponse check(String url, boolean accept, String origin) throws Exception { + @Test + public void crossDomainPutTopic() throws Exception { + Result change = createChange(); + BasicCookieStore cookies = new BasicCookieStore(); + Executor http = Executor.newInstance().cookieStore(cookies); + + Request req = Request.Get(canonicalWebUrl.get() + "/login/?account_id=" + admin.id.get()); + HttpResponse r = http.execute(req).returnResponse(); + String auth = null; + for (Cookie c : cookies.getCookies()) { + if ("GerritAccount".equals(c.getName())) { + auth = c.getValue(); + } + } + assertThat(auth).named("GerritAccount cookie").isNotNull(); + cookies.clear(); + + UrlEncoded url = + new UrlEncoded(canonicalWebUrl.get() + "/changes/" + change.getChangeId() + "/topic"); + url.put("$m", "PUT"); + url.put("$ct", "application/json; charset=US-ASCII"); + url.put("access_token", auth); + + String origin = "http://example.com"; + req = Request.Post(url.toString()); + req.setHeader(CONTENT_TYPE, "text/plain"); + req.setHeader(ORIGIN, origin); + req.bodyByteArray("{\"topic\":\"test-xd\"}".getBytes(StandardCharsets.US_ASCII)); + + r = http.execute(req).returnResponse(); + assertThat(r.getStatusLine().getStatusCode()).isEqualTo(200); + + Header vary = r.getFirstHeader(VARY); + assertThat(vary).named(VARY).isNotNull(); + assertThat(Splitter.on(", ").splitToList(vary.getValue())).named(VARY).contains(ORIGIN); + + Header allowOrigin = r.getFirstHeader(ACCESS_CONTROL_ALLOW_ORIGIN); + assertThat(allowOrigin).named(ACCESS_CONTROL_ALLOW_ORIGIN).isNotNull(); + assertThat(allowOrigin.getValue()).named(ACCESS_CONTROL_ALLOW_ORIGIN).isEqualTo(origin); + checkTopic(change, "test-xd"); + } + + @Test + public void crossDomainRejectsBadOrigin() throws Exception { + Result change = createChange(); + UrlEncoded url = + new UrlEncoded(canonicalWebUrl.get() + "/changes/" + change.getChangeId() + "/topic"); + url.put("$m", "PUT"); + url.put("$ct", "application/json; charset=US-ASCII"); + + Request req = Request.Post(url.toString()); + req.setHeader(CONTENT_TYPE, "text/plain"); + req.setHeader(ORIGIN, "http://evil.attacker"); + req.bodyByteArray("{\"topic\":\"test-xd\"}".getBytes(StandardCharsets.US_ASCII)); + adminRestSession.execute(req).assertBadRequest(); + checkTopic(change, null); + } + + private void checkTopic(Result change, @Nullable String topic) throws RestApiException { + ChangeInfo info = gApi.changes().id(change.getChangeId()).get(); + StringSubject t = assertThat(info.topic).named("topic"); + if (topic != null) { + t.isEqualTo(topic); + } else { + t.isNull(); + } + } + + private void check(String url, boolean accept, String origin) throws Exception { Header hdr = new BasicHeader(ORIGIN, origin); RestResponse r = adminRestSession.getWithHeader(url, hdr); r.assertOK(); checkCors(r, accept, origin); - return r; } private void checkCors(RestResponse r, boolean accept, String origin) { + String vary = r.getHeader(VARY); + assertThat(vary).named(VARY).isNotNull(); + assertThat(Splitter.on(", ").splitToList(vary)).named(VARY).contains(ORIGIN); + String allowOrigin = r.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN); String allowCred = r.getHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS); + String maxAge = r.getHeader(ACCESS_CONTROL_MAX_AGE); String allowMethods = r.getHeader(ACCESS_CONTROL_ALLOW_METHODS); String allowHeaders = r.getHeader(ACCESS_CONTROL_ALLOW_HEADERS); if (accept) { - assertThat(allowOrigin).isEqualTo(origin); - assertThat(allowCred).isEqualTo("true"); - assertThat(allowMethods).isEqualTo("GET, OPTIONS"); - assertThat(allowHeaders).isEqualTo("X-Requested-With"); + assertThat(allowOrigin).named(ACCESS_CONTROL_ALLOW_ORIGIN).isEqualTo(origin); + assertThat(allowCred).named(ACCESS_CONTROL_ALLOW_CREDENTIALS).isEqualTo("true"); + assertThat(maxAge).named(ACCESS_CONTROL_MAX_AGE).isEqualTo("600"); + + assertThat(allowMethods).named(ACCESS_CONTROL_ALLOW_METHODS).isNotNull(); + assertThat(Splitter.on(", ").splitToList(allowMethods)) + .named(ACCESS_CONTROL_ALLOW_METHODS) + .containsExactly("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"); + + assertThat(allowHeaders).named(ACCESS_CONTROL_ALLOW_HEADERS).isNotNull(); + assertThat(Splitter.on(", ").splitToList(allowHeaders)) + .named(ACCESS_CONTROL_ALLOW_HEADERS) + .containsExactlyElementsIn( + Stream.of(AUTHORIZATION, CONTENT_TYPE, "X-Gerrit-Auth", "X-Requested-With") + .map(s -> s.toLowerCase(Locale.US)) + .collect(ImmutableSet.toImmutableSet())); } else { - assertThat(allowOrigin).isNull(); - assertThat(allowCred).isNull(); - assertThat(allowMethods).isNull(); - assertThat(allowHeaders).isNull(); + assertThat(allowOrigin).named(ACCESS_CONTROL_ALLOW_ORIGIN).isNull(); + assertThat(allowCred).named(ACCESS_CONTROL_ALLOW_CREDENTIALS).isNull(); + assertThat(maxAge).named(ACCESS_CONTROL_MAX_AGE).isNull(); + assertThat(allowMethods).named(ACCESS_CONTROL_ALLOW_METHODS).isNull(); + assertThat(allowHeaders).named(ACCESS_CONTROL_ALLOW_HEADERS).isNull(); } } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java index f79b5fa..43d02a6 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -16,11 +16,14 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.TruthJUnit.assume; +import static com.google.gerrit.common.data.Permission.READ; import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; import static java.util.concurrent.TimeUnit.SECONDS; import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.PushOneCommit; @@ -30,14 +33,20 @@ import com.google.gerrit.extensions.api.changes.CherryPickInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeInput; +import com.google.gerrit.extensions.common.ChangeMessageInfo; +import com.google.gerrit.extensions.common.CommitInfo; import com.google.gerrit.extensions.common.MergeInput; +import com.google.gerrit.extensions.common.RevisionInfo; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.config.AnonymousCowardNameProvider; @@ -45,7 +54,9 @@ import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.FakeEmailSender.Message; import com.google.gerrit.testutil.TestTimeUtil; +import java.util.Iterator; import java.util.List; +import java.util.Map; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; @@ -102,7 +113,7 @@ @Test public void notificationsOnChangeCreation() throws Exception { setApiUser(user); - watch(project.get(), null); + watch(project.get()); // check that watcher is notified setApiUser(admin); @@ -148,6 +159,43 @@ } @Test + public void createNewPrivateChange() throws Exception { + ChangeInput input = newChangeInput(ChangeStatus.NEW); + input.isPrivate = true; + assertCreateSucceeds(input); + } + + @Test + public void createNewWorkInProgressChange() throws Exception { + ChangeInput input = newChangeInput(ChangeStatus.NEW); + input.workInProgress = true; + assertCreateSucceeds(input); + } + + @Test + public void createChangeWithoutAccessToParentCommitFails() throws Exception { + Map<String, PushOneCommit.Result> results = + changeInTwoBranches("invisible-branch", "a.txt", "visible-branch", "b.txt"); + block(project, "refs/heads/invisible-branch", READ, REGISTERED_USERS); + + ChangeInput in = newChangeInput(ChangeStatus.NEW); + in.branch = "visible-branch"; + in.baseChange = results.get("invisible-branch").getChangeId(); + assertCreateFails( + in, UnprocessableEntityException.class, "Base change not found: " + in.baseChange); + } + + @Test + public void createChangeOnInvisibleBranchFails() throws Exception { + changeInTwoBranches("invisible-branch", "a.txt", "branchB", "b.txt"); + block(project, "refs/heads/invisible-branch", READ, REGISTERED_USERS); + + ChangeInput in = newChangeInput(ChangeStatus.NEW); + in.branch = "invisible-branch"; + assertCreateFails(in, ResourceNotFoundException.class, ""); + } + + @Test public void noteDbCommit() throws Exception { assume().that(notesMigration.readChanges()).isTrue(); @@ -278,6 +326,60 @@ assertCreateSucceeds(in); } + @Test + public void cherryPickCommitWithoutChangeId() throws Exception { + // This test is a little superfluous, since the current cherry-pick code ignores + // the commit message of the to-be-cherry-picked change, using the one in + // CherryPickInput instead. + CherryPickInput input = new CherryPickInput(); + input.destination = "foo"; + input.message = "it goes to foo branch"; + gApi.projects().name(project.get()).branch(input.destination).create(new BranchInput()); + + RevCommit revCommit = createNewCommitWithoutChangeId("refs/heads/master", "a.txt", "content"); + ChangeInfo changeInfo = + gApi.projects().name(project.get()).commit(revCommit.getName()).cherryPick(input).get(); + + assertThat(changeInfo.messages).hasSize(1); + Iterator<ChangeMessageInfo> messageIterator = changeInfo.messages.iterator(); + String expectedMessage = + String.format("Patch Set 1: Cherry Picked from commit %s.", revCommit.getName()); + assertThat(messageIterator.next().message).isEqualTo(expectedMessage); + + RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision); + assertThat(revInfo).isNotNull(); + CommitInfo commitInfo = revInfo.commit; + assertThat(commitInfo.message) + .isEqualTo(input.message + "\n\nChange-Id: " + changeInfo.changeId + "\n"); + } + + @Test + public void cherryPickCommitWithChangeId() throws Exception { + CherryPickInput input = new CherryPickInput(); + input.destination = "foo"; + + RevCommit revCommit = createChange().getCommit(); + List<String> footers = revCommit.getFooterLines("Change-Id"); + assertThat(footers).hasSize(1); + String changeId = footers.get(0); + + input.message = "it goes to foo branch\n\nChange-Id: " + changeId; + gApi.projects().name(project.get()).branch(input.destination).create(new BranchInput()); + + ChangeInfo changeInfo = + gApi.projects().name(project.get()).commit(revCommit.getName()).cherryPick(input).get(); + + assertThat(changeInfo.messages).hasSize(1); + Iterator<ChangeMessageInfo> messageIterator = changeInfo.messages.iterator(); + String expectedMessage = + String.format("Patch Set 1: Cherry Picked from commit %s.", revCommit.getName()); + assertThat(messageIterator.next().message).isEqualTo(expectedMessage); + + RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision); + assertThat(revInfo).isNotNull(); + assertThat(revInfo.commit.message).isEqualTo(input.message + "\n"); + } + private ChangeInput newChangeInput(ChangeStatus status) { ChangeInput in = new ChangeInput(); in.project = project.get(); @@ -295,6 +397,8 @@ assertThat(out.subject).isEqualTo(in.subject); assertThat(out.topic).isEqualTo(in.topic); assertThat(out.status).isEqualTo(in.status); + assertThat(out.isPrivate).isEqualTo(in.isPrivate); + assertThat(out.workInProgress).isEqualTo(in.workInProgress); assertThat(out.revisions).hasSize(1); assertThat(out.submitted).isNull(); Boolean draft = Iterables.getOnlyElement(out.revisions.values()).draft; @@ -347,8 +451,18 @@ return in; } - private void changeInTwoBranches(String branchA, String fileA, String branchB, String fileB) - throws Exception { + /** + * Create an empty commit in master, two new branches with one commit each. + * + * @param branchA name of first branch to create + * @param fileA name of file to commit to branchA + * @param branchB name of second branch to create + * @param fileB name of file to commit to branchB + * @return A {@code Map} of branchName => commit result. + * @throws Exception + */ + private Map<String, Result> changeInTwoBranches( + String branchA, String fileA, String branchB, String fileB) throws Exception { // create a initial commit in master Result initialCommit = pushFactory @@ -373,5 +487,7 @@ commitB.setParent(initialCommit.getCommit()); Result changeB = commitB.to("refs/heads/" + branchB); changeB.assertOkStatus(); + + return ImmutableMap.of("master", initialCommit, branchA, changeA, branchB, changeB); } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java index 11ff612..3eee3a4 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteDraftPatchSetIT.java
@@ -92,7 +92,7 @@ din.message = "comment on a.txt"; gApi.changes().id(changeId).current().createDraft(din); - if (notesMigration.writeChanges()) { + if (notesMigration.commitChangeWrites()) { assertThat(getDraftRef(admin, id)).isNotNull(); } @@ -110,7 +110,7 @@ deletePatchSet(changeId, ps); assertThat(queryProvider.get().byKeyPrefix(changeId)).isEmpty(); - if (notesMigration.writeChanges()) { + if (notesMigration.commitChangeWrites()) { assertThat(getDraftRef(admin, id)).isNull(); assertThat(getMetaRef(id)).isNull(); }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java index da7d7b5..904e21e 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
@@ -45,7 +45,6 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.testutil.ConfigSuite; -import com.google.inject.Inject; import java.util.Collection; import java.util.EnumSet; import java.util.List; @@ -58,8 +57,6 @@ return allowDraftsDisabledConfig(); } - @Inject private BatchUpdate.Factory updateFactory; - @Test public void deleteDraftChange() throws Exception { assume().that(isAllowDrafts()).isTrue(); @@ -82,14 +79,13 @@ PushOneCommit.Result changeResult = createDraftChange(); changeResult.assertOkStatus(); String changeId = changeResult.getChangeId(); - Change.Id id = changeResult.getChange().getId(); // The user needs to be able to see the draft change (which reviewers can). gApi.changes().id(changeId).addReviewer(user.fullName); setApiUser(user); exception.expect(AuthException.class); - exception.expectMessage(String.format("Deleting change %s is not permitted", id)); + exception.expectMessage("delete not permitted"); gApi.changes().id(changeId).delete(); } @@ -130,15 +126,15 @@ String changeId = changeResult.getChangeId(); // Grant those permissions to admins. - grant(Permission.VIEW_DRAFTS, project, "refs/*"); - grant(Permission.DELETE_DRAFTS, project, "refs/*"); + grant(project, "refs/*", Permission.VIEW_DRAFTS); + grant(project, "refs/*", Permission.DELETE_DRAFTS); try { setApiUser(admin); gApi.changes().id(changeId).delete(); } finally { - removePermission(Permission.DELETE_DRAFTS, project, "refs/*"); - removePermission(Permission.VIEW_DRAFTS, project, "refs/*"); + removePermission(project, "refs/*", Permission.DELETE_DRAFTS); + removePermission(project, "refs/*", Permission.VIEW_DRAFTS); } setApiUser(user); @@ -245,7 +241,7 @@ private void markChangeAsDraft(Change.Id id) throws Exception { try (BatchUpdate batchUpdate = - updateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) { + batchUpdateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) { batchUpdate.addOp(id, new MarkChangeAsDraftUpdateOp()); batchUpdate.execute(); } @@ -257,7 +253,7 @@ private void setDraftStatusOfPatchSetsOfChange(Change.Id id, boolean draftStatus) throws Exception { try (BatchUpdate batchUpdate = - updateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) { + batchUpdateFactory.create(db, project, atrScope.get().getUser(), TimeUtil.nowTs())) { batchUpdate.addOp(id, new DraftStatusOfPatchSetsUpdateOp(draftStatus)); batchUpdate.execute(); }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java index 18925b4..b8c2cf8 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/HashtagsIT.java
@@ -252,14 +252,14 @@ PushOneCommit.Result r = createChange(); setApiUser(user); exception.expect(AuthException.class); - exception.expectMessage("Editing hashtags not permitted"); + exception.expectMessage("edit hashtags not permitted"); addHashtags(r, "MyHashtag"); } @Test public void addHashtagWithPermissionAllowed() throws Exception { PushOneCommit.Result r = createChange(); - grant(Permission.EDIT_HASHTAGS, project, "refs/heads/master", false, REGISTERED_USERS); + grant(project, "refs/heads/master", Permission.EDIT_HASHTAGS, false, REGISTERED_USERS); setApiUser(user); addHashtags(r, "MyHashtag"); assertThatGet(r).containsExactly("MyHashtag");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java index 36f8452..8388ed0 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/MoveChangeIT.java
@@ -159,11 +159,11 @@ new Branch.NameKey(r.getChange().change().getProject(), "blocked_branch"); createBranch(newBranch); block( + "refs/for/" + newBranch.get(), Permission.PUSH, - systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(), - "refs/for/" + newBranch.get()); + systemGroupBackend.getGroup(REGISTERED_USERS).getUUID()); exception.expect(AuthException.class); - exception.expectMessage("Move not permitted"); + exception.expectMessage("move not permitted"); move(r.getChangeId(), newBranch.get()); } @@ -174,12 +174,12 @@ Branch.NameKey newBranch = new Branch.NameKey(r.getChange().change().getProject(), "moveTest"); createBranch(newBranch); block( + r.getChange().change().getDest().get(), Permission.ABANDON, - systemGroupBackend.getGroup(REGISTERED_USERS).getUUID(), - r.getChange().change().getDest().get()); + systemGroupBackend.getGroup(REGISTERED_USERS).getUUID()); setApiUser(user); exception.expect(AuthException.class); - exception.expectMessage("Move not permitted"); + exception.expectMessage("move not permitted"); move(r.getChangeId(), newBranch.get()); } @@ -219,11 +219,11 @@ Util.allow( cfg, Permission.forLabel(patchSetLock.getName()), 0, 1, registeredUsers, "refs/heads/*"); saveProjectConfig(cfg); - grant(Permission.LABEL + "Patch-Set-Lock", project, "refs/heads/*"); + grant(project, "refs/heads/*", Permission.LABEL + "Patch-Set-Lock"); revision(r).review(new ReviewInput().label("Patch-Set-Lock", 1)); exception.expect(AuthException.class); - exception.expectMessage("Move not permitted"); + exception.expectMessage("move not permitted"); move(r.getChangeId(), newBranch.get()); }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java index 26a91aa..a385932 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByCherryPickIT.java
@@ -15,6 +15,7 @@ package com.google.gerrit.acceptance.rest.change; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.PushOneCommit; @@ -388,6 +389,9 @@ @Test public void repairChangeStateAfterFailure() throws Exception { + // In NoteDb-only mode, repo and meta updates are atomic (at least in InMemoryRepository). + assume().that(notesMigration.disableChangeReviewDb()).isFalse(); + RevCommit initialHead = getRemoteHead(); PushOneCommit.Result change = createChange("Change 1", "a.txt", "content"); submit(change.getChangeId()); @@ -396,10 +400,11 @@ testRepo.reset(initialHead); PushOneCommit.Result change2 = createChange("Change 2", "b.txt", "other content"); Change.Id id2 = change2.getChange().getId(); - SubmitInput failAfterRefUpdates = new TestSubmitInput(new SubmitInput(), true); + TestSubmitInput failInput = new TestSubmitInput(); + failInput.failAfterRefUpdates = true; submit( change2.getChangeId(), - failAfterRefUpdates, + failInput, ResourceConflictException.class, "Failing after ref updates"); RevCommit headAfterFailedSubmit = getRemoteHead();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java index 65ad499..d4397d64 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByFastForwardIT.java
@@ -15,13 +15,13 @@ package com.google.gerrit.acceptance.rest.change; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.acceptance.GitUtil.pushHead; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.common.data.Permission; -import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.client.ChangeStatus; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.common.ActionInfo; @@ -145,12 +145,16 @@ @Test public void repairChangeStateAfterFailure() throws Exception { + // In NoteDb-only mode, repo and meta updates are atomic (at least in InMemoryRepository). + assume().that(notesMigration.disableChangeReviewDb()).isFalse(); + PushOneCommit.Result change = createChange("Change 1", "a.txt", "content"); Change.Id id = change.getChange().getId(); - SubmitInput failAfterRefUpdates = new TestSubmitInput(new SubmitInput(), true); + TestSubmitInput failInput = new TestSubmitInput(); + failInput.failAfterRefUpdates = true; submit( change.getChangeId(), - failAfterRefUpdates, + failInput, ResourceConflictException.class, "Failing after ref updates"); @@ -189,8 +193,8 @@ public void submitSameCommitsAsInExperimentalBranch() throws Exception { RevCommit initialHead = getRemoteHead(); - grant(Permission.CREATE, project, "refs/heads/*"); - grant(Permission.PUSH, project, "refs/heads/experimental"); + grant(project, "refs/heads/*", Permission.CREATE); + grant(project, "refs/heads/experimental", Permission.PUSH); RevCommit c1 = commitBuilder().add("b.txt", "1").message("commit at tip").create(); String id1 = GitUtil.getChangeId(testRepo, c1).get();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java index b4fef08..897ac48 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SuggestReviewersIT.java
@@ -144,7 +144,7 @@ List<SuggestedReviewerInfo> reviewers; setApiUser(user3); - block("read", ANONYMOUS_USERS, "refs/*"); + block("refs/*", "read", ANONYMOUS_USERS); allow("read", group1.getGroupUUID(), "refs/*"); reviewers = suggestReviewers(changeId, user2.username, 2); assertThat(reviewers).isEmpty(); @@ -421,7 +421,8 @@ private TestAccount user(String name, String fullName, String emailName, AccountGroup... groups) throws Exception { String[] groupNames = Arrays.stream(groups).map(AccountGroup::getName).toArray(String[]::new); - return accounts.create(name(name), name(emailName) + "@example.com", fullName, groupNames); + return accountCreator.create( + name(name), name(emailName) + "@example.com", fullName, groupNames); } private TestAccount user(String name, String fullName, AccountGroup... groups) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java index f05ecce..7cd9584 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/KillTaskIT.java
@@ -15,6 +15,7 @@ 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; @@ -40,7 +41,7 @@ .filter(t -> "Log File Compressor".equals(t.command)) .map(t -> t.id) .findFirst(); - assertThat(id.isPresent()).isTrue(); + assertThat(id).isPresent(); r = adminRestSession.delete("/config/server/tasks/" + id.get()); r.assertNoContent();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java index f51bbf5..351623a 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/ServerInfoIT.java
@@ -168,7 +168,7 @@ assertThat(i.change.largeChange).isEqualTo(500); assertThat(i.change.replyTooltip).startsWith("Reply and score"); assertThat(i.change.replyLabel).isEqualTo("Reply\u2026"); - assertThat(i.change.updateDelay).isEqualTo(30); + assertThat(i.change.updateDelay).isEqualTo(300); // download assertThat(i.download.archives).containsExactly("tar", "tbz2", "tgz", "txz");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java new file mode 100644 index 0000000..cea907d --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AbstractPushTag.java
@@ -0,0 +1,261 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.project; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.GitUtil.createAnnotatedTag; +import static com.google.gerrit.acceptance.GitUtil.deleteRef; +import static com.google.gerrit.acceptance.GitUtil.pushHead; +import static com.google.gerrit.acceptance.GitUtil.updateAnnotatedTag; +import static com.google.gerrit.acceptance.rest.project.AbstractPushTag.TagType.ANNOTATED; +import static com.google.gerrit.acceptance.rest.project.AbstractPushTag.TagType.LIGHTWEIGHT; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; + +import com.google.common.base.MoreObjects; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.GitUtil; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.reviewdb.client.RefNames; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.transport.PushResult; +import org.eclipse.jgit.transport.RemoteRefUpdate; +import org.eclipse.jgit.transport.RemoteRefUpdate.Status; +import org.junit.Before; +import org.junit.Test; + +@NoHttpd +public abstract class AbstractPushTag extends AbstractDaemonTest { + enum TagType { + LIGHTWEIGHT(Permission.CREATE), + ANNOTATED(Permission.CREATE_TAG); + + final String createPermission; + + TagType(String createPermission) { + this.createPermission = createPermission; + } + } + + private RevCommit initialHead; + private TagType tagType; + + @Before + public void setup() throws Exception { + // clone with user to avoid inherited tag permissions of admin user + testRepo = cloneProject(project, user); + + initialHead = getRemoteHead(); + tagType = getTagType(); + } + + protected abstract TagType getTagType(); + + @Test + public void createTagForExistingCommit() throws Exception { + pushTagForExistingCommit(Status.REJECTED_OTHER_REASON); + + allowTagCreation(); + pushTagForExistingCommit(Status.OK); + + allowPushOnRefsTags(); + pushTagForExistingCommit(Status.OK); + + removePushFromRefsTags(); + } + + @Test + public void createTagForNewCommit() throws Exception { + pushTagForNewCommit(Status.REJECTED_OTHER_REASON); + + allowTagCreation(); + pushTagForNewCommit(Status.REJECTED_OTHER_REASON); + + allowPushOnRefsTags(); + pushTagForNewCommit(Status.OK); + + removePushFromRefsTags(); + } + + @Test + public void fastForward() throws Exception { + allowTagCreation(); + String tagName = pushTagForExistingCommit(Status.OK); + + fastForwardTagToExistingCommit(tagName, Status.REJECTED_OTHER_REASON); + fastForwardTagToNewCommit(tagName, Status.REJECTED_OTHER_REASON); + + allowTagDeletion(); + fastForwardTagToExistingCommit(tagName, Status.REJECTED_OTHER_REASON); + fastForwardTagToNewCommit(tagName, Status.REJECTED_OTHER_REASON); + + allowPushOnRefsTags(); + Status expectedStatus = tagType == ANNOTATED ? Status.REJECTED_OTHER_REASON : Status.OK; + fastForwardTagToExistingCommit(tagName, expectedStatus); + fastForwardTagToNewCommit(tagName, expectedStatus); + + allowForcePushOnRefsTags(); + fastForwardTagToExistingCommit(tagName, Status.OK); + fastForwardTagToNewCommit(tagName, Status.OK); + + removePushFromRefsTags(); + } + + @Test + public void forceUpdate() throws Exception { + allowTagCreation(); + String tagName = pushTagForExistingCommit(Status.OK); + + forceUpdateTagToExistingCommit(tagName, Status.REJECTED_OTHER_REASON); + forceUpdateTagToNewCommit(tagName, Status.REJECTED_OTHER_REASON); + + allowPushOnRefsTags(); + forceUpdateTagToExistingCommit(tagName, Status.REJECTED_OTHER_REASON); + forceUpdateTagToNewCommit(tagName, Status.REJECTED_OTHER_REASON); + + allowTagDeletion(); + forceUpdateTagToExistingCommit(tagName, Status.REJECTED_OTHER_REASON); + forceUpdateTagToNewCommit(tagName, Status.REJECTED_OTHER_REASON); + + allowForcePushOnRefsTags(); + forceUpdateTagToExistingCommit(tagName, Status.OK); + forceUpdateTagToNewCommit(tagName, Status.OK); + + removePushFromRefsTags(); + } + + @Test + public void delete() throws Exception { + allowTagCreation(); + String tagName = pushTagForExistingCommit(Status.OK); + + pushTagDeletion(tagName, Status.REJECTED_OTHER_REASON); + + allowPushOnRefsTags(); + pushTagDeletion(tagName, Status.REJECTED_OTHER_REASON); + + allowForcePushOnRefsTags(); + tagName = pushTagForExistingCommit(Status.OK); + pushTagDeletion(tagName, Status.OK); + + removePushFromRefsTags(); + allowTagDeletion(); + tagName = pushTagForExistingCommit(Status.OK); + pushTagDeletion(tagName, Status.OK); + } + + private String pushTagForExistingCommit(Status expectedStatus) throws Exception { + return pushTag(null, false, false, expectedStatus); + } + + private String pushTagForNewCommit(Status expectedStatus) throws Exception { + return pushTag(null, true, false, expectedStatus); + } + + private void fastForwardTagToExistingCommit(String tagName, Status expectedStatus) + throws Exception { + pushTag(tagName, false, false, expectedStatus); + } + + private void fastForwardTagToNewCommit(String tagName, Status expectedStatus) throws Exception { + pushTag(tagName, true, false, expectedStatus); + } + + private void forceUpdateTagToExistingCommit(String tagName, Status expectedStatus) + throws Exception { + pushTag(tagName, false, true, expectedStatus); + } + + private void forceUpdateTagToNewCommit(String tagName, Status expectedStatus) throws Exception { + pushTag(tagName, true, true, expectedStatus); + } + + private String pushTag(String tagName, boolean newCommit, boolean force, Status expectedStatus) + throws Exception { + if (force) { + testRepo.reset(initialHead); + } + commit(user.getIdent(), "subject"); + + boolean createTag = tagName == null; + tagName = MoreObjects.firstNonNull(tagName, "v1_" + System.nanoTime()); + switch (tagType) { + case LIGHTWEIGHT: + break; + case ANNOTATED: + if (createTag) { + createAnnotatedTag(testRepo, tagName, user.getIdent()); + } else { + updateAnnotatedTag(testRepo, tagName, user.getIdent()); + } + break; + default: + throw new IllegalStateException("unexpected tag type: " + tagType); + } + + if (!newCommit) { + grant(project, "refs/for/refs/heads/master", Permission.SUBMIT, false, REGISTERED_USERS); + pushHead(testRepo, "refs/for/master%submit"); + } + + String tagRef = tagRef(tagName); + PushResult r = + tagType == LIGHTWEIGHT + ? pushHead(testRepo, tagRef, false, force) + : GitUtil.pushTag(testRepo, tagName, !createTag); + RemoteRefUpdate refUpdate = r.getRemoteUpdate(tagRef); + assertThat(refUpdate.getStatus()).named(tagType.name()).isEqualTo(expectedStatus); + return tagName; + } + + private void pushTagDeletion(String tagName, Status expectedStatus) throws Exception { + String tagRef = tagRef(tagName); + PushResult r = deleteRef(testRepo, tagRef); + RemoteRefUpdate refUpdate = r.getRemoteUpdate(tagRef); + assertThat(refUpdate.getStatus()).named(tagType.name()).isEqualTo(expectedStatus); + } + + private void allowTagCreation() throws Exception { + grant(project, "refs/tags/*", tagType.createPermission, false, REGISTERED_USERS); + } + + private void allowPushOnRefsTags() throws Exception { + removePushFromRefsTags(); + grant(project, "refs/tags/*", Permission.PUSH, false, REGISTERED_USERS); + } + + private void allowForcePushOnRefsTags() throws Exception { + removePushFromRefsTags(); + grant(project, "refs/tags/*", Permission.PUSH, true, REGISTERED_USERS); + } + + private void allowTagDeletion() throws Exception { + removePushFromRefsTags(); + grant(project, "refs/tags/*", Permission.DELETE, true, REGISTERED_USERS); + } + + private void removePushFromRefsTags() throws Exception { + removePermission(project, "refs/tags/*", Permission.PUSH); + } + + private void commit(PersonIdent ident, String subject) throws Exception { + commitBuilder().ident(ident).message(subject + " (" + System.nanoTime() + ")").create(); + } + + private static String tagRef(String tagName) { + return RefNames.REFS_TAGS + tagName; + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java index 839f166..8695498 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -218,7 +218,7 @@ setApiUser(user); exception.expect(AuthException.class); - exception.expectMessage("not administrator"); + exception.expectMessage("administrate server not permitted"); gApi.projects().name(newProjectName).access(accessInput); }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD index 3266be8..fbe5d80 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/BUILD
@@ -6,6 +6,7 @@ labels = ["rest"], deps = [ ":project", + ":push_tag_util", ":refassert", ], ) @@ -35,3 +36,14 @@ "//lib:truth", ], ) + +java_library( + name = "push_tag_util", + testonly = 1, + srcs = [ + "AbstractPushTag.java", + ], + deps = [ + "//gerrit-acceptance-tests:lib", + ], +)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java index 2c74949..224e41c 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateBranchIT.java
@@ -80,7 +80,7 @@ } private void blockCreateReference() throws Exception { - block(Permission.CREATE, ANONYMOUS_USERS, "refs/*"); + block("refs/*", Permission.CREATE, ANONYMOUS_USERS); } private void grantOwner() throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java index 66c61f7..1ece7fb 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteBranchIT.java
@@ -112,11 +112,11 @@ } private void blockForcePush() throws Exception { - block(Permission.PUSH, ANONYMOUS_USERS, "refs/heads/*").setForce(true); + block("refs/heads/*", Permission.PUSH, ANONYMOUS_USERS).setForce(true); } private void grantForcePush() throws Exception { - grant(Permission.PUSH, project, "refs/heads/*", true, ANONYMOUS_USERS); + grant(project, "refs/heads/*", Permission.PUSH, true, ANONYMOUS_USERS); } private void grantDelete() throws Exception { @@ -149,7 +149,7 @@ private void assertDeleteForbidden() throws Exception { exception.expect(AuthException.class); - exception.expectMessage("Cannot delete branch"); + exception.expectMessage("delete not permitted"); branch().delete(); } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java index c229a43..e37071e 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/DeleteTagIT.java
@@ -91,11 +91,11 @@ } private void blockForcePush() throws Exception { - block(Permission.PUSH, ANONYMOUS_USERS, "refs/tags/*").setForce(true); + block("refs/tags/*", Permission.PUSH, ANONYMOUS_USERS).setForce(true); } private void grantForcePush() throws Exception { - grant(Permission.PUSH, project, "refs/tags/*", true, ANONYMOUS_USERS); + grant(project, "refs/tags/*", Permission.PUSH, true, ANONYMOUS_USERS); } private void grantDelete() throws Exception { @@ -120,7 +120,7 @@ private void assertDeleteForbidden() throws Exception { exception.expect(AuthException.class); - exception.expectMessage("Cannot delete tag"); + exception.expectMessage("delete not permitted"); tag().delete(); } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushAnnotatedTagIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushAnnotatedTagIT.java new file mode 100644 index 0000000..24c8ed0 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushAnnotatedTagIT.java
@@ -0,0 +1,23 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.project; + +public class PushAnnotatedTagIT extends AbstractPushTag { + + @Override + protected TagType getTagType() { + return TagType.ANNOTATED; + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushLightweightTagIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushLightweightTagIT.java new file mode 100644 index 0000000..20d83a0 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushLightweightTagIT.java
@@ -0,0 +1,23 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.project; + +public class PushLightweightTagIT extends AbstractPushTag { + + @Override + protected TagType getTagType() { + return TagType.LIGHTWEIGHT; + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushTagIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushTagIT.java deleted file mode 100644 index 7ed15f4..0000000 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushTagIT.java +++ /dev/null
@@ -1,275 +0,0 @@ -// Copyright (C) 2016 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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.project; - -import static com.google.common.truth.Truth.assertThat; -import static com.google.gerrit.acceptance.GitUtil.createAnnotatedTag; -import static com.google.gerrit.acceptance.GitUtil.deleteRef; -import static com.google.gerrit.acceptance.GitUtil.pushHead; -import static com.google.gerrit.acceptance.GitUtil.updateAnnotatedTag; -import static com.google.gerrit.acceptance.rest.project.PushTagIT.TagType.ANNOTATED; -import static com.google.gerrit.acceptance.rest.project.PushTagIT.TagType.LIGHTWEIGHT; -import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; - -import com.google.common.base.MoreObjects; -import com.google.gerrit.acceptance.AbstractDaemonTest; -import com.google.gerrit.acceptance.GitUtil; -import com.google.gerrit.acceptance.NoHttpd; -import com.google.gerrit.common.data.Permission; -import com.google.gerrit.reviewdb.client.RefNames; -import org.eclipse.jgit.lib.PersonIdent; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.transport.PushResult; -import org.eclipse.jgit.transport.RemoteRefUpdate; -import org.eclipse.jgit.transport.RemoteRefUpdate.Status; -import org.junit.Before; -import org.junit.Test; - -@NoHttpd -public class PushTagIT extends AbstractDaemonTest { - enum TagType { - LIGHTWEIGHT(Permission.CREATE), - ANNOTATED(Permission.CREATE_TAG); - - final String createPermission; - - TagType(String createPermission) { - this.createPermission = createPermission; - } - } - - private RevCommit initialHead; - - @Before - public void setup() throws Exception { - // clone with user to avoid inherited tag permissions of admin user - testRepo = cloneProject(project, user); - - initialHead = getRemoteHead(); - } - - @Test - public void createTagForExistingCommit() throws Exception { - for (TagType tagType : TagType.values()) { - pushTagForExistingCommit(tagType, Status.REJECTED_OTHER_REASON); - - allowTagCreation(tagType); - pushTagForExistingCommit(tagType, Status.OK); - - allowPushOnRefsTags(); - pushTagForExistingCommit(tagType, Status.OK); - - removePushFromRefsTags(); - } - } - - @Test - public void createTagForNewCommit() throws Exception { - for (TagType tagType : TagType.values()) { - pushTagForNewCommit(tagType, Status.REJECTED_OTHER_REASON); - - allowTagCreation(tagType); - pushTagForNewCommit(tagType, Status.REJECTED_OTHER_REASON); - - allowPushOnRefsTags(); - pushTagForNewCommit(tagType, Status.OK); - - removePushFromRefsTags(); - } - } - - @Test - public void fastForward() throws Exception { - for (TagType tagType : TagType.values()) { - allowTagCreation(tagType); - String tagName = pushTagForExistingCommit(tagType, Status.OK); - - fastForwardTagToExistingCommit(tagType, tagName, Status.REJECTED_OTHER_REASON); - fastForwardTagToNewCommit(tagType, tagName, Status.REJECTED_OTHER_REASON); - - allowTagDeletion(); - fastForwardTagToExistingCommit(tagType, tagName, Status.REJECTED_OTHER_REASON); - fastForwardTagToNewCommit(tagType, tagName, Status.REJECTED_OTHER_REASON); - - allowPushOnRefsTags(); - Status expectedStatus = tagType == ANNOTATED ? Status.REJECTED_OTHER_REASON : Status.OK; - fastForwardTagToExistingCommit(tagType, tagName, expectedStatus); - fastForwardTagToNewCommit(tagType, tagName, expectedStatus); - - allowForcePushOnRefsTags(); - fastForwardTagToExistingCommit(tagType, tagName, Status.OK); - fastForwardTagToNewCommit(tagType, tagName, Status.OK); - - removePushFromRefsTags(); - } - } - - @Test - public void forceUpdate() throws Exception { - for (TagType tagType : TagType.values()) { - allowTagCreation(tagType); - String tagName = pushTagForExistingCommit(tagType, Status.OK); - - forceUpdateTagToExistingCommit(tagType, tagName, Status.REJECTED_OTHER_REASON); - forceUpdateTagToNewCommit(tagType, tagName, Status.REJECTED_OTHER_REASON); - - allowPushOnRefsTags(); - forceUpdateTagToExistingCommit(tagType, tagName, Status.REJECTED_OTHER_REASON); - forceUpdateTagToNewCommit(tagType, tagName, Status.REJECTED_OTHER_REASON); - - allowTagDeletion(); - forceUpdateTagToExistingCommit(tagType, tagName, Status.REJECTED_OTHER_REASON); - forceUpdateTagToNewCommit(tagType, tagName, Status.REJECTED_OTHER_REASON); - - allowForcePushOnRefsTags(); - forceUpdateTagToExistingCommit(tagType, tagName, Status.OK); - forceUpdateTagToNewCommit(tagType, tagName, Status.OK); - - removePushFromRefsTags(); - } - } - - @Test - public void delete() throws Exception { - for (TagType tagType : TagType.values()) { - allowTagCreation(tagType); - String tagName = pushTagForExistingCommit(tagType, Status.OK); - - pushTagDeletion(tagType, tagName, Status.REJECTED_OTHER_REASON); - - allowPushOnRefsTags(); - pushTagDeletion(tagType, tagName, Status.REJECTED_OTHER_REASON); - } - - allowForcePushOnRefsTags(); - for (TagType tagType : TagType.values()) { - String tagName = pushTagForExistingCommit(tagType, Status.OK); - pushTagDeletion(tagType, tagName, Status.OK); - } - - removePushFromRefsTags(); - allowTagDeletion(); - for (TagType tagType : TagType.values()) { - String tagName = pushTagForExistingCommit(tagType, Status.OK); - pushTagDeletion(tagType, tagName, Status.OK); - } - } - - private String pushTagForExistingCommit(TagType tagType, Status expectedStatus) throws Exception { - return pushTag(tagType, null, false, false, expectedStatus); - } - - private String pushTagForNewCommit(TagType tagType, Status expectedStatus) throws Exception { - return pushTag(tagType, null, true, false, expectedStatus); - } - - private void fastForwardTagToExistingCommit( - TagType tagType, String tagName, Status expectedStatus) throws Exception { - pushTag(tagType, tagName, false, false, expectedStatus); - } - - private void fastForwardTagToNewCommit(TagType tagType, String tagName, Status expectedStatus) - throws Exception { - pushTag(tagType, tagName, true, false, expectedStatus); - } - - private void forceUpdateTagToExistingCommit( - TagType tagType, String tagName, Status expectedStatus) throws Exception { - pushTag(tagType, tagName, false, true, expectedStatus); - } - - private void forceUpdateTagToNewCommit(TagType tagType, String tagName, Status expectedStatus) - throws Exception { - pushTag(tagType, tagName, true, true, expectedStatus); - } - - private String pushTag( - TagType tagType, String tagName, boolean newCommit, boolean force, Status expectedStatus) - throws Exception { - if (force) { - testRepo.reset(initialHead); - } - commit(user.getIdent(), "subject"); - - boolean createTag = tagName == null; - tagName = MoreObjects.firstNonNull(tagName, "v1_" + System.nanoTime()); - switch (tagType) { - case LIGHTWEIGHT: - break; - case ANNOTATED: - if (createTag) { - createAnnotatedTag(testRepo, tagName, user.getIdent()); - } else { - updateAnnotatedTag(testRepo, tagName, user.getIdent()); - } - break; - default: - throw new IllegalStateException("unexpected tag type: " + tagType); - } - - if (!newCommit) { - grant(Permission.SUBMIT, project, "refs/for/refs/heads/master", false, REGISTERED_USERS); - pushHead(testRepo, "refs/for/master%submit"); - } - - String tagRef = tagRef(tagName); - PushResult r = - tagType == LIGHTWEIGHT - ? pushHead(testRepo, tagRef, false, force) - : GitUtil.pushTag(testRepo, tagName, !createTag); - RemoteRefUpdate refUpdate = r.getRemoteUpdate(tagRef); - assertThat(refUpdate.getStatus()).named(tagType.name()).isEqualTo(expectedStatus); - return tagName; - } - - private void pushTagDeletion(TagType tagType, String tagName, Status expectedStatus) - throws Exception { - String tagRef = tagRef(tagName); - PushResult r = deleteRef(testRepo, tagRef); - RemoteRefUpdate refUpdate = r.getRemoteUpdate(tagRef); - assertThat(refUpdate.getStatus()).named(tagType.name()).isEqualTo(expectedStatus); - } - - private void allowTagCreation(TagType tagType) throws Exception { - grant(tagType.createPermission, project, "refs/tags/*", false, REGISTERED_USERS); - } - - private void allowPushOnRefsTags() throws Exception { - removePushFromRefsTags(); - grant(Permission.PUSH, project, "refs/tags/*", false, REGISTERED_USERS); - } - - private void allowForcePushOnRefsTags() throws Exception { - removePushFromRefsTags(); - grant(Permission.PUSH, project, "refs/tags/*", true, REGISTERED_USERS); - } - - private void allowTagDeletion() throws Exception { - removePushFromRefsTags(); - grant(Permission.DELETE, project, "refs/tags/*", true, REGISTERED_USERS); - } - - private void removePushFromRefsTags() throws Exception { - removePermission(Permission.PUSH, project, "refs/tags/*"); - } - - private void commit(PersonIdent ident, String subject) throws Exception { - commitBuilder().ident(ident).message(subject + " (" + System.nanoTime() + ")").create(); - } - - private static String tagRef(String tagName) { - return RefNames.REFS_TAGS + tagName; - } -}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java index f191681..a34a745 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -239,17 +239,17 @@ @Test public void createTagNotAllowed() throws Exception { - block(Permission.CREATE, REGISTERED_USERS, R_TAGS + "*"); + block(R_TAGS + "*", Permission.CREATE, REGISTERED_USERS); TagInput input = new TagInput(); input.ref = "test"; exception.expect(AuthException.class); - exception.expectMessage("Cannot create tag \"" + R_TAGS + "test\""); + exception.expectMessage("create not permitted"); tag(input.ref).create(input); } @Test public void createAnnotatedTagNotAllowed() throws Exception { - block(Permission.CREATE_TAG, REGISTERED_USERS, R_TAGS + "*"); + block(R_TAGS + "*", Permission.CREATE_TAG, REGISTERED_USERS); TagInput input = new TagInput(); input.ref = "test"; input.message = "annotation";
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/revision/BUILD b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/revision/BUILD new file mode 100644 index 0000000..f47ac46 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/revision/BUILD
@@ -0,0 +1,7 @@ +load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests") + +acceptance_tests( + srcs = glob(["*IT.java"]), + group = "rest_revision", + labels = ["rest"], +)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/revision/RevisionIT.java new file mode 100644 index 0000000..89fdeff --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/revision/RevisionIT.java
@@ -0,0 +1,78 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.revision; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT; +import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import org.eclipse.jgit.util.Base64; +import org.junit.Test; + +public class RevisionIT extends AbstractDaemonTest { + @Test + public void contentOfParent() throws Exception { + String parentContent = "parent content"; + PushOneCommit.Result parent = createChange("Parent change", FILE_NAME, parentContent); + parent.assertOkStatus(); + + gApi.changes().id(parent.getChangeId()).current().review(ReviewInput.approve()); + gApi.changes().id(parent.getChangeId()).current().submit(); + + PushOneCommit.Result child = createChange("Child change", FILE_NAME, FILE_CONTENT); + child.assertOkStatus(); + assertContent(child, FILE_NAME, FILE_CONTENT); + + RestResponse response = + adminRestSession.get( + "/changes/" + + child.getChangeId() + + "/revisions/current/files/" + + FILE_NAME + + "/content?parent=1"); + response.assertOK(); + assertThat(new String(Base64.decode(response.getEntityContent()), UTF_8)) + .isEqualTo(parentContent); + } + + @Test + public void contentOfInvalidParent() throws Exception { + String parentContent = "parent content"; + PushOneCommit.Result parent = createChange("Parent change", FILE_NAME, parentContent); + parent.assertOkStatus(); + + gApi.changes().id(parent.getChangeId()).current().review(ReviewInput.approve()); + gApi.changes().id(parent.getChangeId()).current().submit(); + + PushOneCommit.Result child = createChange("Child change", FILE_NAME, FILE_CONTENT); + child.assertOkStatus(); + assertContent(child, FILE_NAME, FILE_CONTENT); + + RestResponse response = + adminRestSession.get( + "/changes/" + + child.getChangeId() + + "/revisions/current/files/" + + FILE_NAME + + "/content?parent=10"); + response.assertBadRequest(); + assertThat(response.getEntityContent()).isEqualTo("invalid parent"); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java index 7e95da6..20b92b2 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -15,17 +15,20 @@ package com.google.gerrit.acceptance.server.change; 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 com.google.common.base.Function; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AcceptanceTestRequestScope; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.extensions.api.changes.DeleteCommentInput; 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; @@ -34,27 +37,44 @@ import com.google.gerrit.extensions.client.Side; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.CommentInfo; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.TopLevelResource; +import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Patch; +import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.ChangesCollection; import com.google.gerrit.server.change.PostReview; import com.google.gerrit.server.change.RevisionResource; +import com.google.gerrit.server.notedb.ChangeNoteUtil; +import com.google.gerrit.server.notedb.DeleteCommentRewriter; import com.google.gerrit.testutil.FakeEmailSender; import com.google.gerrit.testutil.FakeEmailSender.Message; import com.google.inject.Inject; import com.google.inject.Provider; +import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.notes.NoteMap; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.junit.Before; import org.junit.Test; @@ -67,6 +87,8 @@ @Inject private FakeEmailSender email; + @Inject private ChangeNoteUtil noteUtil; + private final Integer[] lines = {0, 1}; @Before @@ -380,7 +402,7 @@ ChangeResource changeRsrc = changes.get().parse(TopLevelResource.INSTANCE, IdString.fromDecoded(changeId)); RevisionResource revRsrc = revisions.parse(changeRsrc, IdString.fromDecoded(revId)); - postReview.get().apply(revRsrc, input, timestamp); + postReview.get().apply(batchUpdateFactory, revRsrc, input, timestamp); Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId); assertThat(result).isNotEmpty(); CommentInfo actual = Iterables.getOnlyElement(result.get(comment.path)); @@ -739,6 +761,282 @@ } } + @Test + public void deleteCommentCannotBeAppliedByUser() throws Exception { + PushOneCommit.Result result = createChange(); + CommentInput targetComment = addComment(result.getChangeId(), "My password: abc123"); + + Map<String, List<CommentInfo>> commentsMap = + getPublishedComments(result.getChangeId(), result.getCommit().name()); + + assertThat(commentsMap.size()).isEqualTo(1); + assertThat(commentsMap.get(FILE_NAME)).hasSize(1); + + String uuid = commentsMap.get(targetComment.path).get(0).id; + DeleteCommentInput input = new DeleteCommentInput("contains confidential information"); + + setApiUser(user); + exception.expect(AuthException.class); + gApi.changes().id(result.getChangeId()).current().comment(uuid).delete(input); + } + + @Test + public void deleteCommentByRewritingCommitHistory() throws Exception { + // Creates the following commit history on the meta branch of the test change. Then tries to + // delete the comments one by one, which will rewrite most of the commits on the 'meta' branch. + // Commits will be rewritten N times for N added comments. After each deletion, the meta branch + // should keep its previous state except that the target comment's message should be updated. + + // 1st commit: Create PS1. + PushOneCommit.Result result1 = createChange(SUBJECT, "a.txt", "a"); + Change.Id id = result1.getChange().getId(); + String changeId = result1.getChangeId(); + String ps1 = result1.getCommit().name(); + + // 2nd commit: Add (c1) to PS1. + CommentInput c1 = newComment("a.txt", "comment 1"); + addComments(changeId, ps1, c1); + + // 3rd commit: Add (c2, c3) to PS1. + CommentInput c2 = newComment("a.txt", "comment 2"); + CommentInput c3 = newComment("a.txt", "comment 3"); + addComments(changeId, ps1, c2, c3); + + // 4th commit: Add (c4) to PS1. + CommentInput c4 = newComment("a.txt", "comment 4"); + addComments(changeId, ps1, c4); + + // 5th commit: Create PS2. + PushOneCommit.Result result2 = amendChange(changeId, "refs/for/master", "b.txt", "b"); + String ps2 = result2.getCommit().name(); + + // 6th commit: Add (c5) to PS1. + CommentInput c5 = newComment("a.txt", "comment 5"); + addComments(changeId, ps1, c5); + + // 7th commit: Add (c6) to PS2. + CommentInput c6 = newComment("b.txt", "comment 6"); + addComments(changeId, ps2, c6); + + // 8th commit: Create PS3. + PushOneCommit.Result result3 = amendChange(changeId); + String ps3 = result3.getCommit().name(); + + // 9th commit: Create PS4. + PushOneCommit.Result result4 = amendChange(changeId, "refs/for/master", "c.txt", "c"); + String ps4 = result4.getCommit().name(); + + // 10th commit: Add (c7, c8) to PS4. + CommentInput c7 = newComment("c.txt", "comment 7"); + CommentInput c8 = newComment("b.txt", "comment 8"); + addComments(changeId, ps4, c7, c8); + + // 11th commit: Add (c9) to PS2. + CommentInput c9 = newComment("b.txt", "comment 9"); + addComments(changeId, ps2, c9); + + List<CommentInfo> commentsBeforeDelete = getChangeSortedComments(changeId); + assertThat(commentsBeforeDelete).hasSize(9); + // PS1 has comments [c1, c2, c3, c4, c5]. + assertThat(getRevisionComments(changeId, ps1)).hasSize(5); + // PS2 has comments [c6, c9]. + assertThat(getRevisionComments(changeId, ps2)).hasSize(2); + // PS3 has no comment. + assertThat(getRevisionComments(changeId, ps3)).hasSize(0); + // PS4 has comments [c7, c8]. + assertThat(getRevisionComments(changeId, ps4)).hasSize(2); + + setApiUser(admin); + for (int i = 0; i < commentsBeforeDelete.size(); i++) { + List<RevCommit> commitsBeforeDelete = new ArrayList<>(); + if (notesMigration.commitChangeWrites()) { + commitsBeforeDelete = getCommits(id); + } + + CommentInfo comment = commentsBeforeDelete.get(i); + String uuid = comment.id; + int patchSet = comment.patchSet; + // 'oldComment' has some fields unset compared with 'comment'. + CommentInfo oldComment = gApi.changes().id(changeId).revision(patchSet).comment(uuid).get(); + + DeleteCommentInput input = new DeleteCommentInput("delete comment " + uuid); + CommentInfo updatedComment = + gApi.changes().id(changeId).revision(patchSet).comment(uuid).delete(input); + + String expectedMsg = + String.format("Comment removed by: %s; Reason: %s", admin.fullName, input.reason); + assertThat(updatedComment.message).isEqualTo(expectedMsg); + oldComment.message = expectedMsg; + assertThat(updatedComment).isEqualTo(oldComment); + + // Check the NoteDb state after the deletion. + if (notesMigration.commitChangeWrites()) { + assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg); + } + + comment.message = expectedMsg; + commentsBeforeDelete.set(i, comment); + List<CommentInfo> commentsAfterDelete = getChangeSortedComments(changeId); + assertThat(commentsAfterDelete).isEqualTo(commentsBeforeDelete); + } + + // Make sure that comments can still be added correctly. + CommentInput c10 = newComment("a.txt", "comment 10"); + CommentInput c11 = newComment("b.txt", "comment 11"); + CommentInput c12 = newComment("a.txt", "comment 12"); + CommentInput c13 = newComment("c.txt", "comment 13"); + addComments(changeId, ps1, c10); + addComments(changeId, ps2, c11); + addComments(changeId, ps3, c12); + addComments(changeId, ps4, c13); + + assertThat(getChangeSortedComments(changeId)).hasSize(13); + assertThat(getRevisionComments(changeId, ps1)).hasSize(6); + assertThat(getRevisionComments(changeId, ps2)).hasSize(3); + assertThat(getRevisionComments(changeId, ps3)).hasSize(1); + assertThat(getRevisionComments(changeId, ps4)).hasSize(3); + } + + @Test + public void deleteOneCommentMultipleTimes() throws Exception { + PushOneCommit.Result result = createChange(); + Change.Id id = result.getChange().getId(); + String changeId = result.getChangeId(); + String ps1 = result.getCommit().name(); + + CommentInput c1 = newComment(FILE_NAME, "comment 1"); + CommentInput c2 = newComment(FILE_NAME, "comment 2"); + CommentInput c3 = newComment(FILE_NAME, "comment 3"); + addComments(changeId, ps1, c1); + addComments(changeId, ps1, c2); + addComments(changeId, ps1, c3); + + List<CommentInfo> commentsBeforeDelete = getChangeSortedComments(changeId); + assertThat(commentsBeforeDelete).hasSize(3); + Optional<CommentInfo> targetComment = + commentsBeforeDelete.stream().filter(c -> c.message.equals("comment 2")).findFirst(); + assertThat(targetComment).isPresent(); + String uuid = targetComment.get().id; + CommentInfo oldComment = gApi.changes().id(changeId).revision(ps1).comment(uuid).get(); + + List<RevCommit> commitsBeforeDelete = new ArrayList<>(); + if (notesMigration.commitChangeWrites()) { + commitsBeforeDelete = getCommits(id); + } + + setApiUser(admin); + for (int i = 0; i < 3; i++) { + DeleteCommentInput input = new DeleteCommentInput("delete comment 2, iteration: " + i); + gApi.changes().id(changeId).revision(ps1).comment(uuid).delete(input); + } + + CommentInfo updatedComment = gApi.changes().id(changeId).revision(ps1).comment(uuid).get(); + String expectedMsg = + String.format( + "Comment removed by: %s; Reason: %s", admin.fullName, "delete comment 2, iteration: 2"); + assertThat(updatedComment.message).isEqualTo(expectedMsg); + oldComment.message = expectedMsg; + assertThat(updatedComment).isEqualTo(oldComment); + + if (notesMigration.commitChangeWrites()) { + assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg); + } + assertThat(getChangeSortedComments(changeId)).hasSize(3); + } + + private List<CommentInfo> getChangeSortedComments(String changeId) throws Exception { + List<CommentInfo> comments = new ArrayList<>(); + Map<String, List<CommentInfo>> commentsMap = getPublishedComments(changeId); + for (Entry<String, List<CommentInfo>> e : commentsMap.entrySet()) { + for (CommentInfo c : e.getValue()) { + c.path = e.getKey(); // Set the comment's path field. + comments.add(c); + } + } + comments.sort(Comparator.comparing(c -> c.id)); + return comments; + } + + private List<CommentInfo> getRevisionComments(String changeId, String revId) throws Exception { + return getPublishedComments(changeId, revId) + .values() + .stream() + .flatMap(List::stream) + .collect(Collectors.toList()); + } + + private CommentInput addComment(String changeId, String message) throws Exception { + ReviewInput input = new ReviewInput(); + CommentInput comment = newComment(FILE_NAME, Side.REVISION, 0, message, false); + input.comments = ImmutableMap.of(comment.path, Lists.newArrayList(comment)); + gApi.changes().id(changeId).current().review(input); + return comment; + } + + private void addComments(String changeId, String revision, CommentInput... commentInputs) + throws Exception { + ReviewInput input = new ReviewInput(); + input.comments = Arrays.stream(commentInputs).collect(Collectors.groupingBy(c -> c.path)); + gApi.changes().id(changeId).revision(revision).review(input); + } + + private List<RevCommit> getCommits(Change.Id changeId) throws IOException { + try (Repository repo = repoManager.openRepository(project); + RevWalk revWalk = new RevWalk(repo)) { + Ref metaRef = repo.exactRef(RefNames.changeMetaRef(changeId)); + revWalk.markStart(revWalk.parseCommit(metaRef.getObjectId())); + return Lists.newArrayList(revWalk); + } + } + + /** + * All the commits, which contain the target comment before, should still contain the comment with + * the updated message. All the other metas of the commits should be exactly the same. + */ + private void assertMetaBranchCommitsAfterRewriting( + List<RevCommit> beforeDelete, + Change.Id changeId, + String targetCommentUuid, + String expectedMessage) + throws Exception { + List<RevCommit> afterDelete = getCommits(changeId); + assertThat(afterDelete).hasSize(beforeDelete.size()); + + try (Repository repo = repoManager.openRepository(project); + ObjectReader reader = repo.newObjectReader()) { + for (int i = 0; i < beforeDelete.size(); i++) { + RevCommit commitBefore = beforeDelete.get(i); + RevCommit commitAfter = afterDelete.get(i); + + Map<String, com.google.gerrit.reviewdb.client.Comment> commentMapBefore = + DeleteCommentRewriter.getPublishedComments( + noteUtil, changeId, reader, NoteMap.read(reader, commitBefore)); + Map<String, com.google.gerrit.reviewdb.client.Comment> commentMapAfter = + DeleteCommentRewriter.getPublishedComments( + noteUtil, changeId, reader, NoteMap.read(reader, commitAfter)); + + if (commentMapBefore.containsKey(targetCommentUuid)) { + assertThat(commentMapAfter).containsKey(targetCommentUuid); + com.google.gerrit.reviewdb.client.Comment comment = + commentMapAfter.get(targetCommentUuid); + assertThat(comment.message).isEqualTo(expectedMessage); + comment.message = commentMapBefore.get(targetCommentUuid).message; + commentMapAfter.put(targetCommentUuid, comment); + assertThat(commentMapAfter).isEqualTo(commentMapBefore); + } else { + assertThat(commentMapAfter).doesNotContainKey(targetCommentUuid); + } + + // Other metas should be exactly the same. + assertThat(commitAfter.getFullMessage()).isEqualTo(commitBefore.getFullMessage()); + assertThat(commitAfter.getCommitterIdent()).isEqualTo(commitBefore.getCommitterIdent()); + assertThat(commitAfter.getAuthorIdent()).isEqualTo(commitBefore.getAuthorIdent()); + assertThat(commitAfter.getEncoding()).isEqualTo(commitBefore.getEncoding()); + assertThat(commitAfter.getEncodingName()).isEqualTo(commitBefore.getEncodingName()); + } + } + } + private static String extractComments(String msg) { // Extract lines between start "....." and end "-- ". Pattern p = Pattern.compile(".*[.]{5}\n+(.*)\\n+-- \n.*", Pattern.DOTALL); @@ -803,10 +1101,18 @@ return gApi.changes().id(changeId).revision(revId).drafts(); } + private Map<String, List<CommentInfo>> getPublishedComments(String changeId) throws Exception { + return gApi.changes().id(changeId).comments(); + } + private CommentInfo getDraftComment(String changeId, String revId, String uuid) throws Exception { return gApi.changes().id(changeId).revision(revId).draft(uuid).get(); } + private static CommentInput newComment(String file, String message) { + return newComment(file, Side.REVISION, 0, message, false); + } + private static CommentInput newComment( String path, Side side, int line, String message, Boolean unresolved) { CommentInput c = new CommentInput();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java index e0346b3..c2fcd87 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -41,11 +41,11 @@ import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.Sequences; +import com.google.gerrit.server.account.AccountsUpdate; import com.google.gerrit.server.change.ChangeInserter; import com.google.gerrit.server.change.ConsistencyChecker; import com.google.gerrit.server.change.PatchSetInserter; import com.google.gerrit.server.config.AnonymousCowardName; -import com.google.gerrit.server.git.validators.CommitValidators; import com.google.gerrit.server.notedb.ChangeNoteUtil; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.project.ChangeControl; @@ -68,7 +68,6 @@ import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.transport.ReceiveCommand; import org.junit.Before; import org.junit.Test; @@ -80,8 +79,6 @@ @Inject private IdentifiedUser.GenericFactory userFactory; - @Inject private BatchUpdate.Factory updateFactory; - @Inject private ChangeInserter.Factory changeInserterFactory; @Inject private PatchSetInserter.Factory patchSetInserterFactory; @@ -92,6 +89,8 @@ @Inject private Sequences sequences; + @Inject private AccountsUpdate.Server accountsUpdate; + private RevCommit tip; private Account.Id adminId; private ConsistencyChecker checker; @@ -119,9 +118,9 @@ @Test public void missingOwner() throws Exception { - TestAccount owner = accounts.create("missing"); + TestAccount owner = accountCreator.create("missing"); ChangeControl ctl = insertChange(owner); - db.accounts().deleteKeys(singleton(owner.getId())); + accountsUpdate.create().deleteByKey(db, owner.getId()); assertProblems(ctl, null, problem("Missing change owner: " + owner.getId())); } @@ -783,7 +782,7 @@ } private BatchUpdate newUpdate(Account.Id owner) { - return updateFactory.create(db, project, userFactory.create(owner), TimeUtil.nowTs()); + return batchUpdateFactory.create(db, project, userFactory.create(owner), TimeUtil.nowTs()); } private ChangeControl insertChange() throws Exception { @@ -802,7 +801,7 @@ ins = changeInserterFactory .create(id, commit, dest) - .setValidatePolicy(CommitValidators.Policy.NONE) + .setValidate(false) .setNotify(NotifyHandling.NONE) .setFireRevisionCreated(false) .setSendMail(false); @@ -826,7 +825,7 @@ ins = patchSetInserterFactory .create(ctl, nextPatchSetId(ctl), commit) - .setValidatePolicy(CommitValidators.Policy.NONE) + .setValidate(false) .setFireRevisionCreated(false) .setNotify(NotifyHandling.NONE); bu.addOp(ctl.getId(), ins).execute(); @@ -920,7 +919,7 @@ new BatchUpdateOp() { @Override public void updateRepo(RepoContext ctx) throws IOException { - ctx.addRefUpdate(new ReceiveCommand(oldId, newId, dest)); + ctx.addRefUpdate(oldId, newId, dest); } @Override
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java index 6c06753..be3d17f 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -64,8 +64,6 @@ System.setProperty("user.timezone", systemTimeZone); } - @Inject private BatchUpdate.Factory updateFactory; - @Inject private ChangesCollection changes; @Test @@ -577,8 +575,8 @@ return result; } - private void clearGroups(final PatchSet.Id psId) throws Exception { - try (BatchUpdate bu = updateFactory.create(db, project, user(user), TimeUtil.nowTs())) { + private void clearGroups(PatchSet.Id psId) throws Exception { + try (BatchUpdate bu = batchUpdateFactory.create(db, project, user(user), TimeUtil.nowTs())) { bu.addOp( psId.getParentKey(), new BatchUpdateOp() { @@ -586,7 +584,7 @@ public boolean updateChange(ChangeContext ctx) throws OrmException { PatchSet ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId); psUtil.setGroups(ctx.getDb(), ctx.getUpdate(psId), ps, ImmutableList.<String>of()); - ctx.bumpLastUpdatedOn(false); + ctx.dontBumpLastUpdatedOn(); return true; } });
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java index 05dc219..3964253 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
@@ -17,17 +17,26 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.acceptance.GitUtil.getChangeId; import static com.google.gerrit.acceptance.GitUtil.pushHead; +import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.Patch.ChangeType; +import com.google.gerrit.server.patch.IntraLineDiff; +import com.google.gerrit.server.patch.IntraLineDiffArgs; +import com.google.gerrit.server.patch.IntraLineDiffKey; import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.patch.PatchListEntry; import com.google.gerrit.server.patch.PatchListKey; +import com.google.gerrit.server.patch.Text; import com.google.inject.Inject; +import java.util.ArrayList; import java.util.List; +import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Test; @@ -168,6 +177,33 @@ assertDeleted(FILE_C, entriesReverse.get(1)); } + @Test + public void harmfulMutationsOfEditsAreNotPossibleForIntraLineDiffArgsAndCachedValue() + throws Exception { + String a = "First line\nSecond line\n"; + String b = "1st line\n2nd line\n"; + Text aText = new Text(a.getBytes(UTF_8)); + Text bText = new Text(b.getBytes(UTF_8)); + Edit inputEdit = new Edit(0, 2, 0, 2); + List<Edit> inputEdits = new ArrayList<>(ImmutableList.of(inputEdit)); + + IntraLineDiffKey diffKey = + IntraLineDiffKey.create(ObjectId.zeroId(), ObjectId.zeroId(), Whitespace.IGNORE_NONE); + IntraLineDiffArgs diffArgs = + IntraLineDiffArgs.create(aText, bText, inputEdits, project, ObjectId.zeroId(), "file.txt"); + IntraLineDiff intraLineDiff = patchListCache.getIntraLineDiff(diffKey, diffArgs); + + Edit outputEdit = Iterables.getOnlyElement(intraLineDiff.getEdits()); + + outputEdit.shift(5); + inputEdit.shift(7); + inputEdits.add(new Edit(43, 47, 50, 51)); + + Edit originalEdit = new Edit(0, 2, 0, 2); + assertThat(diffArgs.edits()).containsExactly(originalEdit); + assertThat(intraLineDiff.getEdits()).containsExactly(originalEdit); + } + private static void assertAdded(String expectedNewName, PatchListEntry e) { assertName(expectedNewName, e); assertThat(e.getChangeType()).isEqualTo(ChangeType.ADDED); @@ -198,7 +234,7 @@ } private PatchListKey getKey(ObjectId revisionIdA, ObjectId revisionIdB) { - return new PatchListKey(revisionIdA, revisionIdB, Whitespace.IGNORE_NONE); + return PatchListKey.againstCommit(revisionIdA, revisionIdB, Whitespace.IGNORE_NONE); } private ObjectId getCurrentRevisionId(String changeId) throws Exception {
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java new file mode 100644 index 0000000..1845103 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/ChangeNotificationsIT.java
@@ -0,0 +1,2265 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.mail; + +import static com.google.common.truth.TruthJUnit.assume; +import static com.google.gerrit.extensions.api.changes.NotifyHandling.ALL; +import static com.google.gerrit.extensions.api.changes.NotifyHandling.NONE; +import static com.google.gerrit.extensions.api.changes.NotifyHandling.OWNER; +import static com.google.gerrit.extensions.api.changes.NotifyHandling.OWNER_REVIEWERS; +import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.CC_ON_OWN_COMMENTS; +import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.ENABLED; +import static com.google.gerrit.server.account.WatchConfig.NotifyType.ABANDONED_CHANGES; +import static com.google.gerrit.server.account.WatchConfig.NotifyType.ALL_COMMENTS; +import static com.google.gerrit.server.account.WatchConfig.NotifyType.NEW_CHANGES; +import static com.google.gerrit.server.account.WatchConfig.NotifyType.NEW_PATCHSETS; +import static com.google.gerrit.server.account.WatchConfig.NotifyType.SUBMITTED_CHANGES; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.acceptance.AbstractNotificationTest; +import com.google.gerrit.acceptance.TestAccount; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.extensions.api.changes.AbandonInput; +import com.google.gerrit.extensions.api.changes.AddReviewerInput; +import com.google.gerrit.extensions.api.changes.AssigneeInput; +import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; +import com.google.gerrit.extensions.api.changes.DeleteVoteInput; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.api.changes.SubmitInput; +import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy; +import com.google.gerrit.extensions.client.ReviewerState; +import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.project.Util; +import org.junit.Before; +import org.junit.Test; + +public class ChangeNotificationsIT extends AbstractNotificationTest { + /* + * Set up for extra standard test accounts and permissions. + */ + private TestAccount other; + private TestAccount extraReviewer; + private TestAccount extraCcer; + + @Before + public void createExtraAccounts() throws Exception { + extraReviewer = + accountCreator.create("extraReviewer", "extraReviewer@example.com", "extraReviewer"); + extraCcer = accountCreator.create("extraCcer", "extraCcer@example.com", "extraCcer"); + other = accountCreator.create("other", "other@example.com", "other"); + } + + @Before + public void grantPermissions() throws Exception { + grant(project, "refs/*", Permission.FORGE_COMMITTER, false, REGISTERED_USERS); + grant(project, "refs/*", Permission.SUBMIT, false, REGISTERED_USERS); + grant(project, "refs/heads/master", Permission.ABANDON, false, REGISTERED_USERS); + ProjectConfig cfg = projectCache.get(project).getConfig(); + Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2, REGISTERED_USERS, "refs/*"); + } + + /* + * AbandonedSender tests. + */ + + @Test + public void abandonReviewableChangeByOwner() throws Exception { + StagedChange sc = stageReviewableChange(); + abandon(sc.changeId, sc.owner); + assertThat(sender) + .sent("abandon", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ABANDONED_CHANGES) + .noOneElse(); + } + + @Test + public void abandonReviewableChangeByOwnerCcingSelf() throws Exception { + StagedChange sc = stageReviewableChange(); + abandon(sc.changeId, sc.owner, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("abandon", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ABANDONED_CHANGES) + .noOneElse(); + } + + @Test + public void abandonReviewableChangeByOther() throws Exception { + StagedChange sc = stageReviewableChange(); + TestAccount other = accountCreator.create("other", "other@example.com", "other"); + abandon(sc.changeId, other); + assertThat(sender) + .sent("abandon", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ABANDONED_CHANGES) + .noOneElse(); + } + + @Test + public void abandonReviewableChangeByOtherCcingSelf() throws Exception { + StagedChange sc = stageReviewableChange(); + TestAccount other = accountCreator.create("other", "other@example.com", "other"); + abandon(sc.changeId, other, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("abandon", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer, other) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ABANDONED_CHANGES) + .noOneElse(); + } + + @Test + public void abandonReviewableChangeNotifyOwnersReviewers() throws Exception { + StagedChange sc = stageReviewableChange(); + abandon(sc.changeId, sc.owner, OWNER_REVIEWERS); + assertThat(sender) + .sent("abandon", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void abandonReviewableChangeNotifyOwner() throws Exception { + StagedChange sc = stageReviewableChange(); + abandon(sc.changeId, sc.owner, OWNER); + assertThat(sender).notSent(); + } + + @Test + public void abandonReviewableChangeNotifyOwnerCcingSelf() throws Exception { + StagedChange sc = stageReviewableChange(); + abandon(sc.changeId, sc.owner, CC_ON_OWN_COMMENTS, OWNER); + // Self-CC applies *after* need for sending notification is determined. + // Since there are no recipients before including the user taking action, + // there should no notification sent. + assertThat(sender).notSent(); + } + + @Test + public void abandonReviewableChangeByOtherCcingSelfNotifyOwner() throws Exception { + StagedChange sc = stageReviewableChange(); + TestAccount other = accountCreator.create("other", "other@example.com", "other"); + abandon(sc.changeId, other, CC_ON_OWN_COMMENTS, OWNER); + assertThat(sender).sent("abandon", sc).to(sc.owner).cc(other).noOneElse(); + } + + @Test + public void abandonReviewableChangeNotifyNone() throws Exception { + StagedChange sc = stageReviewableChange(); + abandon(sc.changeId, sc.owner, NONE); + assertThat(sender).notSent(); + } + + @Test + public void abandonReviewableChangeNotifyNoneCcingSelf() throws Exception { + StagedChange sc = stageReviewableChange(); + abandon(sc.changeId, sc.owner, CC_ON_OWN_COMMENTS, NONE); + assertThat(sender).notSent(); + } + + @Test + public void abandonReviewableWipChange() throws Exception { + StagedChange sc = stageReviewableWipChange(); + abandon(sc.changeId, sc.owner); + assertThat(sender) + .sent("abandon", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ABANDONED_CHANGES) + .noOneElse(); + } + + @Test + public void abandonWipChange() throws Exception { + StagedChange sc = stageWipChange(); + abandon(sc.changeId, sc.owner); + assertThat(sender).notSent(); + } + + @Test + public void abandonWipChangeNotifyAll() throws Exception { + StagedChange sc = stageWipChange(); + abandon(sc.changeId, sc.owner, ALL); + assertThat(sender) + .sent("abandon", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ABANDONED_CHANGES) + .noOneElse(); + } + + private void abandon(String changeId, TestAccount by) throws Exception { + abandon(changeId, by, ENABLED); + } + + private void abandon(String changeId, TestAccount by, EmailStrategy emailStrategy) + throws Exception { + abandon(changeId, by, emailStrategy, null); + } + + private void abandon(String changeId, TestAccount by, @Nullable NotifyHandling notify) + throws Exception { + abandon(changeId, by, ENABLED, notify); + } + + private void abandon( + String changeId, TestAccount by, EmailStrategy emailStrategy, @Nullable NotifyHandling notify) + throws Exception { + setEmailStrategy(by, emailStrategy); + setApiUser(by); + AbandonInput in = new AbandonInput(); + if (notify != null) { + in.notify = notify; + } + gApi.changes().id(changeId).abandon(in); + } + + /* + * AddReviewerSender tests. + */ + + private void addReviewerToReviewableChangeInReviewDb(Adder adder) throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageReviewableChange(); + TestAccount reviewer = accountCreator.create("added", "added@example.com", "added"); + addReviewer(adder, sc.changeId, sc.owner, reviewer.email); + assertThat(sender) + .sent("newchange", sc) + .to(reviewer) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void addReviewerToReviewableChangeInReviewDbSingly() throws Exception { + addReviewerToReviewableChangeInReviewDb(singly()); + } + + @Test + public void addReviewerToReviewableChangeInReviewDbBatch() throws Exception { + addReviewerToReviewableChangeInReviewDb(batch()); + } + + private void addReviewerToReviewableChangeInNoteDb(Adder adder) throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + TestAccount reviewer = accountCreator.create("added", "added@example.com", "added"); + addReviewer(adder, sc.changeId, sc.owner, reviewer.email); + // TODO(logan): Should CCs be included? + assertThat(sender) + .sent("newchange", sc) + .to(reviewer) + .cc(sc.reviewer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void addReviewerToReviewableChangeInNoteDbSingly() throws Exception { + addReviewerToReviewableChangeInNoteDb(singly()); + } + + @Test + public void addReviewerToReviewableChangeInNoteDbBatch() throws Exception { + addReviewerToReviewableChangeInNoteDb(batch()); + } + + private void addReviewerToReviewableChangeByOwnerCcingSelfInNoteDb(Adder adder) throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + TestAccount reviewer = accountCreator.create("added", "added@example.com", "added"); + 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, sc.reviewer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void addReviewerToReviewableChangeByOwnerCcingSelfInNoteDbSingly() throws Exception { + addReviewerToReviewableChangeByOwnerCcingSelfInNoteDb(singly()); + } + + @Test + public void addReviewerToReviewableChangeByOwnerCcingSelfInNoteDbBatch() throws Exception { + addReviewerToReviewableChangeByOwnerCcingSelfInNoteDb(batch()); + } + + private void addReviewerToReviewableChangeByOtherInNoteDb(Adder adder) throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + TestAccount other = accountCreator.create("other", "other@example.com", "other"); + StagedChange sc = stageReviewableChange(); + TestAccount reviewer = accountCreator.create("added", "added@example.com", "added"); + addReviewer(adder, sc.changeId, other, reviewer.email); + // TODO(logan): Should CCs be included? + assertThat(sender) + .sent("newchange", sc) + .to(reviewer) + .cc(sc.owner, sc.reviewer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void addReviewerToReviewableChangeByOtherInNoteDbSingly() throws Exception { + addReviewerToReviewableChangeByOtherInNoteDb(singly()); + } + + @Test + public void addReviewerToReviewableChangeByOtherInNoteDbBatch() throws Exception { + addReviewerToReviewableChangeByOtherInNoteDb(batch()); + } + + private void addReviewerToReviewableChangeByOtherCcingSelfInNoteDb(Adder adder) throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + TestAccount other = accountCreator.create("other", "other@example.com", "other"); + StagedChange sc = stageReviewableChange(); + TestAccount reviewer = accountCreator.create("added", "added@example.com", "added"); + 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(sc.owner, sc.reviewer, other) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void addReviewerToReviewableChangeByOtherCcingSelfInNoteDbSingly() throws Exception { + addReviewerToReviewableChangeByOtherCcingSelfInNoteDb(singly()); + } + + @Test + public void addReviewerToReviewableChangeByOtherCcingSelfInNoteDbBatch() throws Exception { + addReviewerToReviewableChangeByOtherCcingSelfInNoteDb(batch()); + } + + private void addReviewerByEmailToReviewableChangeInReviewDb(Adder adder) throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + String email = "addedbyemail@example.com"; + StagedChange sc = stageReviewableChange(); + addReviewer(adder, sc.changeId, sc.owner, email); + assertThat(sender).notSent(); + } + + @Test + public void addReviewerByEmailToReviewableChangeInReviewDbSingly() throws Exception { + addReviewerByEmailToReviewableChangeInReviewDb(singly()); + } + + @Test + public void addReviewerByEmailToReviewableChangeInReviewDbBatch() throws Exception { + addReviewerByEmailToReviewableChangeInReviewDb(batch()); + } + + private void addReviewerByEmailToReviewableChangeInNoteDb(Adder adder) throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + String email = "addedbyemail@example.com"; + StagedChange sc = stageReviewableChange(); + addReviewer(adder, sc.changeId, sc.owner, email); + // TODO(logan): Should CCs be included? + assertThat(sender) + .sent("newchange", sc) + .to(email) + .cc(sc.reviewer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void addReviewerByEmailToReviewableChangeInNoteDbSingly() throws Exception { + addReviewerByEmailToReviewableChangeInNoteDb(singly()); + } + + @Test + public void addReviewerByEmailToReviewableChangeInNoteDbBatch() throws Exception { + addReviewerByEmailToReviewableChangeInNoteDb(batch()); + } + + private void addReviewerToWipChange(Adder adder) throws Exception { + StagedChange sc = stageWipChange(); + TestAccount reviewer = accountCreator.create("added", "added@example.com", "added"); + addReviewer(adder, sc.changeId, sc.owner, reviewer.email); + assertThat(sender).notSent(); + } + + @Test + public void addReviewerToWipChangeSingly() throws Exception { + addReviewerToWipChange(singly()); + } + + @Test + public void addReviewerToWipChangeBatch() throws Exception { + addReviewerToWipChange(batch()); + } + + private void addReviewerToReviewableWipChange(Adder adder) throws Exception { + StagedChange sc = stageReviewableWipChange(); + TestAccount reviewer = accountCreator.create("added", "added@example.com", "added"); + addReviewer(adder, sc.changeId, sc.owner, reviewer.email); + assertThat(sender).notSent(); + } + + @Test + public void addReviewerToReviewableWipChangeSingly() throws Exception { + addReviewerToReviewableWipChange(singly()); + } + + @Test + public void addReviewerToReviewableWipChangeBatch() throws Exception { + addReviewerToReviewableWipChange(batch()); + } + + private void addReviewerToWipChangeInNoteDbNotifyAll(Adder adder) throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageWipChange(); + TestAccount reviewer = accountCreator.create("added", "added@example.com", "added"); + addReviewer(adder, sc.changeId, sc.owner, reviewer.email, NotifyHandling.ALL); + // TODO(logan): Should CCs be included? + assertThat(sender) + .sent("newchange", sc) + .to(reviewer) + .cc(sc.reviewer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void addReviewerToWipChangeInNoteDbNotifyAllSingly() throws Exception { + addReviewerToWipChangeInNoteDbNotifyAll(singly()); + } + + @Test + public void addReviewerToWipChangeInNoteDbNotifyAllBatch() throws Exception { + addReviewerToWipChangeInNoteDbNotifyAll(batch()); + } + + private void addReviewerToWipChangeInReviewDbNotifyAll(Adder adder) throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageWipChange(); + TestAccount reviewer = accountCreator.create("added", "added@example.com", "added"); + addReviewer(adder, sc.changeId, sc.owner, reviewer.email, NotifyHandling.ALL); + assertThat(sender) + .sent("newchange", sc) + .to(reviewer) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void addReviewerToWipChangeInReviewDbNotifyAllSingly() throws Exception { + addReviewerToWipChangeInReviewDbNotifyAll(singly()); + } + + @Test + public void addReviewerToWipChangeInReviewDbNotifyAllBatch() throws Exception { + addReviewerToWipChangeInReviewDbNotifyAll(batch()); + } + + private void addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewers(Adder adder) + throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + TestAccount reviewer = accountCreator.create("added", "added@example.com", "added"); + addReviewer(adder, sc.changeId, sc.owner, reviewer.email, OWNER_REVIEWERS); + // TODO(logan): Should CCs be included? + assertThat(sender) + .sent("newchange", sc) + .to(reviewer) + .cc(sc.reviewer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewersSingly() throws Exception { + addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewers(singly()); + } + + @Test + public void addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewersBatch() throws Exception { + addReviewerToReviewableChangeInNoteDbNotifyOwnerReviewers(batch()); + } + + private void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwner(Adder adder) + throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + TestAccount reviewer = accountCreator.create("added", "added@example.com", "added"); + addReviewer(adder, sc.changeId, sc.owner, reviewer.email, CC_ON_OWN_COMMENTS, OWNER); + assertThat(sender).notSent(); + } + + @Test + public void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwnerSingly() + throws Exception { + addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwner(singly()); + } + + @Test + public void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwnerBatch() + throws Exception { + addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyOwner(batch()); + } + + private void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNone(Adder adder) + throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + TestAccount reviewer = accountCreator.create("added", "added@example.com", "added"); + addReviewer(adder, sc.changeId, sc.owner, reviewer.email, CC_ON_OWN_COMMENTS, NONE); + assertThat(sender).notSent(); + } + + @Test + public void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNoneSingly() + throws Exception { + addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNone(singly()); + } + + @Test + public void addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNoneBatch() + throws Exception { + addReviewerToReviewableChangeInNoteDbByOwnerCcingSelfNotifyNone(batch()); + } + + private interface Adder { + void addReviewer(String changeId, String reviewer, @Nullable NotifyHandling notify) + throws Exception; + } + + private Adder singly() { + return (String changeId, String reviewer, @Nullable NotifyHandling notify) -> { + AddReviewerInput in = new AddReviewerInput(); + in.reviewer = reviewer; + if (notify != null) { + in.notify = notify; + } + gApi.changes().id(changeId).addReviewer(in); + }; + } + + private Adder batch() { + return (String changeId, String reviewer, @Nullable NotifyHandling notify) -> { + ReviewInput in = ReviewInput.noScore(); + in.reviewer(reviewer); + if (notify != null) { + in.notify = notify; + } + gApi.changes().id(changeId).revision("current").review(in); + }; + } + + private void addReviewer(Adder adder, String changeId, TestAccount by, String reviewer) + throws Exception { + addReviewer(adder, changeId, by, reviewer, ENABLED, null); + } + + private void addReviewer( + Adder adder, String changeId, TestAccount by, String reviewer, NotifyHandling notify) + throws Exception { + addReviewer(adder, changeId, by, reviewer, ENABLED, notify); + } + + private void addReviewer( + Adder adder, + String changeId, + TestAccount by, + String reviewer, + EmailStrategy emailStrategy, + @Nullable NotifyHandling notify) + throws Exception { + setEmailStrategy(by, emailStrategy); + setApiUser(by); + adder.addReviewer(changeId, reviewer, notify); + } + + /* + * CommentSender tests. + */ + + @Test + public void commentOnReviewableChangeByOwner() throws Exception { + StagedChange sc = stageReviewableChange(); + review(sc.owner, sc.changeId, ENABLED); + assertThat(sender) + .sent("comment", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void commentOnReviewableChangeByReviewer() throws Exception { + StagedChange sc = stageReviewableChange(); + review(sc.reviewer, sc.changeId, ENABLED); + assertThat(sender) + .sent("comment", sc) + .to(sc.owner) + .cc(sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void commentOnReviewableChangeByOwnerCcingSelf() throws Exception { + StagedChange sc = stageReviewableChange(); + review(sc.owner, sc.changeId, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("comment", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void commentOnReviewableChangeByReviewerCcingSelf() throws Exception { + StagedChange sc = stageReviewableChange(); + review(sc.reviewer, sc.changeId, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("comment", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void commentOnReviewableChangeByOther() throws Exception { + TestAccount other = accountCreator.create("other", "other@example.com", "other"); + StagedChange sc = stageReviewableChange(); + review(other, sc.changeId, ENABLED); + assertThat(sender) + .sent("comment", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void commentOnReviewableChangeByOtherCcingSelf() throws Exception { + TestAccount other = accountCreator.create("other", "other@example.com", "other"); + StagedChange sc = stageReviewableChange(); + review(other, sc.changeId, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("comment", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer, other) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void commentOnReviewableChangeByOwnerNotifyOwnerReviewers() throws Exception { + StagedChange sc = stageReviewableChange(); + review(sc.owner, sc.changeId, ENABLED, OWNER_REVIEWERS); + assertThat(sender) + .sent("comment", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void commentOnReviewableChangeByOwnerNotifyOwner() throws Exception { + StagedChange sc = stageReviewableChange(); + review(sc.owner, sc.changeId, ENABLED, OWNER); + assertThat(sender).notSent(); + } + + @Test + public void commentOnReviewableChangeByOwnerCcingSelfNotifyOwner() throws Exception { + StagedChange sc = stageReviewableChange(); + setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS); + review(sc.owner, sc.changeId, ENABLED, OWNER); + assertThat(sender).notSent(); // TODO(logan): Why not send to owner? + } + + @Test + public void commentOnReviewableChangeByOwnerNotifyNone() throws Exception { + StagedChange sc = stageReviewableChange(); + review(sc.owner, sc.changeId, ENABLED, NONE); + assertThat(sender).notSent(); + } + + @Test + public void commentOnReviewableChangeByOwnerCcingSelfNotifyNone() throws Exception { + StagedChange sc = stageReviewableChange(); + setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS); + review(sc.owner, sc.changeId, ENABLED, NONE); + assertThat(sender).notSent(); // TODO(logan): Why not send to owner? + } + + @Test + public void commentOnReviewableChangeByBot() throws Exception { + StagedChange sc = stageReviewableChange(); + TestAccount bot = sc.testAccount("bot"); + review(bot, sc.changeId, ENABLED, null, "autogenerated:bot"); + assertThat(sender) + .sent("comment", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void commentOnWipChangeByOwner() throws Exception { + StagedChange sc = stageWipChange(); + review(sc.owner, sc.changeId, ENABLED); + assertThat(sender).notSent(); + } + + @Test + public void commentOnWipChangeByOwnerCcingSelf() throws Exception { + StagedChange sc = stageWipChange(); + review(sc.owner, sc.changeId, CC_ON_OWN_COMMENTS); + assertThat(sender).notSent(); + } + + @Test + public void commentOnWipChangeByOwnerNotifyAll() throws Exception { + StagedChange sc = stageWipChange(); + review(sc.owner, sc.changeId, ENABLED, ALL); + assertThat(sender) + .sent("comment", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void commentOnWipChangeByBot() throws Exception { + StagedChange sc = stageWipChange(); + TestAccount bot = sc.testAccount("bot"); + review(bot, sc.changeId, ENABLED, null, "autogenerated:tag"); + assertThat(sender).sent("comment", sc).to(sc.owner).noOneElse(); + } + + @Test + public void commentOnReviewableWipChangeByBot() throws Exception { + StagedChange sc = stageReviewableWipChange(); + TestAccount bot = sc.testAccount("bot"); + review(bot, sc.changeId, ENABLED, null, "autogenerated:tag"); + assertThat(sender).sent("comment", sc).to(sc.owner).noOneElse(); + } + + @Test + public void commentOnReviewableWipChangeByBotNotifyAll() throws Exception { + StagedChange sc = stageWipChange(); + TestAccount bot = sc.testAccount("bot"); + review(bot, sc.changeId, ENABLED, ALL, "tag"); + assertThat(sender) + .sent("comment", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void commentOnReviewableWipChangeByOwner() throws Exception { + StagedChange sc = stageReviewableWipChange(); + review(sc.owner, sc.changeId, ENABLED); + assertThat(sender) + .sent("comment", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + private void review(TestAccount account, String changeId, EmailStrategy strategy) + throws Exception { + review(account, changeId, strategy, null); + } + + private void review( + TestAccount account, String changeId, EmailStrategy strategy, @Nullable NotifyHandling notify) + throws Exception { + review(account, changeId, strategy, notify, null); + } + + private void review( + TestAccount account, + String changeId, + EmailStrategy strategy, + @Nullable NotifyHandling notify, + @Nullable String tag) + throws Exception { + setEmailStrategy(account, strategy); + ReviewInput in = ReviewInput.recommend(); + in.notify = notify; + in.tag = tag; + gApi.changes().id(changeId).revision("current").review(in); + } + + /* + * CreateChangeSender tests. + */ + + @Test + public void createReviewableChange() throws Exception { + StagedPreChange spc = stagePreChange("refs/for/master"); + assertThat(sender) + .sent("newchange", spc) + .to(spc.watchingProjectOwner) + .bcc(NEW_CHANGES, NEW_PATCHSETS) + .noOneElse(); + } + + @Test + public void createWipChange() throws Exception { + stagePreChange("refs/for/master%wip"); + assertThat(sender).notSent(); + } + + @Test + public void createReviewableChangeWithNotifyOwnerReviewers() throws Exception { + stagePreChange("refs/for/master%notify=OWNER_REVIEWERS"); + assertThat(sender).notSent(); + } + + @Test + public void createReviewableChangeWithNotifyOwner() throws Exception { + stagePreChange("refs/for/master%notify=OWNER"); + assertThat(sender).notSent(); + } + + @Test + public void createReviewableChangeWithNotifyNone() throws Exception { + stagePreChange("refs/for/master%notify=OWNER"); + assertThat(sender).notSent(); + } + + @Test + public void createWipChangeWithNotifyAll() throws Exception { + StagedPreChange spc = stagePreChange("refs/for/master%wip,notify=ALL"); + assertThat(sender) + .sent("newchange", spc) + .to(spc.watchingProjectOwner) + .bcc(NEW_CHANGES, NEW_PATCHSETS) + .noOneElse(); + } + + @Test + public void createReviewableChangeWithReviewersAndCcs() throws Exception { + // TODO(logan): Support reviewers/CCs-by-email via push option. + StagedPreChange spc = + stagePreChange( + "refs/for/master", + users -> ImmutableList.of("r=" + users.reviewer.username, "cc=" + users.ccer.username)); + assertThat(sender) + .sent("newchange", spc) + .to(spc.reviewer, spc.watchingProjectOwner) + .cc(spc.ccer) + .bcc(NEW_CHANGES, NEW_PATCHSETS) + .noOneElse(); + } + + /* + * DeleteReviewerSender tests. + */ + + @Test + public void deleteReviewerFromReviewableChange() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + setApiUser(sc.owner); + removeReviewer(sc, extraReviewer); + assertThat(sender) + .sent("deleteReviewer", sc) + .to(extraReviewer) + .cc(extraCcer, sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void deleteReviewerFromReviewableChangeByOwnerCcingSelf() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + setEmailStrategy(sc.owner, EmailStrategy.CC_ON_OWN_COMMENTS); + removeReviewer(sc, extraReviewer); + assertThat(sender) + .sent("deleteReviewer", sc) + .to(sc.owner, extraReviewer) + .cc(extraCcer, sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void deleteReviewerFromReviewableChangeByAdmin() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + setApiUser(admin); + removeReviewer(sc, extraReviewer); + assertThat(sender) + .sent("deleteReviewer", sc) + .to(sc.owner, extraReviewer) + .cc(extraCcer, sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void deleteReviewerFromReviewableChangeByAdminCcingSelf() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + setEmailStrategy(admin, EmailStrategy.CC_ON_OWN_COMMENTS); + setApiUser(admin); + removeReviewer(sc, extraReviewer); + assertThat(sender) + .sent("deleteReviewer", sc) + .to(sc.owner, extraReviewer) + .cc(admin, extraCcer, sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void deleteCcerFromReviewableChange() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + setApiUser(sc.owner); + removeReviewer(sc, extraCcer); + assertThat(sender) + .sent("deleteReviewer", sc) + .to(extraCcer) + .cc(sc.reviewer, sc.ccer, extraReviewer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void deleteReviewerFromReviewableChangeNotifyOwnerReviewers() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + setApiUser(sc.owner); + removeReviewer(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS); + assertThat(sender) + .sent("deleteReviewer", sc) + .to(extraReviewer) + .cc(extraCcer, sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void deleteReviewerFromReviewableChangeNotifyOwner() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + removeReviewer(sc, extraReviewer, NotifyHandling.OWNER); + assertThat(sender).notSent(); + } + + @Test + public void deleteReviewerFromReviewableChangeByOwnerCcingSelfNotifyOwner() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + setEmailStrategy(sc.owner, EmailStrategy.CC_ON_OWN_COMMENTS); + removeReviewer(sc, extraReviewer, NotifyHandling.OWNER); + assertThat(sender).sent("deleteReviewer", sc).to(sc.owner, extraReviewer).noOneElse(); + } + + @Test + public void deleteReviewerFromReviewableChangeNotifyNone() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + removeReviewer(sc, extraReviewer, NotifyHandling.NONE); + assertThat(sender).notSent(); + } + + @Test + public void deleteReviewerFromReviewableChangeByOwnerCcingSelfNotifyNone() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + setEmailStrategy(sc.owner, EmailStrategy.CC_ON_OWN_COMMENTS); + removeReviewer(sc, extraReviewer, NotifyHandling.NONE); + assertThat(sender).notSent(); + } + + @Test + public void deleteReviewerFromReviewableWipChange() throws Exception { + StagedChange sc = stageReviewableWipChangeWithExtraReviewer(); + removeReviewer(sc, extraReviewer); + assertThat(sender).notSent(); + } + + @Test + public void deleteReviewerFromWipChange() throws Exception { + StagedChange sc = stageWipChangeWithExtraReviewer(); + removeReviewer(sc, extraReviewer); + assertThat(sender).notSent(); + } + + @Test + public void deleteReviewerFromWipChangeNotifyAll() throws Exception { + StagedChange sc = stageWipChangeWithExtraReviewer(); + setApiUser(sc.owner); + removeReviewer(sc, extraReviewer, NotifyHandling.ALL); + assertThat(sender) + .sent("deleteReviewer", sc) + .to(extraReviewer) + .cc(extraCcer, sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void deleteReviewerWithApprovalFromWipChange() throws Exception { + StagedChange sc = stageWipChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + setApiUser(sc.owner); + removeReviewer(sc, extraReviewer); + assertThat(sender).sent("deleteReviewer", sc).to(extraReviewer).noOneElse(); + } + + @Test + public void deleteReviewerWithApprovalFromWipChangeNotifyOwner() throws Exception { + StagedChange sc = stageWipChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + removeReviewer(sc, extraReviewer, NotifyHandling.OWNER); + assertThat(sender).notSent(); + } + + @Test + public void deleteReviewerByEmailFromWipChangeInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageWipChangeWithExtraReviewer(); + gApi.changes().id(sc.changeId).reviewer(sc.reviewerByEmail).remove(); + assertThat(sender).notSent(); + } + + private void recommend(StagedChange sc, TestAccount by) throws Exception { + setApiUser(by); + gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.recommend()); + } + + private interface Stager { + StagedChange stage() throws Exception; + } + + private StagedChange stageChangeWithExtraReviewer(Stager stager) throws Exception { + StagedChange sc = stager.stage(); + ReviewInput in = + ReviewInput.noScore() + .reviewer(extraReviewer.email) + .reviewer(extraCcer.email, ReviewerState.CC, false); + setApiUser(extraReviewer); + gApi.changes().id(sc.changeId).revision("current").review(in); + return sc; + } + + private StagedChange stageReviewableChangeWithExtraReviewer() throws Exception { + return stageChangeWithExtraReviewer(this::stageReviewableChange); + } + + private StagedChange stageReviewableWipChangeWithExtraReviewer() throws Exception { + return stageChangeWithExtraReviewer(this::stageReviewableWipChange); + } + + private StagedChange stageWipChangeWithExtraReviewer() throws Exception { + return stageChangeWithExtraReviewer(this::stageWipChange); + } + + private void removeReviewer(StagedChange sc, TestAccount account) throws Exception { + sender.clear(); + gApi.changes().id(sc.changeId).reviewer(account.email).remove(); + } + + private void removeReviewer(StagedChange sc, TestAccount account, NotifyHandling notify) + throws Exception { + sender.clear(); + DeleteReviewerInput in = new DeleteReviewerInput(); + in.notify = notify; + gApi.changes().id(sc.changeId).reviewer(account.email).remove(in); + } + + /* + * DeleteVoteSender tests. + */ + + @Test + public void deleteVoteFromReviewableChange() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + setApiUser(sc.owner); + deleteVote(sc, extraReviewer); + assertThat(sender) + .sent("deleteVote", sc) + .cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void deleteVoteFromReviewableChangeWithSelfCc() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS); + setApiUser(sc.owner); + deleteVote(sc, extraReviewer); + assertThat(sender) + .sent("deleteVote", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void deleteVoteFromReviewableChangeByAdmin() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + setApiUser(admin); + deleteVote(sc, extraReviewer); + assertThat(sender) + .sent("deleteVote", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void deleteVoteFromReviewableChangeByAdminCcingSelf() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + setEmailStrategy(admin, EmailStrategy.CC_ON_OWN_COMMENTS); + setApiUser(admin); + deleteVote(sc, extraReviewer); + assertThat(sender) + .sent("deleteVote", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer, admin, extraReviewer, extraCcer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void deleteVoteFromReviewableChangeNotifyOwnerReviewers() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + setApiUser(sc.owner); + deleteVote(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS); + assertThat(sender) + .sent("deleteVote", sc) + .cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void deleteVoteFromReviewableChangeNotifyOwnerReviewersWithSelfCc() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS); + setApiUser(sc.owner); + deleteVote(sc, extraReviewer, NotifyHandling.OWNER_REVIEWERS); + assertThat(sender) + .sent("deleteVote", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void deleteVoteFromReviewableChangeNotifyOwner() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + setApiUser(admin); + deleteVote(sc, extraReviewer, NotifyHandling.OWNER); + assertThat(sender).sent("deleteVote", sc).to(sc.owner).noOneElse(); + } + + @Test + public void deleteVoteFromReviewableChangeNotifyNone() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + setApiUser(sc.owner); + deleteVote(sc, extraReviewer, NotifyHandling.NONE); + assertThat(sender).notSent(); + } + + @Test + public void deleteVoteFromReviewableChangeNotifyNoneWithSelfCc() throws Exception { + StagedChange sc = stageReviewableChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS); + setApiUser(sc.owner); + deleteVote(sc, extraReviewer, NotifyHandling.NONE); + assertThat(sender).notSent(); + } + + @Test + public void deleteVoteFromReviewableWipChange() throws Exception { + StagedChange sc = stageReviewableWipChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + setApiUser(sc.owner); + deleteVote(sc, extraReviewer); + assertThat(sender) + .sent("deleteVote", sc) + .cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void deleteVoteFromWipChange() throws Exception { + StagedChange sc = stageWipChangeWithExtraReviewer(); + recommend(sc, extraReviewer); + setApiUser(sc.owner); + deleteVote(sc, extraReviewer); + assertThat(sender) + .sent("deleteVote", sc) + .cc(sc.reviewer, sc.ccer, extraReviewer, extraCcer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + private void deleteVote(StagedChange sc, TestAccount account) throws Exception { + sender.clear(); + gApi.changes().id(sc.changeId).reviewer(account.email).deleteVote("Code-Review"); + } + + private void deleteVote(StagedChange sc, TestAccount account, NotifyHandling notify) + throws Exception { + sender.clear(); + DeleteVoteInput in = new DeleteVoteInput(); + in.label = "Code-Review"; + in.notify = notify; + gApi.changes().id(sc.changeId).reviewer(account.email).deleteVote(in); + } + + /* + * MergedSender tests. + */ + + @Test + public void mergeByOwner() throws Exception { + StagedChange sc = stageChangeReadyForMerge(); + merge(sc.changeId, sc.owner); + assertThat(sender) + .sent("merged", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS, SUBMITTED_CHANGES) + .noOneElse(); + } + + @Test + public void mergeByOwnerCcingSelf() throws Exception { + StagedChange sc = stageChangeReadyForMerge(); + merge(sc.changeId, sc.owner, EmailStrategy.CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("merged", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS, SUBMITTED_CHANGES) + .noOneElse(); + } + + @Test + public void mergeByReviewer() throws Exception { + StagedChange sc = stageChangeReadyForMerge(); + merge(sc.changeId, sc.reviewer); + assertThat(sender) + .sent("merged", sc) + .to(sc.owner) + .cc(sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS, SUBMITTED_CHANGES) + .noOneElse(); + } + + @Test + public void mergeByReviewerCcingSelf() throws Exception { + StagedChange sc = stageChangeReadyForMerge(); + merge(sc.changeId, sc.reviewer, EmailStrategy.CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("merged", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS, SUBMITTED_CHANGES) + .noOneElse(); + } + + @Test + public void mergeByOtherNotifyOwnerReviewers() throws Exception { + StagedChange sc = stageChangeReadyForMerge(); + merge(sc.changeId, other, OWNER_REVIEWERS); + assertThat(sender) + .sent("merged", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void mergeByOtherNotifyOwner() throws Exception { + StagedChange sc = stageChangeReadyForMerge(); + merge(sc.changeId, other, OWNER); + assertThat(sender).sent("merged", sc).to(sc.owner).noOneElse(); + } + + @Test + public void mergeByOtherCcingSelfNotifyOwner() throws Exception { + StagedChange sc = stageChangeReadyForMerge(); + setEmailStrategy(other, EmailStrategy.CC_ON_OWN_COMMENTS); + merge(sc.changeId, other, OWNER); + assertThat(sender).sent("merged", sc).to(sc.owner).noOneElse(); + } + + @Test + public void mergeByOtherNotifyNone() throws Exception { + StagedChange sc = stageChangeReadyForMerge(); + merge(sc.changeId, other, NONE); + assertThat(sender).notSent(); + } + + @Test + public void mergeByOtherCcingSelfNotifyNone() throws Exception { + StagedChange sc = stageChangeReadyForMerge(); + setEmailStrategy(other, EmailStrategy.CC_ON_OWN_COMMENTS); + merge(sc.changeId, other, NONE); + assertThat(sender).notSent(); + } + + private void merge(String changeId, TestAccount by) throws Exception { + merge(changeId, by, ENABLED); + } + + private void merge(String changeId, TestAccount by, EmailStrategy emailStrategy) + throws Exception { + setEmailStrategy(by, emailStrategy); + setApiUser(by); + gApi.changes().id(changeId).revision("current").submit(); + } + + private void merge(String changeId, TestAccount by, NotifyHandling notify) throws Exception { + merge(changeId, by, ENABLED, notify); + } + + private void merge( + String changeId, TestAccount by, EmailStrategy emailStrategy, NotifyHandling notify) + throws Exception { + setEmailStrategy(by, emailStrategy); + setApiUser(by); + SubmitInput in = new SubmitInput(); + in.notify = notify; + gApi.changes().id(changeId).revision("current").submit(in); + } + + private StagedChange stageChangeReadyForMerge() throws Exception { + StagedChange sc = stageReviewableChange(); + setApiUser(sc.reviewer); + gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.approve()); + sender.clear(); + return sc; + } + + /* + * ReplacePatchSetSender tests. + */ + + @Test + public void newPatchSetByOwnerOnReviewableChangeInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master", sc.owner); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer) + .cc(sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + } + + @Test + public void newPatchSetByOwnerOnReviewableChangeInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master", sc.owner); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer, sc.ccer) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + } + + @Test + public void newPatchSetByOtherOnReviewableChangeInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master", other); + // TODO(logan): This should include owner but currently doesn't because + // it's sent *from* the owner. + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer, other) + .cc(sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + } + + @Test + public void newPatchSetByOtherOnReviewableChangeInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master", other); + assertThat(sender) + .sent("newpatchset", sc) + .notTo(sc.owner) // TODO(logan): This email shouldn't come from the owner. + .to(sc.reviewer, sc.ccer, other) + .notTo(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + } + + @Test + public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master", other, EmailStrategy.CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.owner, sc.reviewer, other) + .cc(sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + } + + @Test + public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master", other, EmailStrategy.CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.owner, sc.reviewer, sc.ccer, other) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + } + + @Test + public void newPatchSetByOtherOnReviewableChangeNotifyOwnerReviewersInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master%notify=OWNER_REVIEWERS", other); + // TODO(logan): This should include owner but currently doesn't because + // it's sent *from* the owner. + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer) + .cc(sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void newPatchSetByOtherOnReviewableChangeNotifyOwnerReviewersInReviewDb() + throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master%notify=OWNER_REVIEWERS", other); + // TODO(logan): This should include owner but currently doesn't because + // it's sent *from* the owner. + assertThat(sender).sent("newpatchset", sc).to(sc.reviewer, sc.ccer).noOneElse(); + } + + @Test + public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcNotifyOwnerReviewersInNoteDb() + throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master%notify=OWNER_REVIEWERS", other, EmailStrategy.CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.owner, sc.reviewer) + .cc(sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .noOneElse(); + } + + @Test + public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcNotifyOwnerReviewersInReviewDb() + throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master%notify=OWNER_REVIEWERS", other, EmailStrategy.CC_ON_OWN_COMMENTS); + assertThat(sender).sent("newpatchset", sc).to(sc.owner, sc.reviewer, sc.ccer).noOneElse(); + } + + @Test + public void newPatchSetByOtherOnReviewableChangeNotifyOwner() throws Exception { + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master%notify=OWNER", other); + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcNotifyOwner() throws Exception { + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master%notify=OWNER", other, EmailStrategy.CC_ON_OWN_COMMENTS); + // TODO(logan): This email shouldn't come from the owner, and that's why + // no email is currently sent (owner isn't CCing self). + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetByOtherOnReviewableChangeNotifyNone() throws Exception { + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master%notify=NONE", other); + // TODO(logan): This email shouldn't come from the owner, and that's why + // no email is currently sent (owner isn't CCing self). + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetByOtherOnReviewableChangeOwnerSelfCcNotifyNone() throws Exception { + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master%notify=NONE", other, EmailStrategy.CC_ON_OWN_COMMENTS); + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetByOwnerOnReviewableChangeToWip() throws Exception { + StagedChange sc = stageReviewableChange(); + pushTo(sc, "refs/for/master%wip", sc.owner); + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetOnWipChange() throws Exception { + StagedChange sc = stageWipChange(); + pushTo(sc, "refs/for/master%wip", sc.owner); + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetOnWipChangeNotifyAllInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageWipChange(); + pushTo(sc, "refs/for/master%wip,notify=ALL", sc.owner); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer) + .cc(sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + } + + @Test + public void newPatchSetOnWipChangeNotifyAllInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageWipChange(); + pushTo(sc, "refs/for/master%wip,notify=ALL", sc.owner); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer, sc.ccer) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + } + + @Test + public void newPatchSetOnWipChangeToReadyInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageWipChange(); + pushTo(sc, "refs/for/master%ready", sc.owner); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer) + .cc(sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + } + + @Test + public void newPatchSetOnWipChangeToReadyInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageWipChange(); + pushTo(sc, "refs/for/master%ready", sc.owner); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer, sc.ccer) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + } + + @Test + public void newPatchSetOnReviewableWipChange() throws Exception { + StagedChange sc = stageReviewableWipChange(); + pushTo(sc, "refs/for/master%wip", sc.owner); + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetOnReviewableChangeAddingReviewerInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + TestAccount newReviewer = sc.testAccount("newReviewer"); + pushTo(sc, "refs/for/master%r=" + newReviewer.username, sc.owner); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer, newReviewer) + .cc(sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetOnReviewableChangeAddingReviewerInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageReviewableChange(); + TestAccount newReviewer = sc.testAccount("newReviewer"); + pushTo(sc, "refs/for/master%r=" + newReviewer.username, sc.owner); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer, sc.ccer, newReviewer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetOnWipChangeAddingReviewer() throws Exception { + StagedChange sc = stageWipChange(); + TestAccount newReviewer = sc.testAccount("newReviewer"); + pushTo(sc, "refs/for/master%r=" + newReviewer.username, sc.owner); + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetOnWipChangeAddingReviewerNotifyAllInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageWipChange(); + TestAccount newReviewer = sc.testAccount("newReviewer"); + pushTo(sc, "refs/for/master%notify=ALL,r=" + newReviewer.username, sc.owner); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer, newReviewer) + .cc(sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetOnWipChangeAddingReviewerNotifyAllInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageWipChange(); + TestAccount newReviewer = sc.testAccount("newReviewer"); + pushTo(sc, "refs/for/master%notify=ALL,r=" + newReviewer.username, sc.owner); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer, sc.ccer, newReviewer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetOnWipChangeSettingReadyInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageWipChange(); + pushTo(sc, "refs/for/master%ready", sc.owner); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer) + .cc(sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + assertThat(sender).notSent(); + } + + @Test + public void newPatchSetOnWipChangeSettingReadyInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageWipChange(); + pushTo(sc, "refs/for/master%ready", sc.owner); + assertThat(sender) + .sent("newpatchset", sc) + .to(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(NEW_PATCHSETS) + .noOneElse(); + assertThat(sender).notSent(); + } + + private void pushTo(StagedChange sc, String ref, TestAccount by) throws Exception { + pushTo(sc, ref, by, ENABLED); + } + + private void pushTo(StagedChange sc, String ref, TestAccount by, EmailStrategy emailStrategy) + throws Exception { + setEmailStrategy(sc.owner, emailStrategy); + pushFactory.create(db, by.getIdent(), sc.repo, sc.changeId).to(ref).assertOkStatus(); + } + + /* + * RestoredSender tests. + */ + + @Test + public void restoreReviewableChange() throws Exception { + StagedChange sc = stageAbandonedReviewableChange(); + restore(sc.changeId, sc.owner); + assertThat(sender) + .sent("restore", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void restoreReviewableWipChange() throws Exception { + StagedChange sc = stageAbandonedReviewableWipChange(); + restore(sc.changeId, sc.owner); + assertThat(sender) + .sent("restore", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void restoreWipChange() throws Exception { + StagedChange sc = stageAbandonedWipChange(); + restore(sc.changeId, sc.owner); + assertThat(sender) + .sent("restore", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void restoreReviewableChangeByAdmin() throws Exception { + StagedChange sc = stageAbandonedReviewableChange(); + restore(sc.changeId, admin); + assertThat(sender) + .sent("restore", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void restoreReviewableChangeByOwnerCcingSelf() throws Exception { + StagedChange sc = stageAbandonedReviewableChange(); + restore(sc.changeId, sc.owner, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("restore", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void restoreReviewableChangeByAdminCcingSelf() throws Exception { + StagedChange sc = stageAbandonedReviewableChange(); + restore(sc.changeId, admin, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("restore", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer, admin) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + private void restore(String changeId, TestAccount by) throws Exception { + restore(changeId, by, ENABLED); + } + + private void restore(String changeId, TestAccount by, EmailStrategy emailStrategy) + throws Exception { + setEmailStrategy(by, emailStrategy); + setApiUser(by); + gApi.changes().id(changeId).restore(); + } + + /* + * RevertedSender tests. + */ + + @Test + public void revertChangeByOwnerInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageChange(); + revert(sc, sc.owner); + assertThat(sender) + .sent("newchange", sc) + .to(sc.reviewer, sc.ccer, sc.watchingProjectOwner, admin) + .bcc(NEW_CHANGES, NEW_PATCHSETS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + + assertThat(sender) + .sent("revert", sc) + .notTo(sc.owner) + .cc(sc.reviewer, sc.ccer, admin) + .bcc(ALL_COMMENTS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + } + + @Test + public void revertChangeByOwnerInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageChange(); + revert(sc, sc.owner); + assertThat(sender) + .sent("newchange", sc) + .to(sc.reviewer, sc.watchingProjectOwner, admin) + .cc(sc.ccer) + .bcc(NEW_CHANGES, NEW_PATCHSETS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + + assertThat(sender) + .sent("revert", sc) + .cc(sc.reviewer, sc.ccer, admin) + .bcc(ALL_COMMENTS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + } + + @Test + public void revertChangeByOwnerCcingSelfInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageChange(); + revert(sc, sc.owner, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("newchange", sc) + .to(sc.reviewer, sc.ccer, sc.watchingProjectOwner, admin) + .cc(sc.owner) + .bcc(NEW_CHANGES, NEW_PATCHSETS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + + assertThat(sender) + .sent("revert", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer, admin) + .bcc(ALL_COMMENTS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + } + + @Test + public void revertChangeByOwnerCcingSelfInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageChange(); + revert(sc, sc.owner, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("newchange", sc) + .to(sc.reviewer, sc.watchingProjectOwner, admin) + .cc(sc.owner, sc.ccer) + .bcc(NEW_CHANGES, NEW_PATCHSETS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + + assertThat(sender) + .sent("revert", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer, admin) + .bcc(ALL_COMMENTS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + } + + @Test + public void revertChangeByOtherInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageChange(); + revert(sc, other); + assertThat(sender) + .sent("newchange", sc) + .to(sc.owner, sc.reviewer, sc.ccer, sc.watchingProjectOwner, admin) + .bcc(NEW_CHANGES, NEW_PATCHSETS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + + assertThat(sender) + .sent("revert", sc) + .cc(sc.owner, sc.reviewer, sc.ccer, admin) + .bcc(ALL_COMMENTS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + } + + @Test + public void revertChangeByOtherInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageChange(); + revert(sc, other); + assertThat(sender) + .sent("newchange", sc) + .to(sc.owner, sc.reviewer, sc.watchingProjectOwner, admin) + .cc(sc.ccer) + .bcc(NEW_CHANGES, NEW_PATCHSETS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + + assertThat(sender) + .sent("revert", sc) + .cc(sc.owner, sc.reviewer, sc.ccer, admin) + .bcc(ALL_COMMENTS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + } + + @Test + public void revertChangeByOtherCcingSelfInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageChange(); + revert(sc, other, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("newchange", sc) + .to(sc.owner, sc.reviewer, sc.ccer, sc.watchingProjectOwner, admin) + .cc(other) + .bcc(NEW_CHANGES, NEW_PATCHSETS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + + assertThat(sender) + .sent("revert", sc) + .to(other) + .cc(sc.owner, sc.reviewer, sc.ccer, admin) + .bcc(ALL_COMMENTS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + } + + @Test + public void revertChangeByOtherCcingSelfInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageChange(); + revert(sc, other, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("newchange", sc) + .to(sc.owner, sc.reviewer, sc.watchingProjectOwner, admin) + .cc(sc.ccer, other) + .bcc(NEW_CHANGES, NEW_PATCHSETS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + + assertThat(sender) + .sent("revert", sc) + .to(other) + .cc(sc.owner, sc.reviewer, sc.ccer, admin) + .bcc(ALL_COMMENTS) + .noOneElse(); // TODO(logan): Why not starrer/reviewers-by-email? + } + + private StagedChange stageChange() throws Exception { + StagedChange sc = stageReviewableChange(); + setApiUser(admin); + gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.approve()); + gApi.changes().id(sc.changeId).revision("current").submit(); + sender.clear(); + return sc; + } + + private void revert(StagedChange sc, TestAccount by) throws Exception { + revert(sc, by, ENABLED); + } + + private void revert(StagedChange sc, TestAccount by, EmailStrategy emailStrategy) + throws Exception { + setEmailStrategy(by, emailStrategy); + setApiUser(by); + gApi.changes().id(sc.changeId).revert(); + } + + /* + * SetAssigneeSender tests. + */ + + @Test + public void setAssigneeOnReviewableChange() throws Exception { + StagedChange sc = stageReviewableChange(); + assign(sc, sc.owner, sc.assignee); + assertThat(sender) + .sent("setassignee", sc) + .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended! + .to(sc.assignee) + .noOneElse(); + } + + @Test + public void setAssigneeOnReviewableChangeByOwnerCcingSelf() throws Exception { + StagedChange sc = stageReviewableChange(); + assign(sc, sc.owner, sc.assignee, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("setassignee", sc) + .cc(sc.owner) + .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended! + .to(sc.assignee) + .noOneElse(); + } + + @Test + public void setAssigneeOnReviewableChangeByAdmin() throws Exception { + StagedChange sc = stageReviewableChange(); + assign(sc, admin, sc.assignee); + assertThat(sender) + .sent("setassignee", sc) + .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended! + .to(sc.assignee) + .noOneElse(); + } + + @Test + public void setAssigneeOnReviewableChangeByAdminCcingSelf() throws Exception { + StagedChange sc = stageReviewableChange(); + assign(sc, admin, sc.assignee, CC_ON_OWN_COMMENTS); + assertThat(sender) + .sent("setassignee", sc) + .cc(admin) + .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended! + .to(sc.assignee) + .noOneElse(); + } + + @Test + public void setAssigneeToSelfOnReviewableChangeInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + assign(sc, sc.owner, sc.owner); + assertThat(sender) + .sent("setassignee", sc) + .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended! + .noOneElse(); + } + + @Test + public void setAssigneeToSelfOnReviewableChangeInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageReviewableChange(); + assign(sc, sc.owner, sc.owner); + assertThat(sender).notSent(); + } + + @Test + public void changeAssigneeOnReviewableChange() throws Exception { + StagedChange sc = stageReviewableChange(); + TestAccount other = accountCreator.create("other", "other@example.com", "other"); + assign(sc, sc.owner, other); + sender.clear(); + assign(sc, sc.owner, sc.assignee); + assertThat(sender) + .sent("setassignee", sc) + .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended! + .to(sc.assignee) + .noOneElse(); + } + + @Test + public void changeAssigneeToSelfOnReviewableChangeInNoteDb() throws Exception { + assume().that(notesMigration.readChanges()).isTrue(); + StagedChange sc = stageReviewableChange(); + assign(sc, sc.owner, sc.assignee); + sender.clear(); + assign(sc, sc.owner, sc.owner); + assertThat(sender) + .sent("setassignee", sc) + .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended! + .noOneElse(); + } + + @Test + public void changeAssigneeToSelfOnReviewableChangeInReviewDb() throws Exception { + assume().that(notesMigration.readChanges()).isFalse(); + StagedChange sc = stageReviewableChange(); + assign(sc, sc.owner, sc.assignee); + sender.clear(); + assign(sc, sc.owner, sc.owner); + assertThat(sender).notSent(); + } + + @Test + public void setAssigneeOnReviewableWipChange() throws Exception { + StagedChange sc = stageReviewableWipChange(); + assign(sc, sc.owner, sc.assignee); + assertThat(sender) + .sent("setassignee", sc) + .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended! + .to(sc.assignee) + .noOneElse(); + } + + @Test + public void setAssigneeOnWipChange() throws Exception { + StagedChange sc = stageWipChange(); + assign(sc, sc.owner, sc.assignee); + assertThat(sender) + .sent("setassignee", sc) + .cc(sc.reviewerByEmail, sc.ccerByEmail) // TODO(logan): This is probably not intended! + .to(sc.assignee) + .noOneElse(); + } + + private void assign(StagedChange sc, TestAccount by, TestAccount to) throws Exception { + assign(sc, by, to, ENABLED); + } + + private void assign(StagedChange sc, TestAccount by, TestAccount to, EmailStrategy emailStrategy) + throws Exception { + setEmailStrategy(by, emailStrategy); + setApiUser(by); + AssigneeInput in = new AssigneeInput(); + in.assignee = to.email; + gApi.changes().id(sc.changeId).setAssignee(in); + } + + /* + * Start review and WIP tests. + */ + + @Test + public void startReviewOnWipChange() throws Exception { + StagedChange sc = stageWipChange(); + startReview(sc); + assertThat(sender) + .sent("comment", sc) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void startReviewOnWipChangeCcingSelf() throws Exception { + StagedChange sc = stageWipChange(); + setEmailStrategy(sc.owner, CC_ON_OWN_COMMENTS); + startReview(sc); + assertThat(sender) + .sent("comment", sc) + .to(sc.owner) + .cc(sc.reviewer, sc.ccer) + .cc(sc.reviewerByEmail, sc.ccerByEmail) + .bcc(sc.starrer) + .bcc(ALL_COMMENTS) + .noOneElse(); + } + + @Test + public void setWorkInProgress() throws Exception { + StagedChange sc = stageReviewableChange(); + gApi.changes().id(sc.changeId).setWorkInProgress(); + assertThat(sender).notSent(); + } + + private void startReview(StagedChange sc) throws Exception { + setApiUser(sc.owner); + gApi.changes().id(sc.changeId).setReadyForReview(); + // PolyGerrit current immediately follows up with a review. + gApi.changes().id(sc.changeId).revision("current").review(ReviewInput.noScore()); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java new file mode 100644 index 0000000..cb039ad --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/mail/EmailValidatorIT.java
@@ -0,0 +1,100 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.mail; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assert_; + +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.GerritConfig; +import com.google.gerrit.server.mail.send.OutgoingEmailValidator; +import com.google.inject.Inject; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; + +public class EmailValidatorIT extends AbstractDaemonTest { + private static final String UNSUPPORTED_PREFIX = "#! "; + + @Inject private OutgoingEmailValidator validator; + + @BeforeClass + public static void setUpClass() throws Exception { + // Reset before first use, in case other tests have already run in this JVM. + resetDomainValidator(); + } + + @After + public void tearDown() throws Exception { + resetDomainValidator(); + } + + private static void resetDomainValidator() throws Exception { + Class<?> c = Class.forName("org.apache.commons.validator.routines.DomainValidator"); + Field f = c.getDeclaredField("inUse"); + f.setAccessible(true); + f.setBoolean(c, false); + } + + @Test + @GerritConfig(name = "sendemail.allowTLD", value = "example") + public void testCustomTopLevelDomain() throws Exception { + assertThat(validator.isValid("foo@bar.local")).isFalse(); + assertThat(validator.isValid("foo@bar.example")).isTrue(); + assertThat(validator.isValid("foo@example")).isTrue(); + } + + @Test + @GerritConfig(name = "sendemail.allowTLD", value = "a") + public void testCustomTopLevelDomainOneCharacter() throws Exception { + assertThat(validator.isValid("foo@bar.local")).isFalse(); + assertThat(validator.isValid("foo@bar.a")).isTrue(); + assertThat(validator.isValid("foo@a")).isTrue(); + } + + @Test + public void validateTopLevelDomains() throws Exception { + try (InputStream in = this.getClass().getResourceAsStream("tlds-alpha-by-domain.txt")) { + if (in == null) { + throw new Exception("TLD list not found"); + } + BufferedReader r = new BufferedReader(new InputStreamReader(in)); + String tld; + while ((tld = r.readLine()) != null) { + if (tld.startsWith("# ") || tld.startsWith("XN--")) { + // Ignore comments and non-latin domains + continue; + } + if (tld.startsWith(UNSUPPORTED_PREFIX)) { + String test = "test@example." + tld.toLowerCase().substring(UNSUPPORTED_PREFIX.length()); + assert_() + .withFailureMessage("expected invalid TLD \"" + test + "\"") + .that(validator.isValid(test)) + .isFalse(); + } else { + String test = "test@example." + tld.toLowerCase(); + assert_() + .withFailureMessage("failed to validate TLD \"" + test + "\"") + .that(validator.isValid(test)) + .isTrue(); + } + } + } + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java index 87ca2a0..ae875f4 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -122,6 +122,10 @@ // want precise control over when auto-rebuilding happens. cfg.setBoolean("index", null, "autoReindexIfStale", false); + // setNotesMigration tries to keep IDs in sync between ReviewDb and NoteDb, which is behavior + // unique to this test. This gets prohibitively slow if we use the default sequence gap. + cfg.setInt("noteDb", "changes", "initialSequenceGap", 0); + return cfg; } @@ -139,8 +143,6 @@ @Inject private TestChangeRebuilderWrapper rebuilderWrapper; - @Inject private BatchUpdate.Factory batchUpdateFactory; - @Inject private Sequences seq; @Inject private ChangeBundleReader bundleReader; @@ -215,6 +217,7 @@ Change c = TestChanges.newChange(project, user.getId(), seq.nextChangeId()); c.setCreatedOn(ts); c.setLastUpdatedOn(ts); + c.setReviewStarted(true); PatchSet ps = TestChanges.newPatchSet( c.currentPatchSetId(), "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", user.getId()); @@ -754,7 +757,7 @@ assertThat(ts).isGreaterThan(c.getCreatedOn()); assertThat(ts).isLessThan(db.patchSets().get(psId).getCreatedOn()); RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId()); - postReview.get().apply(revRsrc, rin, ts); + postReview.get().apply(batchUpdateFactory, revRsrc, rin, ts); checker.rebuildAndCheckChanges(id); } @@ -772,7 +775,7 @@ Timestamp ts = new Timestamp(c.getCreatedOn().getTime() - 10000); RevisionResource revRsrc = parseCurrentRevisionResource(r.getChangeId()); setApiUser(user); - postReview.get().apply(revRsrc, rin, ts); + postReview.get().apply(batchUpdateFactory, revRsrc, rin, ts); checker.rebuildAndCheckChanges(id); }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java new file mode 100644 index 0000000..9859086 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -0,0 +1,347 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.notedb; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assert_; +import static com.google.common.truth.Truth8.assertThat; +import static com.google.common.truth.TruthJUnit.assume; +import static java.util.stream.Collectors.toList; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Streams; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.client.ListChangesOption; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.BatchUpdateListener; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.RepoContext; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.testutil.ConfigSuite; +import com.google.inject.Inject; +import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Constants; +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.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevSort; +import org.eclipse.jgit.revwalk.RevWalk; +import org.junit.Before; +import org.junit.Test; + +public class NoteDbOnlyIT extends AbstractDaemonTest { + @ConfigSuite.Default + public static Config defaultConfig() { + Config cfg = new Config(); + // Avoid spurious timeouts during intentional retries due to overloaded test machines. + cfg.setString("noteDb", null, "retryTimeout", Integer.MAX_VALUE + "s"); + return cfg; + } + + @Inject private RetryHelper retryHelper; + + @Before + public void setUp() throws Exception { + assume().that(notesMigration.disableChangeReviewDb()).isTrue(); + } + + @Test + public void updateChangeFailureRollsBackRefUpdate() throws Exception { + assume().that(notesMigration.fuseUpdates()).isTrue(); + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + + String master = "refs/heads/master"; + String backup = "refs/backup/master"; + ObjectId master1 = getRef(master).get(); + assertThat(getRef(backup)).isEmpty(); + + // Toy op that copies the value of refs/heads/master to refs/backup/master. + BatchUpdateOp backupMasterOp = + new BatchUpdateOp() { + ObjectId newId; + + @Override + public void updateRepo(RepoContext ctx) throws IOException { + ObjectId oldId = ctx.getRepoView().getRef(backup).orElse(ObjectId.zeroId()); + newId = ctx.getRepoView().getRef(master).get(); + ctx.addRefUpdate(oldId, newId, backup); + } + + @Override + public boolean updateChange(ChangeContext ctx) { + ctx.getUpdate(ctx.getChange().currentPatchSetId()) + .setChangeMessage("Backed up master branch to " + newId.name()); + return true; + } + }; + + try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) { + bu.addOp(id, backupMasterOp); + bu.execute(); + } + + // Ensure backupMasterOp worked. + assertThat(getRef(backup)).hasValue(master1); + assertThat(getMessages(id)).contains("Backed up master branch to " + master1.name()); + + // Advance master by submitting the change. + gApi.changes().id(id.get()).current().review(ReviewInput.approve()); + gApi.changes().id(id.get()).current().submit(); + ObjectId master2 = getRef(master).get(); + assertThat(master2).isNotEqualTo(master1); + int msgCount = getMessages(id).size(); + + try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) { + // This time, we attempt to back up master, but we fail during updateChange. + bu.addOp(id, backupMasterOp); + String msg = "Change is bad"; + bu.addOp( + id, + new BatchUpdateOp() { + @Override + public boolean updateChange(ChangeContext ctx) throws ResourceConflictException { + throw new ResourceConflictException(msg); + } + }); + try { + bu.execute(); + assert_().fail("expected ResourceConflictException"); + } catch (ResourceConflictException e) { + assertThat(e).hasMessageThat().isEqualTo(msg); + } + } + + // If updateChange hadn't failed, backup would have been updated to master2. + assertThat(getRef(backup)).hasValue(master1); + assertThat(getMessages(id)).hasSize(msgCount); + } + + @Test + public void retryOnLockFailureWithAtomicUpdates() throws Exception { + assume().that(notesMigration.fuseUpdates()).isTrue(); + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + String master = "refs/heads/master"; + ObjectId initial; + try (Repository repo = repoManager.openRepository(project)) { + ((InMemoryRepository) repo).setPerformsAtomicTransactions(true); + initial = repo.exactRef(master).getObjectId(); + } + + AtomicInteger updateRepoCalledCount = new AtomicInteger(); + AtomicInteger updateChangeCalledCount = new AtomicInteger(); + AtomicInteger afterUpdateReposCalledCount = new AtomicInteger(); + + String result = + retryHelper.execute( + batchUpdateFactory -> { + try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) { + bu.addOp( + id, + new UpdateRefAndAddMessageOp(updateRepoCalledCount, updateChangeCalledCount)); + bu.execute(new ConcurrentWritingListener(afterUpdateReposCalledCount)); + } + return "Done"; + }); + + assertThat(result).isEqualTo("Done"); + assertThat(updateRepoCalledCount.get()).isEqualTo(2); + assertThat(afterUpdateReposCalledCount.get()).isEqualTo(2); + assertThat(updateChangeCalledCount.get()).isEqualTo(2); + + List<String> messages = getMessages(id); + assertThat(Iterables.getLast(messages)).isEqualTo(UpdateRefAndAddMessageOp.CHANGE_MESSAGE); + assertThat(Collections.frequency(messages, UpdateRefAndAddMessageOp.CHANGE_MESSAGE)) + .isEqualTo(1); + + try (Repository repo = repoManager.openRepository(project)) { + // Op lost the race, so the other writer's commit happened first. Then op retried and wrote + // its commit with the other writer's commit as parent. + assertThat(commitMessages(repo, initial, repo.exactRef(master).getObjectId())) + .containsExactly( + ConcurrentWritingListener.MSG_PREFIX + "1", UpdateRefAndAddMessageOp.COMMIT_MESSAGE) + .inOrder(); + } + } + + @Test + public void noRetryOnLockFailureWithoutAtomicUpdates() throws Exception { + assume().that(notesMigration.fuseUpdates()).isFalse(); + + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + String master = "refs/heads/master"; + ObjectId initial; + try (Repository repo = repoManager.openRepository(project)) { + initial = repo.exactRef(master).getObjectId(); + } + + AtomicInteger updateRepoCalledCount = new AtomicInteger(); + AtomicInteger updateChangeCalledCount = new AtomicInteger(); + AtomicInteger afterUpdateReposCalledCount = new AtomicInteger(); + + try { + retryHelper.execute( + batchUpdateFactory -> { + try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) { + bu.addOp( + id, new UpdateRefAndAddMessageOp(updateRepoCalledCount, updateChangeCalledCount)); + bu.execute(new ConcurrentWritingListener(afterUpdateReposCalledCount)); + } + return null; + }); + assert_().fail("expected RestApiException"); + } catch (RestApiException e) { + // Expected. + } + + assertThat(updateRepoCalledCount.get()).isEqualTo(1); + assertThat(afterUpdateReposCalledCount.get()).isEqualTo(1); + assertThat(updateChangeCalledCount.get()).isEqualTo(0); + + // updateChange was never called, so no message was ever added. + assertThat(getMessages(id)).doesNotContain(UpdateRefAndAddMessageOp.CHANGE_MESSAGE); + + try (Repository repo = repoManager.openRepository(project)) { + // Op lost the race, so the other writer's commit happened first. Op didn't retry, because the + // ref updates weren't atomic, so it didn't throw LockFailureException on failure. + assertThat(commitMessages(repo, initial, repo.exactRef(master).getObjectId())) + .containsExactly(ConcurrentWritingListener.MSG_PREFIX + "1"); + } + } + + private class ConcurrentWritingListener implements BatchUpdateListener { + static final String MSG_PREFIX = "Other writer "; + + private final AtomicInteger calledCount; + + private ConcurrentWritingListener(AtomicInteger calledCount) { + this.calledCount = calledCount; + } + + @Override + public void afterUpdateRepos() throws Exception { + // Reopen repo and update ref, to simulate a concurrent write in another + // thread. Only do this the first time the listener is called. + if (calledCount.getAndIncrement() > 0) { + return; + } + try (Repository repo = repoManager.openRepository(project); + RevWalk rw = new RevWalk(repo); + ObjectInserter ins = repo.newObjectInserter()) { + String master = "refs/heads/master"; + ObjectId oldId = repo.exactRef(master).getObjectId(); + ObjectId newId = newCommit(rw, ins, oldId, MSG_PREFIX + calledCount.get()); + ins.flush(); + RefUpdate ru = repo.updateRef(master); + ru.setExpectedOldObjectId(oldId); + ru.setNewObjectId(newId); + assertThat(ru.update(rw)).isEqualTo(RefUpdate.Result.FAST_FORWARD); + } + } + } + + private class UpdateRefAndAddMessageOp implements BatchUpdateOp { + static final String COMMIT_MESSAGE = "A commit"; + static final String CHANGE_MESSAGE = "A change message"; + + private final AtomicInteger updateRepoCalledCount; + private final AtomicInteger updateChangeCalledCount; + + private UpdateRefAndAddMessageOp( + AtomicInteger updateRepoCalledCount, AtomicInteger updateChangeCalledCount) { + this.updateRepoCalledCount = updateRepoCalledCount; + this.updateChangeCalledCount = updateChangeCalledCount; + } + + @Override + public void updateRepo(RepoContext ctx) throws Exception { + String master = "refs/heads/master"; + ObjectId oldId = ctx.getRepoView().getRef(master).get(); + ObjectId newId = newCommit(ctx.getRevWalk(), ctx.getInserter(), oldId, COMMIT_MESSAGE); + ctx.addRefUpdate(oldId, newId, master); + updateRepoCalledCount.incrementAndGet(); + } + + @Override + public boolean updateChange(ChangeContext ctx) throws Exception { + ctx.getUpdate(ctx.getChange().currentPatchSetId()).setChangeMessage(CHANGE_MESSAGE); + updateChangeCalledCount.incrementAndGet(); + return true; + } + } + + private ObjectId newCommit(RevWalk rw, ObjectInserter ins, ObjectId parent, String msg) + throws IOException { + PersonIdent ident = serverIdent.get(); + CommitBuilder cb = new CommitBuilder(); + cb.setParentId(parent); + cb.setTreeId(rw.parseCommit(parent).getTree()); + cb.setMessage(msg); + cb.setAuthor(ident); + cb.setCommitter(ident); + return ins.insert(Constants.OBJ_COMMIT, cb.build()); + } + + private BatchUpdate newBatchUpdate(BatchUpdate.Factory buf) { + return buf.create(db, project, identifiedUserFactory.create(user.getId()), TimeUtil.nowTs()); + } + + private Optional<ObjectId> getRef(String name) throws Exception { + try (Repository repo = repoManager.openRepository(project)) { + return Optional.ofNullable(repo.exactRef(name)).map(Ref::getObjectId); + } + } + + private List<String> getMessages(Change.Id id) throws Exception { + return gApi.changes() + .id(id.get()) + .get(EnumSet.of(ListChangesOption.MESSAGES)) + .messages + .stream() + .map(m -> m.message) + .collect(toList()); + } + + private static List<String> commitMessages( + Repository repo, ObjectId fromExclusive, ObjectId toInclusive) throws Exception { + try (RevWalk rw = new RevWalk(repo)) { + rw.markStart(rw.parseCommit(toInclusive)); + rw.markUninteresting(rw.parseCommit(fromExclusive)); + rw.sort(RevSort.REVERSE); + rw.setRetainBody(true); + return Streams.stream(rw).map(c -> c.getShortMessage()).collect(toList()); + } + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java index 1d2f0e0..37bdeee 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
@@ -16,6 +16,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assert_; +import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.TruthJUnit.assume; import static com.google.gerrit.server.notedb.ChangeNoteUtil.formatTime; import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; @@ -63,7 +64,7 @@ import com.google.gerrit.server.notedb.PrimaryStorageMigrator; import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper; import com.google.gerrit.server.project.ChangeControl; -import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.NoteDbMode; import com.google.gerrit.testutil.TestTimeUtil; @@ -97,13 +98,13 @@ } @Inject private AllUsersName allUsers; - @Inject private BatchUpdate.Factory batchUpdateFactory; @Inject private ChangeBundleReader bundleReader; @Inject private CommentsUtil commentsUtil; @Inject private TestChangeRebuilderWrapper rebuilderWrapper; @Inject private ChangeControl.GenericFactory changeControlFactory; @Inject private ChangeUpdate.Factory updateFactory; @Inject private InternalUser.Factory internalUserFactory; + @Inject private RetryHelper retryHelper; private PrimaryStorageMigrator migrator; @@ -128,7 +129,7 @@ queryProvider, updateFactory, internalUserFactory, - batchUpdateFactory); + retryHelper); } @After @@ -274,7 +275,7 @@ .stream() .filter(x -> x instanceof OrmRuntimeException) .findFirst(); - assertThat(oe.isPresent()).named("OrmRuntimeException in causal chain of " + e).isTrue(); + assertThat(oe).named("OrmRuntimeException in causal chain of " + e).isPresent(); assertThat(oe.get().getMessage()).contains("read-only"); } assertThat(gApi.changes().id(id.get()).get().topic).isNull();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java new file mode 100644 index 0000000..29b8ee7 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
@@ -0,0 +1,422 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.notedb; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static com.google.common.truth.TruthJUnit.assume; +import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB_UNFUSED; +import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_NO_SEQUENCE; +import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY; +import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY; +import static com.google.gerrit.server.notedb.NotesMigrationState.REVIEW_DB; +import static com.google.gerrit.server.notedb.NotesMigrationState.WRITE; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.GerritConfig; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.acceptance.Sandboxed; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.Sequences; +import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.notedb.ConfigNotesMigration; +import com.google.gerrit.server.notedb.NoteDbChangeState; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; +import com.google.gerrit.server.notedb.NoteDbChangeState.RefState; +import com.google.gerrit.server.notedb.NotesMigrationState; +import com.google.gerrit.server.notedb.rebuild.MigrationException; +import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator; +import com.google.gerrit.server.schema.ReviewDbFactory; +import com.google.gerrit.testutil.ConfigSuite; +import com.google.gerrit.testutil.NoteDbMode; +import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.util.List; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; +import org.junit.Before; +import org.junit.Test; + +@Sandboxed +@NoHttpd +public class OnlineNoteDbMigrationIT extends AbstractDaemonTest { + private static final String INVALID_STATE = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + + @ConfigSuite.Default + public static Config defaultConfig() { + Config cfg = new Config(); + cfg.setInt("noteDb", "changes", "sequenceBatchSize", 10); + cfg.setInt("noteDb", "changes", "initialSequenceGap", 500); + return cfg; + } + + // Tests in this class are generally interested in the actual ReviewDb contents, but the shifting + // migration state may result in various kinds of wrappers showing up unexpectedly. + @Inject @ReviewDbFactory private SchemaFactory<ReviewDb> schemaFactory; + + @Inject private SitePaths sitePaths; + @Inject private Provider<NoteDbMigrator.Builder> migratorBuilderProvider; + @Inject private Sequences sequences; + + private FileBasedConfig gerritConfig; + + @Before + public void setUp() throws Exception { + assume().that(NoteDbMode.get()).isEqualTo(NoteDbMode.OFF); + gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect()); + assertNotesMigrationState(REVIEW_DB); + } + + @Test + public void preconditionsFail() throws Exception { + List<Change.Id> cs = ImmutableList.of(new Change.Id(1)); + List<Project.NameKey> ps = ImmutableList.of(new Project.NameKey("p")); + assertMigrationException( + "Cannot rebuild without noteDb.changes.write=true", b -> b, NoteDbMigrator::rebuild); + assertMigrationException( + "Cannot set both changes and projects", b -> b.setChanges(cs).setProjects(ps), m -> {}); + assertMigrationException( + "Auto-migration cannot be used with trial mode", + b -> b.setAutoMigrate(true).setTrialMode(true), + m -> {}); + assertMigrationException( + "Cannot set changes or projects during full migration", + b -> b.setChanges(cs), + NoteDbMigrator::migrate); + assertMigrationException( + "Cannot set changes or projects during full migration", + b -> b.setProjects(ps), + NoteDbMigrator::migrate); + + setNotesMigrationState(READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY); + assertMigrationException( + "Migration has already progressed past the endpoint of the \"trial mode\" state", + b -> b.setTrialMode(true), + NoteDbMigrator::migrate); + + setNotesMigrationState(READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY); + assertMigrationException( + "Cannot force rebuild changes; NoteDb is already the primary storage for some changes", + b -> b.setForceRebuild(true), + NoteDbMigrator::migrate); + } + + @Test + @GerritConfig(name = "noteDb.changes.initialSequenceGap", value = "-7") + public void initialSequenceGapMustBeNonNegative() throws Exception { + setNotesMigrationState(READ_WRITE_NO_SEQUENCE); + assertMigrationException("Sequence gap must be non-negative: -7", b -> b, m -> {}); + } + + @Test + public void rebuildOneChangeTrialModeAndForceRebuild() throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + + try (NoteDbMigrator migrator = migratorBuilderProvider.get().setTrialMode(true).build()) { + migrator.migrate(); + } + assertNotesMigrationState(READ_WRITE_NO_SEQUENCE); + + ObjectId oldMetaId; + try (Repository repo = repoManager.openRepository(project); + ReviewDb db = schemaFactory.open()) { + Ref ref = repo.exactRef(RefNames.changeMetaRef(id)); + assertThat(ref).isNotNull(); + oldMetaId = ref.getObjectId(); + + Change c = db.changes().get(id); + assertThat(c).isNotNull(); + NoteDbChangeState state = NoteDbChangeState.parse(c); + assertThat(state).isNotNull(); + assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB); + assertThat(state.getRefState()).hasValue(RefState.create(oldMetaId, ImmutableMap.of())); + + // Force change to be out of date, and change topic so it will get rebuilt as something other + // than oldMetaId. + c.setNoteDbState(INVALID_STATE); + c.setTopic(name("a-new-topic")); + db.changes().update(ImmutableList.of(c)); + } + + migrate(b -> b.setTrialMode(true)); + assertNotesMigrationState(READ_WRITE_NO_SEQUENCE); + + try (Repository repo = repoManager.openRepository(project); + ReviewDb db = schemaFactory.open()) { + // Change is out of date, but was not rebuilt without forceRebuild. + assertThat(repo.exactRef(RefNames.changeMetaRef(id)).getObjectId()).isEqualTo(oldMetaId); + Change c = db.changes().get(id); + assertThat(c.getNoteDbState()).isEqualTo(INVALID_STATE); + } + + migrate(b -> b.setTrialMode(true).setForceRebuild(true)); + assertNotesMigrationState(READ_WRITE_NO_SEQUENCE); + + try (Repository repo = repoManager.openRepository(project); + ReviewDb db = schemaFactory.open()) { + Ref ref = repo.exactRef(RefNames.changeMetaRef(id)); + assertThat(ref).isNotNull(); + ObjectId newMetaId = ref.getObjectId(); + assertThat(newMetaId).isNotEqualTo(oldMetaId); + + NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id)); + assertThat(state).isNotNull(); + assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB); + assertThat(state.getRefState()).hasValue(RefState.create(newMetaId, ImmutableMap.of())); + } + } + + @Test + public void rebuildSubsetOfChanges() throws Exception { + setNotesMigrationState(WRITE); + + PushOneCommit.Result r1 = createChange(); + PushOneCommit.Result r2 = createChange(); + Change.Id id1 = r1.getChange().getId(); + Change.Id id2 = r2.getChange().getId(); + + try (ReviewDb db = schemaFactory.open()) { + Change c1 = db.changes().get(id1); + c1.setNoteDbState(INVALID_STATE); + Change c2 = db.changes().get(id2); + c2.setNoteDbState(INVALID_STATE); + db.changes().update(ImmutableList.of(c1, c2)); + } + + migrate(b -> b.setChanges(ImmutableList.of(id2)), NoteDbMigrator::rebuild); + + try (ReviewDb db = schemaFactory.open()) { + NoteDbChangeState s1 = NoteDbChangeState.parse(db.changes().get(id1)); + assertThat(s1.getChangeMetaId().name()).isEqualTo(INVALID_STATE); + + NoteDbChangeState s2 = NoteDbChangeState.parse(db.changes().get(id2)); + assertThat(s2.getChangeMetaId().name()).isNotEqualTo(INVALID_STATE); + } + } + + @Test + public void rebuildSubsetOfProjects() throws Exception { + setNotesMigrationState(WRITE); + + Project.NameKey p2 = createProject("project2"); + TestRepository<?> tr2 = cloneProject(p2, admin); + + PushOneCommit.Result r1 = createChange(); + PushOneCommit.Result r2 = pushFactory.create(db, admin.getIdent(), tr2).to("refs/for/master"); + Change.Id id1 = r1.getChange().getId(); + Change.Id id2 = r2.getChange().getId(); + + String invalidState = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; + try (ReviewDb db = schemaFactory.open()) { + Change c1 = db.changes().get(id1); + c1.setNoteDbState(invalidState); + Change c2 = db.changes().get(id2); + c2.setNoteDbState(invalidState); + db.changes().update(ImmutableList.of(c1, c2)); + } + + migrate(b -> b.setProjects(ImmutableList.of(p2)), NoteDbMigrator::rebuild); + + try (ReviewDb db = schemaFactory.open()) { + NoteDbChangeState s1 = NoteDbChangeState.parse(db.changes().get(id1)); + assertThat(s1.getChangeMetaId().name()).isEqualTo(invalidState); + + NoteDbChangeState s2 = NoteDbChangeState.parse(db.changes().get(id2)); + assertThat(s2.getChangeMetaId().name()).isNotEqualTo(invalidState); + } + } + + @Test + public void enableSequencesNoGap() throws Exception { + testEnableSequences(0, 2, "12"); + } + + @Test + public void enableSequencesWithGap() throws Exception { + testEnableSequences(-1, 502, "512"); + } + + private void testEnableSequences(int builderOption, int expectedFirstId, String expectedRefValue) + throws Exception { + PushOneCommit.Result r = createChange(); + Change.Id id = r.getChange().getId(); + assertThat(id.get()).isEqualTo(1); + + migrate( + b -> + b.setSequenceGap(builderOption) + .setStopAtStateForTesting(READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY)); + + assertThat(sequences.nextChangeId()).isEqualTo(expectedFirstId); + assertThat(sequences.nextChangeId()).isEqualTo(expectedFirstId + 1); + + try (Repository repo = repoManager.openRepository(allProjects); + ObjectReader reader = repo.newObjectReader()) { + Ref ref = repo.exactRef("refs/sequences/changes"); + assertThat(ref).isNotNull(); + ObjectLoader loader = reader.open(ref.getObjectId()); + assertThat(loader.getType()).isEqualTo(Constants.OBJ_BLOB); + // Acquired a block of 10 to serve the first nextChangeId call after migration. + assertThat(new String(loader.getCachedBytes(), UTF_8)).isEqualTo(expectedRefValue); + } + + try (ReviewDb db = schemaFactory.open()) { + // Underlying, unused ReviewDb is still on its own sequence. + @SuppressWarnings("deprecation") + int nextFromReviewDb = db.nextChangeId(); + assertThat(nextFromReviewDb).isEqualTo(3); + } + } + + @Test + public void fullMigrationSameThread() throws Exception { + testFullMigration(1); + } + + @Test + public void fullMigrationMultipleThreads() throws Exception { + testFullMigration(2); + } + + private void testFullMigration(int threads) throws Exception { + PushOneCommit.Result r1 = createChange(); + PushOneCommit.Result r2 = createChange(); + Change.Id id1 = r1.getChange().getId(); + Change.Id id2 = r2.getChange().getId(); + + migrate(b -> b.setThreads(threads)); + assertNotesMigrationState(NOTE_DB_UNFUSED); + + assertThat(sequences.nextChangeId()).isEqualTo(503); + + ObjectId oldMetaId = null; + int rowVersion = 0; + try (ReviewDb db = schemaFactory.open(); + Repository repo = repoManager.openRepository(project)) { + for (Change.Id id : ImmutableList.of(id1, id2)) { + String refName = RefNames.changeMetaRef(id); + Ref ref = repo.exactRef(refName); + assertThat(ref).named(refName).isNotNull(); + + Change c = db.changes().get(id); + assertThat(c.getTopic()).named("topic of change %s", id).isNull(); + NoteDbChangeState s = NoteDbChangeState.parse(c); + assertThat(s.getPrimaryStorage()) + .named("primary storage of change %s", id) + .isEqualTo(PrimaryStorage.NOTE_DB); + assertThat(s.getRefState()).named("ref state of change %s").isEmpty(); + + if (id.equals(id1)) { + oldMetaId = ref.getObjectId(); + rowVersion = c.getRowVersion(); + } + } + } + + // Do not open a new context, to simulate races with other threads that opened a context earlier + // in the migration process; this needs to work. + gApi.changes().id(id1.get()).topic(name("a-topic")); + + // Of course, it should also work with a new context. + resetCurrentApiUser(); + gApi.changes().id(id1.get()).topic(name("another-topic")); + + try (ReviewDb db = schemaFactory.open(); + Repository repo = repoManager.openRepository(project)) { + assertThat(repo.exactRef(RefNames.changeMetaRef(id1)).getObjectId()).isNotEqualTo(oldMetaId); + + Change c = db.changes().get(id1); + assertThat(c.getTopic()).isNull(); + assertThat(c.getRowVersion()).isEqualTo(rowVersion); + } + } + + @Test + public void autoMigrationConfig() throws Exception { + createChange(); + + migrate(b -> b.setStopAtStateForTesting(WRITE)); + assertNotesMigrationState(WRITE); + assertThat(NoteDbMigrator.getAutoMigrate(gerritConfig)).isFalse(); + + migrate(b -> b.setAutoMigrate(true).setStopAtStateForTesting(READ_WRITE_NO_SEQUENCE)); + assertNotesMigrationState(READ_WRITE_NO_SEQUENCE); + assertThat(NoteDbMigrator.getAutoMigrate(gerritConfig)).isTrue(); + + migrate(b -> b); + assertNotesMigrationState(NOTE_DB_UNFUSED); + assertThat(NoteDbMigrator.getAutoMigrate(gerritConfig)).isFalse(); + } + + private void assertNotesMigrationState(NotesMigrationState expected) throws Exception { + assertThat(NotesMigrationState.forNotesMigration(notesMigration)).hasValue(expected); + gerritConfig.load(); + assertThat(NotesMigrationState.forNotesMigration(new ConfigNotesMigration(gerritConfig))) + .hasValue(expected); + } + + private void setNotesMigrationState(NotesMigrationState state) throws Exception { + gerritConfig.load(); + ConfigNotesMigration.setConfigValues(gerritConfig, state.migration()); + gerritConfig.save(); + notesMigration.setFrom(state.migration()); + } + + @FunctionalInterface + interface PrepareBuilder { + NoteDbMigrator.Builder prepare(NoteDbMigrator.Builder b) throws Exception; + } + + @FunctionalInterface + interface RunMigration { + void run(NoteDbMigrator m) throws Exception; + } + + private void migrate(PrepareBuilder b) throws Exception { + migrate(b, NoteDbMigrator::migrate); + } + + private void migrate(PrepareBuilder b, RunMigration m) throws Exception { + try (NoteDbMigrator migrator = b.prepare(migratorBuilderProvider.get()).build()) { + m.run(migrator); + } + } + + private void assertMigrationException( + String expectMessageContains, PrepareBuilder b, RunMigration m) throws Exception { + try { + migrate(b, m); + } catch (MigrationException e) { + assertThat(e).hasMessageThat().contains(expectMessageContains); + } + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java index 174fb76..e7fe81f 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/project/ProjectWatchIT.java
@@ -23,7 +23,6 @@ import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.Sandboxed; import com.google.gerrit.acceptance.TestAccount; -import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.Permission; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.StarsInput; @@ -39,7 +38,6 @@ import com.google.gerrit.server.mail.Address; import com.google.gerrit.testutil.FakeEmailSender.Message; import com.google.inject.Inject; -import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; @@ -162,11 +160,132 @@ } @Test + public void noNotificationForPrivateChangesForWatchersInNotifyConfig() throws Exception { + Address addr = new Address("Watcher", "watcher@example.com"); + NotifyConfig nc = new NotifyConfig(); + nc.addEmail(addr); + nc.setName("team"); + nc.setHeader(NotifyConfig.Header.TO); + nc.setTypes(EnumSet.of(NotifyType.NEW_CHANGES, NotifyType.ALL_COMMENTS)); + + ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); + cfg.putNotifyConfig("team", nc); + saveProjectConfig(project, cfg); + + sender.clear(); + PushOneCommit.Result r = + pushFactory + .create(db, admin.getIdent(), testRepo, "private change", "a", "a1") + .to("refs/for/master%private"); + r.assertOkStatus(); + + assertThat(sender.getMessages()).isEmpty(); + + setApiUser(admin); + ReviewInput in = new ReviewInput(); + in.message = "comment"; + gApi.changes().id(r.getChangeId()).current().review(in); + + assertThat(sender.getMessages()).isEmpty(); + } + + @Test + public void noNotificationForChangeThatIsTurnedPrivateForWatchersInNotifyConfig() + throws Exception { + Address addr = new Address("Watcher", "watcher@example.com"); + NotifyConfig nc = new NotifyConfig(); + nc.addEmail(addr); + nc.setName("team"); + nc.setHeader(NotifyConfig.Header.TO); + nc.setTypes(EnumSet.of(NotifyType.NEW_PATCHSETS)); + + ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); + cfg.putNotifyConfig("team", nc); + saveProjectConfig(project, cfg); + + PushOneCommit.Result r = + pushFactory + .create(db, admin.getIdent(), testRepo, "subject", "a", "a1") + .to("refs/for/master"); + r.assertOkStatus(); + + sender.clear(); + + r = + pushFactory + .create(db, admin.getIdent(), testRepo, "subject", "a", "a2", r.getChangeId()) + .to("refs/for/master%private"); + r.assertOkStatus(); + + assertThat(sender.getMessages()).isEmpty(); + } + + @Test + public void noNotificationForWipChangesForWatchersInNotifyConfig() throws Exception { + Address addr = new Address("Watcher", "watcher@example.com"); + NotifyConfig nc = new NotifyConfig(); + nc.addEmail(addr); + nc.setName("team"); + nc.setHeader(NotifyConfig.Header.TO); + nc.setTypes(EnumSet.of(NotifyType.NEW_CHANGES, NotifyType.ALL_COMMENTS)); + + ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); + cfg.putNotifyConfig("team", nc); + saveProjectConfig(project, cfg); + + sender.clear(); + PushOneCommit.Result r = + pushFactory + .create(db, admin.getIdent(), testRepo, "wip change", "a", "a1") + .to("refs/for/master%wip"); + r.assertOkStatus(); + + assertThat(sender.getMessages()).isEmpty(); + + setApiUser(admin); + ReviewInput in = new ReviewInput(); + in.message = "comment"; + gApi.changes().id(r.getChangeId()).current().review(in); + + assertThat(sender.getMessages()).isEmpty(); + } + + @Test + public void noNotificationForChangeThatIsTurnedWipForWatchersInNotifyConfig() throws Exception { + Address addr = new Address("Watcher", "watcher@example.com"); + NotifyConfig nc = new NotifyConfig(); + nc.addEmail(addr); + nc.setName("team"); + nc.setHeader(NotifyConfig.Header.TO); + nc.setTypes(EnumSet.of(NotifyType.NEW_PATCHSETS)); + + ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); + cfg.putNotifyConfig("team", nc); + saveProjectConfig(project, cfg); + + PushOneCommit.Result r = + pushFactory + .create(db, admin.getIdent(), testRepo, "subject", "a", "a1") + .to("refs/for/master"); + r.assertOkStatus(); + + sender.clear(); + + r = + pushFactory + .create(db, admin.getIdent(), testRepo, "subject", "a", "a2", r.getChangeId()) + .to("refs/for/master%wip"); + r.assertOkStatus(); + + assertThat(sender.getMessages()).isEmpty(); + } + + @Test public void watchProject() throws Exception { // watch project String watchedProject = createProject("watchedProject").get(); setApiUser(user); - watch(watchedProject, null); + watch(watchedProject); // push a change to watched project -> should trigger email notification setApiUser(admin); @@ -208,7 +327,7 @@ watch(watchedProject, "file:a.txt"); // watch other project as user - watch(otherWatchedProject, null); + watch(otherWatchedProject); // push a change to watched file -> should trigger email notification for // user @@ -231,9 +350,9 @@ sender.clear(); // watch project as user2 - TestAccount user2 = accounts.create("user2", "user2@test.com", "User2"); + TestAccount user2 = accountCreator.create("user2", "user2@test.com", "User2"); setApiUser(user2); - watch(watchedProject, null); + watch(watchedProject); // push a change to non-watched file -> should not trigger email // notification for user, only for user2 @@ -297,7 +416,7 @@ setApiUser(user); // watch the All-Projects project to watch all projects - watch(allProjects.get(), null); + watch(allProjects.get()); // push a change to any project -> should trigger email notification setApiUser(admin); @@ -348,9 +467,9 @@ sender.clear(); // watch project as user2 - TestAccount user2 = accounts.create("user2", "user2@test.com", "User2"); + TestAccount user2 = accountCreator.create("user2", "user2@test.com", "User2"); setApiUser(user2); - watch(anyProject, null); + watch(anyProject); // push a change to non-watched file in any project -> should not trigger // email notification for user, only for user2 @@ -414,7 +533,7 @@ // watch project String watchedProject = createProject("watchedProject").get(); setApiUser(user); - watch(watchedProject, null); + watch(watchedProject); // push a draft change to watched project -> should not trigger email notification setApiUser(admin); @@ -437,21 +556,21 @@ // create group that can view all drafts GroupInfo groupThatCanViewDrafts = gApi.groups().create("groupThatCanViewDrafts").get(); grant( - Permission.VIEW_DRAFTS, new Project.NameKey(watchedProject), "refs/*", + Permission.VIEW_DRAFTS, false, new AccountGroup.UUID(groupThatCanViewDrafts.id)); // watch project as user that can't view drafts setApiUser(user); - watch(watchedProject, null); + watch(watchedProject); // watch project as user that can view all drafts TestAccount userThatCanViewDrafts = - accounts.create("user2", "user2@test.com", "User2", groupThatCanViewDrafts.name); + accountCreator.create("user2", "user2@test.com", "User2", groupThatCanViewDrafts.name); setApiUser(userThatCanViewDrafts); - watch(watchedProject, null); + watch(watchedProject); // push a draft change to watched project -> should trigger email notification for // userThatCanViewDrafts, but not for user @@ -478,7 +597,7 @@ // watch project String watchedProject = createProject("watchedProject").get(); setApiUser(user); - watch(watchedProject, null); + watch(watchedProject); // push a change to watched project setApiUser(admin); @@ -520,9 +639,7 @@ @Test public void deleteAllProjectWatchesIfWatchConfigIsTheOnlyFileInUserBranch() throws Exception { // Create account that has no files in its refs/users/ branch. - Account.Id id = new Account.Id(db.nextAccountId()); - Account a = new Account(id, TimeUtil.nowTs()); - db.accounts().insert(Collections.singleton(a)); + Account.Id id = accountCreator.create().id; // Add a project watch so that a watch.config file in the refs/users/ branch is created. Map<ProjectWatchKey, Set<NotifyType>> watches = new HashMap<>(); @@ -535,4 +652,70 @@ watchConfig.deleteAllProjectWatches(id); assertThat(watchConfig.getProjectWatches(id)).isEmpty(); } + + @Test + public void watchProjectNoNotificationForPrivateChange() throws Exception { + // watch project + String watchedProject = createProject("watchedProject").get(); + setApiUser(user); + watch(watchedProject); + + // push a private change to watched project -> should not trigger email notification + setApiUser(admin); + TestRepository<InMemoryRepository> watchedRepo = + cloneProject(new Project.NameKey(watchedProject), admin); + PushOneCommit.Result r = + pushFactory + .create(db, admin.getIdent(), watchedRepo, "private change", "a", "a1") + .to("refs/for/master%private"); + r.assertOkStatus(); + + // assert email notification + assertThat(sender.getMessages()).isEmpty(); + } + + @Test + public void watchProjectNotifyOnPrivateChange() throws Exception { + String watchedProject = createProject("watchedProject").get(); + + // create group that can view all private changes + GroupInfo groupThatCanViewPrivateChanges = + gApi.groups().create("groupThatCanViewPrivateChanges").get(); + grant( + new Project.NameKey(watchedProject), + "refs/*", + Permission.VIEW_PRIVATE_CHANGES, + false, + new AccountGroup.UUID(groupThatCanViewPrivateChanges.id)); + + // watch project as user that can't view private changes + setApiUser(user); + watch(watchedProject); + + // watch project as user that can view all private change + TestAccount userThatCanViewPrivateChanges = + accountCreator.create( + "user2", "user2@test.com", "User2", groupThatCanViewPrivateChanges.name); + setApiUser(userThatCanViewPrivateChanges); + watch(watchedProject); + + // push a private change to watched project -> should trigger email notification for + // userThatCanViewPrivateChanges, but not for user + setApiUser(admin); + TestRepository<InMemoryRepository> watchedRepo = + cloneProject(new Project.NameKey(watchedProject), admin); + PushOneCommit.Result r = + pushFactory + .create(db, admin.getIdent(), watchedRepo, "TRIGGER", "a", "a1") + .to("refs/for/master%private"); + r.assertOkStatus(); + + // assert email notification + List<Message> messages = sender.getMessages(); + assertThat(messages).hasSize(1); + Message m = messages.get(0); + assertThat(m.rcpt()).containsExactly(userThatCanViewPrivateChanges.emailAddress); + assertThat(m.body()).contains("Change subject: TRIGGER\n"); + assertThat(m.body()).contains("Gerrit-PatchSet: 1\n"); + } }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java index b1efb4a..19fcff9 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -83,10 +83,7 @@ assertThat(userSshSession.hasError()).isTrue(); String error = userSshSession.getError(); assertThat(error).isNotNull(); - assertError( - "One of the following capabilities is required to access this" - + " resource: [runGC, maintainServer]", - error); + assertError("maintain server not permitted", error); } @Test
diff --git a/gerrit-server/src/test/resources/com/google/gerrit/server/mail/send/tlds-alpha-by-domain.txt b/gerrit-acceptance-tests/src/test/resources/com/google/gerrit/acceptance/server/mail/tlds-alpha-by-domain.txt similarity index 82% rename from gerrit-server/src/test/resources/com/google/gerrit/server/mail/send/tlds-alpha-by-domain.txt rename to gerrit-acceptance-tests/src/test/resources/com/google/gerrit/acceptance/server/mail/tlds-alpha-by-domain.txt index 9edf6a4..4231f76 100644 --- a/gerrit-server/src/test/resources/com/google/gerrit/server/mail/send/tlds-alpha-by-domain.txt +++ b/gerrit-acceptance-tests/src/test/resources/com/google/gerrit/acceptance/server/mail/tlds-alpha-by-domain.txt
@@ -1,10 +1,13 @@ -# Version 2016060601, Last Updated Tue Jun 7 07:07:01 2016 UTC +# Version 2017032102, Last Updated Wed Mar 22 07:07:01 2017 UTC # From http://data.iana.org/TLD/tlds-alpha-by-domain.txt AAA AARP +ABARTH ABB ABBOTT ABBVIE +ABC +ABLE ABOGADO ABUDHABI AC @@ -22,30 +25,43 @@ AE AEG AERO -#! AETNA +AETNA AF +AFAMILYCOMPANY AFL +#! AFRICA AG AGAKHAN AGENCY AI AIG +AIGO +AIRBUS AIRFORCE AIRTEL AKDN AL +ALFAROMEO ALIBABA ALIPAY ALLFINANZ +ALLSTATE ALLY ALSACE +ALSTOM AM +AMERICANEXPRESS +AMERICANFAMILY +AMEX +AMFAM AMICA AMSTERDAM ANALYTICS ANDROID ANQUAN +ANZ AO +AOL APARTMENTS APP APPLE @@ -56,16 +72,21 @@ ARCHI ARMY ARPA +ART ARTE AS +ASDA ASIA ASSOCIATES AT +ATHLETA ATTORNEY AU AUCTION AUDI +AUDIBLE AUDIO +AUSPOST AUTHOR AUTO AUTOS @@ -79,6 +100,8 @@ BA BABY BAIDU +BANAMEX +BANANAREPUBLIC BAND BANK BAR @@ -87,20 +110,25 @@ BARCLAYS BAREFOOT BARGAINS +BASEBALL +BASKETBALL BAUHAUS BAYERN BB BBC +BBT BBVA BCG BCN BD BE BEATS +BEAUTY BEER BENTLEY BERLIN BEST +BESTBUY BET BF BG @@ -117,7 +145,9 @@ BJ BLACK BLACKFRIDAY -#! BLOG +BLANCO +BLOCKBUSTER +BLOG BLOOMBERG BLUE BM @@ -129,15 +159,19 @@ BO BOATS BOEHRINGER +BOFA BOM BOND BOO BOOK +BOOKING BOOTS BOSCH BOSTIK +BOSTON BOT BOUTIQUE +BOX BR BRADESCO BRIDGESTONE @@ -164,12 +198,15 @@ CAFE CAL CALL +CALVINKLEIN +CAM CAMERA CAMP CANCERRESEARCH CANON CAPETOWN CAPITAL +CAPITALONE CAR CARAVAN CARDS @@ -179,12 +216,17 @@ CARS CARTIER CASA +CASE +CASEIH CASH CASINO CAT CATERING +CATHOLIC CBA CBN +CBRE +CBS CC CD CEB @@ -201,14 +243,18 @@ CHASE CHAT CHEAP +CHINTAI CHLOE CHRISTMAS CHROME +CHRYSLER CHURCH CI CIPRIANI CIRCLE CISCO +CITADEL +CITI CITIC CITY CITYEATS @@ -232,6 +278,7 @@ COLLEGE COLOGNE COM +COMCAST COMMBANK COMMUNITY COMPANY @@ -244,6 +291,7 @@ CONTACT CONTRACTORS COOKING +COOKINGCHANNEL COOL COOP CORSICA @@ -258,6 +306,7 @@ CRICKET CROWN CRS +CRUISE CRUISES CSC CU @@ -272,13 +321,15 @@ DABUR DAD DANCE +DATA DATE DATING DATSUN DAY DCLK -#! DDS +DDS DE +DEAL DEALER DEALS DEGREE @@ -292,33 +343,44 @@ DESI DESIGN DEV -#! DHL +DHL DIAMONDS DIET DIGITAL DIRECT DIRECTORY DISCOUNT +DISCOVER +DISH +DIY DJ DK DM DNP DO DOCS +DOCTOR +DODGE DOG DOHA DOMAINS -#! DOT +DOT DOWNLOAD DRIVE -#! DTV +DTV DUBAI +DUCK +DUNLOP +DUNS +DUPONT DURBAN DVAG +DVR DZ EARTH EAT EC +ECO EDEKA EDU EDUCATION @@ -330,13 +392,16 @@ ENGINEER ENGINEERING ENTERPRISES +EPOST EPSON EQUIPMENT ER +ERICSSON ERNI ES ESQ ESTATE +ESURANCE ET EU EUROVISION @@ -356,15 +421,22 @@ FAN FANS FARM +FARMERS FASHION FAST +FEDEX FEEDBACK +FERRARI FERRERO FI +FIAT +FIDELITY +FIDO FILM FINAL FINANCE FINANCIAL +FIRE FIRESTONE FIRMDALE FISH @@ -375,14 +447,15 @@ FK FLICKR FLIGHTS -#! FLIR +FLIR FLORIST FLOWERS -FLSMIDTH FLY FM FO FOO +FOOD +FOODNETWORK FOOTBALL FORD FOREX @@ -391,11 +464,16 @@ FOUNDATION FOX FR +FREE FRESENIUS FRL FROGANS +FRONTDOOR FRONTIER FTR +FUJITSU +FUJIXEROX +FUN FUND FURNITURE FUTBOL @@ -406,7 +484,8 @@ GALLO GALLUP GAME -#! GAMES +GAMES +GAP GARDEN GB GBIZ @@ -416,6 +495,7 @@ GEA GENT GENTING +GEORGE GF GG GGEE @@ -426,6 +506,7 @@ GIVES GIVING GL +GLADE GLASS GLE GLOBAL @@ -436,10 +517,13 @@ GMO GMX GN +GODADDY GOLD GOLDPOINT GOLF GOO +GOODHANDS +GOODYEAR GOOG GOOGLE GOP @@ -457,7 +541,7 @@ GS GT GU -#! GUARDIAN +GUARDIAN GUCCI GUGE GUIDE @@ -465,9 +549,12 @@ GURU GW GY +HAIR HAMBURG HANGOUT HAUS +HBO +HDFC HDFCBANK HEALTH HEALTHCARE @@ -475,23 +562,29 @@ HELSINKI HERE HERMES +HGTV HIPHOP -#! HISAMITSU +HISAMITSU HITACHI HIV HK -#! HKT +HKT HM HN HOCKEY HOLDINGS HOLIDAY HOMEDEPOT +HOMEGOODS HOMES +HOMESENSE HONDA +HONEYWELL HORSE +HOSPITAL HOST HOSTING +HOT HOTELES HOTMAIL HOUSE @@ -501,6 +594,8 @@ HT HTC HU +HUGHES +HYATT HYUNDAI IBM ICBC @@ -508,11 +603,13 @@ ICU ID IE +IEEE IFM -IINET +IKANO IL IM IMAMAT +IMDB IMMO IMMOBILIEN IN @@ -525,7 +622,9 @@ INSURANCE INSURE INT +INTEL INTERNATIONAL +INTUIT INVESTMENTS IO IPIRANGA @@ -539,14 +638,18 @@ ISTANBUL IT ITAU +ITV +IVECO IWC JAGUAR JAVA JCB JCP JE +JEEP JETZT JEWELRY +JIO JLC JLL JM @@ -561,6 +664,7 @@ JPMORGAN JPRS JUEGOS +JUNIPER KAUFEN KDDI KE @@ -574,12 +678,14 @@ KIA KIM KINDER +KINDLE KITCHEN KIWI KM KN KOELN KOMATSU +KOSHER KP KPMG KPN @@ -593,14 +699,18 @@ KZ LA LACAIXA +LADBROKES LAMBORGHINI LAMER LANCASTER +LANCIA +LANCOME LAND LANDROVER LANXESS LASALLE LAT +LATINO LATROBE LAW LAWYER @@ -609,7 +719,9 @@ LDS LEASE LECLERC +LEFRAK LEGAL +LEGO LEXUS LGBT LI @@ -620,37 +732,43 @@ LIFESTYLE LIGHTING LIKE +LILLY LIMITED LIMO LINCOLN LINDE LINK -#! LIPSY +LIPSY LIVE LIVING LIXIL LK LOAN LOANS -#! LOCKER +LOCKER LOCUS +LOFT LOL LONDON LOTTE LOTTO LOVE +LPL +LPLFINANCIAL LR LS LT LTD LTDA LU +LUNDBECK LUPIN LUXE LUXURY LV LY MA +MACYS MADRID MAIF MAISON @@ -662,9 +780,14 @@ MARKETING MARKETS MARRIOTT -#! MATTEL +MARSHALLS +MASERATI +MATTEL MBA MC +MCD +MCDONALDS +MCKINSEY MD ME MED @@ -676,22 +799,26 @@ MEN MENU MEO -#! METLIFE +METLIFE MG MH MIAMI MICROSOFT MIL MINI +MINT +MIT +MITSUBISHI MK ML -#! MLB +MLB MLS MM MMA MN MO MOBI +MOBILE MOBILY MODA MOE @@ -699,10 +826,13 @@ MOM MONASH MONEY +MONSTER MONTBLANC +MOPAR MORMON MORTGAGE MOSCOW +MOTO MOTORCYCLES MOV MOVIE @@ -711,6 +841,7 @@ MQ MR MS +MSD MT MTN MTPC @@ -718,37 +849,42 @@ MU MUSEUM MUTUAL -MUTUELLE MV MW MX MY MZ NA +NAB NADEX NAGOYA NAME +NATIONWIDE NATURA NAVY +NBA NC NE NEC NET NETBANK -#! NETFLIX +NETFLIX NETWORK NEUSTAR NEW +NEWHOLLAND NEWS -#! NEXT -#! NEXTDIRECT +NEXT +NEXTDIRECT NEXUS NF +NFL NG NGO NHK NI NICO +NIKE NIKON NINJA NISSAN @@ -758,8 +894,9 @@ NOKIA NORTHWESTERNMUTUAL NORTON +NOW NOWRUZ -#! NOWTV +NOWTV NP NR NRA @@ -769,30 +906,37 @@ NYC NZ OBI +OBSERVER +OFF OFFICE OKINAWA -#! OLAYAN -#! OLAYANGROUP -#! OLLO +OLAYAN +OLAYANGROUP +OLDNAVY +OLLO OM OMEGA ONE ONG ONL ONLINE +ONYOURSIDE OOO +OPEN ORACLE ORANGE ORG ORGANIC +ORIENTEXPRESS ORIGINS OSAKA OTSUKA -#! OTT +OTT OVH PA PAGE PAMPEREDCHEF +PANASONIC PANERAI PARIS PARS @@ -800,14 +944,17 @@ PARTS PARTY PASSAGENS -#! PCCW +PAY +PCCW PE PET PF +PFIZER PG PH PHARMACY PHILIPS +PHONE PHOTO PHOTOGRAPHY PHOTOS @@ -820,7 +967,7 @@ PIN PING PINK -#! PIONEER +PIONEER PIZZA PK PL @@ -831,13 +978,17 @@ PLUS PM PN +PNC POHL POKER +POLITIE PORN POST PR +PRAMERICA PRAXI PRESS +PRIME PRO PROD PRODUCTIONS @@ -847,6 +998,8 @@ PROPERTIES PROPERTY PROTECTION +PRU +PRUDENTIAL PS PT PUB @@ -857,10 +1010,13 @@ QPON QUEBEC QUEST +QVC RACING +RADIO +RAID RE READ -#! REALESTATE +REALESTATE REALTOR REALTY RECIPES @@ -871,6 +1027,7 @@ REISE REISEN REIT +RELIANCE REN RENT RENTALS @@ -883,14 +1040,18 @@ REVIEWS REXROTH RICH -#! RICHARDLI +RICHARDLI RICOH +RIGHTATHOME +RIL RIO RIP +RMIT RO ROCHER ROCKS RODEO +ROGERS ROOM RS RSVP @@ -907,6 +1068,7 @@ SAKURA SALE SALON +SAMSCLUB SAMSUNG SANDVIK SANDVIKCOROMANT @@ -915,6 +1077,7 @@ SAPO SARL SAS +SAVE SAXO SB SBI @@ -929,16 +1092,19 @@ SCHULE SCHWARZ SCIENCE +SCJOHNSON SCOR SCOT SD SE SEAT +SECURE SECURITY SEEK SELECT SENER SERVICES +SES SEVEN SEW SEX @@ -946,17 +1112,21 @@ SFR SG SH +SHANGRILA SHARP SHAW SHELL SHIA SHIKSHA SHOES -#! SHOP +SHOP +SHOPPING SHOUJI SHOW +SHOWTIME SHRIRAM SI +SILK SINA SINGLES SITE @@ -967,7 +1137,9 @@ SKY SKYPE SL +SLING SM +SMART SMILE SN SNCF @@ -988,8 +1160,10 @@ SPREADBETTING SR SRL +SRT ST STADA +STAPLES STAR STARHUB STATEBANK @@ -1014,6 +1188,7 @@ SUZUKI SV SWATCH +SWIFTCOVER SWISS SX SY @@ -1025,6 +1200,7 @@ TAIPEI TALK TAOBAO +TARGET TATAMOTORS TATAR TATTOO @@ -1033,6 +1209,7 @@ TC TCI TD +TDK TEAM TECH TECHNOLOGY @@ -1048,6 +1225,7 @@ THD THEATER THEATRE +TIAA TICKETS TIENDA TIFFANY @@ -1055,7 +1233,10 @@ TIRES TIROL TJ +TJMAXX +TJX TK +TKMAXX TL TM TMALL @@ -1077,6 +1258,7 @@ TRADING TRAINING TRAVEL +TRAVELCHANNEL TRAVELERS TRAVELERSINSURANCE TRUST @@ -1091,20 +1273,23 @@ TW TZ UA +UBANK UBS +UCONNECT UG UK UNICOM UNIVERSITY UNO UOL -#! UPS +UPS US UY UZ VA VACATIONS VANA +VANGUARD VC VE VEGAS @@ -1122,14 +1307,17 @@ VIN VIP VIRGIN +VISA VISION VISTA VISTAPRINT VIVA +VIVO VLAANDEREN VN VODKA VOLKSWAGEN +VOLVO VOTE VOTING VOTO @@ -1137,10 +1325,11 @@ VU VUELOS WALES +WALMART WALTER WANG WANGGOU -#! WARMAN +WARMAN WATCH WATCHES WEATHER @@ -1160,16 +1349,20 @@ WIN WINDOWS WINE +WINNERS WME WOLTERSKLUWER +WOODSIDE WORK WORKS WORLD +WOW WS WTC WTF XBOX XEROX +XFINITY XIHUAN XIN XN--11B4C3D @@ -1179,22 +1372,27 @@ XN--3BST00M XN--3DS443G XN--3E0B707E +XN--3OQ18VL8PN36A XN--3PXU8K XN--42C2D9A XN--45BRJ9C XN--45Q11C XN--4GBRIM +XN--54B7FTA0CC XN--55QW42G XN--55QX5D +XN--5SU34J936BGSG XN--5TZM5G XN--6FRZ82G XN--6QQ986B3XL XN--80ADXHKS XN--80AO21A +XN--80AQECDR1A XN--80ASEHDB XN--80ASWG XN--8Y0A063A XN--90A3AC +XN--90AE XN--90AIS XN--9DBQ2A XN--9ET52U @@ -1229,6 +1427,7 @@ XN--G2XX48C XN--GCKR3F0F XN--GECRJ9C +XN--GK3AT1E XN--H2BRJ9C XN--HXT814E XN--I1B6B1A6A2E @@ -1252,12 +1451,14 @@ XN--MGBA7C0BBN0A XN--MGBAAM7A8H XN--MGBAB2BD +XN--MGBAI9AZGQP6J XN--MGBAYH7GPA XN--MGBB9FBPOB XN--MGBBH1A71E XN--MGBC0A9AZCG XN--MGBCA7DZDO XN--MGBERP4A5D4AR +XN--MGBI4ECEXP XN--MGBPL2FH XN--MGBT3DHD XN--MGBTX2B @@ -1287,6 +1488,7 @@ XN--SES554G XN--T60B56A XN--TCKWE +XN--TIQ49XQYJ XN--UNUP4Y XN--VERMGENSBERATER-CTB XN--VERMGENSBERATUNG-PWB @@ -1319,10 +1521,11 @@ YT YUN ZA -#! ZAPPOS +ZAPPOS ZARA ZERO ZIP +ZIPPO ZM ZONE ZUERICH
diff --git a/gerrit-antlr/src/main/java/com/google/gerrit/server/query/QueryParseException.java b/gerrit-antlr/src/main/java/com/google/gerrit/server/query/QueryParseException.java index 80cffbb..9495d19 100644 --- a/gerrit-antlr/src/main/java/com/google/gerrit/server/query/QueryParseException.java +++ b/gerrit-antlr/src/main/java/com/google/gerrit/server/query/QueryParseException.java
@@ -22,11 +22,11 @@ public class QueryParseException extends Exception { private static final long serialVersionUID = 1L; - public QueryParseException(final String message) { + public QueryParseException(String message) { super(message); } - public QueryParseException(final String msg, final Throwable why) { + public QueryParseException(String msg, Throwable why) { super(msg, why); } }
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java index 474fa36..1283452 100644 --- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java +++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -115,7 +115,7 @@ @Override public void start() { if (executor != null) { - for (final H2CacheImpl<?, ?> cache : caches) { + for (H2CacheImpl<?, ?> cache : caches) { executor.execute(cache::start); @SuppressWarnings("unused") Future<?> possiblyIgnoredError =
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java index 3b86c95..eaa9af9 100644 --- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java +++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -131,11 +131,26 @@ @Override public V get(K key, Callable<? extends V> valueLoader) throws ExecutionException { - return mem.get(key, new LoadingCallable(key, valueLoader)).value; + return mem.get( + key, + () -> { + if (store.mightContain(key)) { + ValueHolder<V> h = store.getIfPresent(key); + if (h != null) { + return h; + } + } + + ValueHolder<V> h = new ValueHolder<>(valueLoader.call()); + h.created = TimeUtil.nowMs(); + executor.execute(() -> store.put(key, h)); + return h; + }) + .value; } @Override - public void put(final K key, V val) { + public void put(K key, V val) { final ValueHolder<V> h = new ValueHolder<>(val); h.created = TimeUtil.nowMs(); mem.put(key, h); @@ -144,7 +159,7 @@ @SuppressWarnings("unchecked") @Override - public void invalidate(final Object key) { + public void invalidate(Object key) { if (keyType.getRawType().isInstance(key) && store.mightContain((K) key)) { executor.execute(() -> store.invalidate((K) key)); } @@ -186,7 +201,7 @@ store.close(); } - void prune(final ScheduledExecutorService service) { + void prune(ScheduledExecutorService service) { store.prune(mem); Calendar cal = Calendar.getInstance(); @@ -224,7 +239,7 @@ } @Override - public ValueHolder<V> load(final K key) throws Exception { + public ValueHolder<V> load(K key) throws Exception { if (store.mightContain(key)) { ValueHolder<V> h = store.getIfPresent(key); if (h != null) { @@ -239,31 +254,6 @@ } } - private class LoadingCallable implements Callable<ValueHolder<V>> { - private final K key; - private final Callable<? extends V> loader; - - LoadingCallable(K key, Callable<? extends V> loader) { - this.key = key; - this.loader = loader; - } - - @Override - public ValueHolder<V> call() throws Exception { - if (store.mightContain(key)) { - ValueHolder<V> h = store.getIfPresent(key); - if (h != null) { - return h; - } - } - - final ValueHolder<V> h = new ValueHolder<>(loader.call()); - h.created = TimeUtil.nowMs(); - executor.execute(() -> store.put(key, h)); - return h; - } - } - private static class KeyType<K> { String columnType() { return "OTHER";
diff --git a/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java b/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java index 15e0de0..80bca6d 100644 --- a/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java +++ b/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -24,7 +24,6 @@ import com.google.gerrit.server.cache.h2.H2CacheImpl.SqlStore; import com.google.gerrit.server.cache.h2.H2CacheImpl.ValueHolder; import com.google.inject.TypeLiteral; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Before; @@ -54,12 +53,9 @@ assertTrue( impl.get( "foo", - new Callable<Boolean>() { - @Override - public Boolean call() throws Exception { - called.set(true); - return true; - } + () -> { + called.set(true); + return true; })); assertTrue("used Callable", called.get()); assertTrue("exists in cache", impl.getIfPresent("foo")); @@ -70,12 +66,9 @@ assertTrue( impl.get( "foo", - new Callable<Boolean>() { - @Override - public Boolean call() throws Exception { - called.set(true); - return true; - } + () -> { + called.set(true); + return true; })); assertFalse("did not invoke Callable", called.get()); }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/Die.java b/gerrit-common/src/main/java/com/google/gerrit/common/Die.java index 6a1f304..5ad5ae8 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/Die.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/Die.java
@@ -17,11 +17,11 @@ public class Die extends RuntimeException { private static final long serialVersionUID = 1L; - public Die(final String why) { + public Die(String why) { super(why); } - public Die(final String why, final Throwable cause) { + public Die(String why, Throwable cause) { super(why, cause); } }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java index 4c5583f..24e3808 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/FileUtil.java
@@ -39,18 +39,18 @@ return !Arrays.equals(curVers, newVers); } - public static void mkdir(final File path) { + public static void mkdir(File path) { if (!path.isDirectory() && !path.mkdir()) { throw new Die("Cannot make directory " + path); } } - public static void chmod(final int mode, final Path path) { + public static void chmod(int mode, Path path) { // TODO(dborowitz): Is there a portable way to do this with NIO? chmod(mode, path.toFile()); } - public static void chmod(final int mode, final File path) { + public static void chmod(int mode, File path) { path.setReadable(false, false /* all */); path.setWritable(false, false /* all */); path.setExecutable(false, false /* all */);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java index 624bcea..526e88b 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
@@ -32,7 +32,7 @@ @GwtIncompatible("Unemulated methods in Class and OutputStream") public final class IoUtil { - public static void copyWithThread(final InputStream src, final OutputStream dst) { + public static void copyWithThread(InputStream src, OutputStream dst) { new Thread("IoUtil-Copy") { @Override public void run() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java index 692285f..6e59284 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -55,7 +55,7 @@ return "/c/" + c + ",edit/"; } - public static String toChange(final Change.Id c) { + public static String toChange(Change.Id c) { return "/c/" + c + "/"; } @@ -72,15 +72,15 @@ return u; } - public static String toChange(final PatchSet.Id ps) { + public static String toChange(PatchSet.Id ps) { return "/c/" + ps.getParentKey() + "/" + ps.getId(); } - public static String toProject(final Project.NameKey p) { + public static String toProject(Project.NameKey p) { return ADMIN_PROJECTS + p.get(); } - public static String toProjectAcceess(final Project.NameKey p) { + public static String toProjectAcceess(Project.NameKey p) { return "/admin/projects/" + p.get() + ",access"; } @@ -100,7 +100,7 @@ return toChangeQuery(op("assignee", fullname)); } - public static String toCustomDashboard(final String params) { + public static String toCustomDashboard(String params) { return "/dashboard/?" + params; }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/ProjectAccessUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/ProjectAccessUtil.java index 5be0878..0369bfe 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/ProjectAccessUtil.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/ProjectAccessUtil.java
@@ -26,7 +26,7 @@ public class ProjectAccessUtil { public static List<AccessSection> mergeSections(List<AccessSection> src) { Map<String, AccessSection> map = new LinkedHashMap<>(); - for (final AccessSection section : src) { + for (AccessSection section : src) { if (section.getPermissions().isEmpty()) { continue; } @@ -44,21 +44,21 @@ public static List<AccessSection> removeEmptyPermissionsAndSections( final List<AccessSection> src) { final Set<AccessSection> sectionsToRemove = new HashSet<>(); - for (final AccessSection section : src) { + for (AccessSection section : src) { final Set<Permission> permissionsToRemove = new HashSet<>(); - for (final Permission permission : section.getPermissions()) { + for (Permission permission : section.getPermissions()) { if (permission.getRules().isEmpty()) { permissionsToRemove.add(permission); } } - for (final Permission permissionToRemove : permissionsToRemove) { + for (Permission permissionToRemove : permissionsToRemove) { section.remove(permissionToRemove); } if (section.getPermissions().isEmpty()) { sectionsToRemove.add(section); } } - for (final AccessSection sectionToRemove : sectionsToRemove) { + for (AccessSection sectionToRemove : sectionsToRemove) { src.remove(sectionToRemove); } return src;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/RawInputUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/RawInputUtil.java index 961f43a..f59d4a9 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/RawInputUtil.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/RawInputUtil.java
@@ -30,7 +30,7 @@ return create(content.getBytes(UTF_8)); } - public static RawInput create(final byte[] bytes, final String contentType) { + public static RawInput create(byte[] bytes, String contentType) { Preconditions.checkNotNull(bytes); Preconditions.checkArgument(bytes.length > 0); return new RawInput() { @@ -51,11 +51,11 @@ }; } - public static RawInput create(final byte[] bytes) { + public static RawInput create(byte[] bytes) { return create(bytes, "application/octet-stream"); } - public static RawInput create(final HttpServletRequest req) { + public static RawInput create(HttpServletRequest req) { return new RawInput() { @Override public String getContentType() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java index 4e14514..cfecd78 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
@@ -121,7 +121,7 @@ } @Override - public boolean equals(final Object obj) { + public boolean equals(Object obj) { if (!super.equals(obj) || !(obj instanceof AccessSection)) { return false; }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java index d6ddddb..788a26d 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
@@ -31,7 +31,7 @@ * <p>This constructor should only be a last-ditch effort, when the usual account lookup has * failed and a stale account id has been discovered in the data store. */ - public AccountInfo(final Account.Id id) { + public AccountInfo(Account.Id id) { this.id = id; } @@ -40,7 +40,7 @@ * * @param a the data store record holding the specific account details. */ - public AccountInfo(final Account a) { + public AccountInfo(Account a) { id = a.getId(); fullName = a.getFullName(); preferredEmail = a.getPreferredEmail(); @@ -66,7 +66,7 @@ return preferredEmail; } - public void setPreferredEmail(final String email) { + public void setPreferredEmail(String email) { preferredEmail = email; }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/FilenameComparator.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/FilenameComparator.java index 9c34c97..e0a6569 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/FilenameComparator.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/FilenameComparator.java
@@ -29,7 +29,7 @@ private FilenameComparator() {} @Override - public int compare(final String path1, final String path2) { + public int compare(String path1, String path2) { if (Patch.COMMIT_MSG.equals(path1) && Patch.COMMIT_MSG.equals(path2)) { return 0; } else if (Patch.COMMIT_MSG.equals(path1)) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java index 4c9b64a..6fd0e77 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -15,6 +15,7 @@ package com.google.gerrit.common.data; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -115,6 +116,9 @@ private static final List<String> NAMES_ALL; private static final List<String> NAMES_LC; + private static final String[] RANGE_NAMES = { + QUERY_LIMIT, BATCH_CHANGES_LIMIT, + }; static { NAMES_ALL = new ArrayList<>(); @@ -158,7 +162,16 @@ /** @return true if the capability should have a range attached. */ public static boolean hasRange(String varName) { - return QUERY_LIMIT.equalsIgnoreCase(varName) || BATCH_CHANGES_LIMIT.equalsIgnoreCase(varName); + for (String n : RANGE_NAMES) { + if (n.equalsIgnoreCase(varName)) { + return true; + } + } + return false; + } + + public static List<String> getRangeNames() { + return Collections.unmodifiableList(Arrays.asList(RANGE_NAMES)); } /** @return the valid range for the capability if it has one, otherwise null. */
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java index b8e498f..0c06868 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
@@ -29,7 +29,7 @@ return null; } - public static GroupDescription.Internal forAccountGroup(final AccountGroup group) { + public static GroupDescription.Internal forAccountGroup(AccountGroup group) { return new GroupDescription.Internal() { @Override public AccountGroup.UUID getGroupUUID() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java index 1f746c4..5de0aad 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java
@@ -31,7 +31,7 @@ * <p>This constructor should only be a last-ditch effort, when the usual group lookup has failed * and a stale group id has been discovered in the data store. */ - public GroupInfo(final AccountGroup.UUID uuid) { + public GroupInfo(AccountGroup.UUID uuid) { this.uuid = uuid; }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java index 8362281..dc22d62 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
@@ -14,10 +14,14 @@ package com.google.gerrit.common.data; +import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.AccountGroup; /** Describes a group within a projects {@link AccessSection}s. */ public class GroupReference implements Comparable<GroupReference> { + + private static final String PREFIX = "group "; + /** @return a new reference to the given group description. */ public static GroupReference forGroup(AccountGroup group) { return new GroupReference(group.getGroupUUID(), group.getName()); @@ -27,10 +31,16 @@ return new GroupReference(group.getGroupUUID(), group.getName()); } - public static GroupReference fromString(String ref) { - String name = ref.substring(ref.indexOf("[") + 1, ref.lastIndexOf("/")).trim(); - String uuid = ref.substring(ref.lastIndexOf("/") + 1, ref.lastIndexOf("]")).trim(); - return new GroupReference(new AccountGroup.UUID(uuid), name); + public static boolean isGroupReference(String configValue) { + return configValue != null && configValue.startsWith(PREFIX); + } + + @Nullable + public static String extractGroupName(String configValue) { + if (!isGroupReference(configValue)) { + return null; + } + return configValue.substring(PREFIX.length()).trim(); } protected String uuid; @@ -78,6 +88,10 @@ return o instanceof GroupReference && compareTo((GroupReference) o) == 0; } + public String toConfigValue() { + return PREFIX + name; + } + @Override public String toString() { return "Group[" + getName() + " / " + getUUID() + "]";
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java index 6d427e7..c90e1fd 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
@@ -274,7 +274,7 @@ return byValue.get(value); } - public LabelValue getValue(final PatchSetApproval ca) { + public LabelValue getValue(PatchSetApproval ca) { initByValue(); return byValue.get(ca.getValue()); } @@ -282,7 +282,7 @@ private void initByValue() { if (byValue == null) { byValue = new HashMap<>(); - for (final LabelValue v : values) { + for (LabelValue v : values) { byValue.put(v.getValue(), v); } }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java index e76db30..d5891d1 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java
@@ -29,7 +29,7 @@ protected LabelTypes() {} - public LabelTypes(final List<? extends LabelType> approvals) { + public LabelTypes(List<? extends LabelType> approvals) { labelTypes = Collections.unmodifiableList(new ArrayList<>(approvals)); }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java index 93b7f90..28e47ee 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
@@ -24,7 +24,7 @@ /** Performs replacements on strings such as <code>Hello ${user}</code>. */ public class ParameterizedString { /** Obtain a string which has no parameters and always produces the value. */ - public static ParameterizedString asis(final String constant) { + public static ParameterizedString asis(String constant) { return new ParameterizedString(new Constant(constant)); } @@ -37,14 +37,14 @@ this(new Constant("")); } - private ParameterizedString(final Constant c) { + private ParameterizedString(Constant c) { pattern = c.text; rawPattern = c.text; patternOps = Collections.<Format>singletonList(c); parameters = Collections.emptyList(); } - public ParameterizedString(final String pattern) { + public ParameterizedString(String pattern) { final StringBuilder raw = new StringBuilder(); final List<Parameter> prs = new ArrayList<>(4); final List<Format> ops = new ArrayList<>(4); @@ -103,7 +103,7 @@ } /** Convert a map of parameters into a value array for binding. */ - public String[] bind(final Map<String, String> params) { + public String[] bind(Map<String, String> params) { final String[] r = new String[parameters.size()]; for (int i = 0; i < r.length; i++) { final StringBuilder b = new StringBuilder(); @@ -114,15 +114,15 @@ } /** Format this string by performing the variable replacements. */ - public String replace(final Map<String, String> params) { + public String replace(Map<String, String> params) { final StringBuilder r = new StringBuilder(); - for (final Format f : patternOps) { + for (Format f : patternOps) { f.format(r, params); } return r.toString(); } - public Builder replace(final String name, final String value) { + public Builder replace(String name, String value) { return new Builder().replace(name, value); } @@ -134,7 +134,7 @@ public final class Builder { private final Map<String, String> params = new HashMap<>(); - public Builder replace(final String name, final String value) { + public Builder replace(String name, String value) { params.put(name, value); return this; } @@ -152,7 +152,7 @@ private static class Constant extends Format { private final String text; - Constant(final String text) { + Constant(String text) { this.text = text; } @@ -166,7 +166,7 @@ private final String name; private final List<Function> functions; - Parameter(final String parameter) { + Parameter(String parameter) { // "parameter[.functions...]" -> (parameter, functions...) final List<String> names = Arrays.asList(parameter.split("\\.")); final List<Function> functs = new ArrayList<>(names.size());
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java index 172be09..3428580 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -16,12 +16,12 @@ import com.google.gerrit.extensions.client.DiffPreferencesInfo; import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; -import com.google.gerrit.prettify.common.EditList; import com.google.gerrit.prettify.common.SparseFileContent; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.Patch.ChangeType; import java.util.List; +import java.util.Set; import org.eclipse.jgit.diff.Edit; public class PatchScript { @@ -48,6 +48,7 @@ private SparseFileContent a; private SparseFileContent b; private List<Edit> edits; + private Set<Edit> editsDueToRebase; private DisplayMethod displayMethodA; private DisplayMethod displayMethodB; private transient String mimeTypeA; @@ -63,30 +64,31 @@ private transient String commitIdB; public PatchScript( - final Change.Key ck, - final ChangeType ct, - final String on, - final String nn, - final FileMode om, - final FileMode nm, - final List<String> h, - final DiffPreferencesInfo dp, - final SparseFileContent ca, - final SparseFileContent cb, - final List<Edit> e, - final DisplayMethod ma, - final DisplayMethod mb, - final String mta, - final String mtb, - final CommentDetail cd, - final List<Patch> hist, - final boolean hf, - final boolean id, - final boolean idf, - final boolean idt, + Change.Key ck, + ChangeType ct, + String on, + String nn, + FileMode om, + FileMode nm, + List<String> h, + DiffPreferencesInfo dp, + SparseFileContent ca, + SparseFileContent cb, + List<Edit> e, + Set<Edit> editsDueToRebase, + DisplayMethod ma, + DisplayMethod mb, + String mta, + String mtb, + CommentDetail cd, + List<Patch> hist, + boolean hf, + boolean id, + boolean idf, + boolean idt, boolean bin, - final String cma, - final String cmb) { + String cma, + String cmb) { changeId = ck; changeType = ct; oldName = on; @@ -98,6 +100,7 @@ a = ca; b = cb; edits = e; + this.editsDueToRebase = editsDueToRebase; displayMethodA = ma; displayMethodB = mb; mimeTypeA = mta; @@ -211,12 +214,8 @@ return edits; } - public Iterable<EditList.Hunk> getHunks() { - int ctx = diffPrefs.context; - if (ctx == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) { - ctx = Math.max(a.size(), b.size()); - } - return new EditList(edits, ctx, a.size(), b.size()).getHunks(); + public Set<Edit> getEditsDueToRebase() { + return editsDueToRebase; } public boolean isBinary() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java index 47c5224..81646fe 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -47,6 +47,7 @@ public static final String SUBMIT = "submit"; public static final String SUBMIT_AS = "submitAs"; public static final String VIEW_DRAFTS = "viewDrafts"; + public static final String VIEW_PRIVATE_CHANGES = "viewPrivateChanges"; private static final List<String> NAMES_LC; private static final int LABEL_INDEX; @@ -74,6 +75,7 @@ NAMES_LC.add(SUBMIT.toLowerCase()); NAMES_LC.add(SUBMIT_AS.toLowerCase()); NAMES_LC.add(VIEW_DRAFTS.toLowerCase()); + NAMES_LC.add(VIEW_PRIVATE_CHANGES.toLowerCase()); NAMES_LC.add(EDIT_TOPIC_NAME.toLowerCase()); NAMES_LC.add(EDIT_HASHTAGS.toLowerCase()); NAMES_LC.add(EDIT_ASSIGNEE.toLowerCase()); @@ -260,7 +262,7 @@ } @Override - public boolean equals(final Object obj) { + public boolean equals(Object obj) { if (!(obj instanceof Permission)) { return false; }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java index 9265830..c50af5c 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
@@ -204,8 +204,7 @@ r.append(' '); } - r.append("group "); - r.append(getGroup().getName()); + r.append(getGroup().toConfigValue()); return r.toString(); } @@ -238,7 +237,7 @@ src = src.substring("+force ".length()).trim(); } - if (mightUseRange && !src.startsWith("group ")) { + if (mightUseRange && !GroupReference.isGroupReference(src)) { int sp = src.indexOf(' '); String range = src.substring(0, sp); @@ -254,10 +253,10 @@ src = src.substring(sp + 1).trim(); } - if (src.startsWith("group ")) { - src = src.substring(6).trim(); + String groupName = GroupReference.extractGroupName(src); + if (groupName != null) { GroupReference group = new GroupReference(); - group.setName(src); + group.setName(groupName); rule.setGroup(group); } else { throw new IllegalArgumentException("Rule must include group: " + orig); @@ -278,7 +277,7 @@ } @Override - public boolean equals(final Object obj) { + public boolean equals(Object obj) { if (!(obj instanceof PermissionRule)) { return false; }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java index f8aa6a0..663379a 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java
@@ -46,7 +46,7 @@ } @Override - public boolean equals(final Object obj) { + public boolean equals(Object obj) { if (!(obj instanceof RefConfigSection)) { return false; }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SshHostKey.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SshHostKey.java index bac9294..05f1611 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SshHostKey.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SshHostKey.java
@@ -22,7 +22,7 @@ protected SshHostKey() {} - public SshHostKey(final String hi, final String hk, final String fp) { + public SshHostKey(String hi, String hk, String fp) { hostIdent = hi; hostKey = hk; fingerprint = fp;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java index 8b740c3..6e3db9e 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
@@ -22,23 +22,23 @@ public static final String MESSAGE = "Group Not Found: "; - public NoSuchGroupException(final AccountGroup.Id key) { + public NoSuchGroupException(AccountGroup.Id key) { this(key, null); } - public NoSuchGroupException(final AccountGroup.UUID key) { + public NoSuchGroupException(AccountGroup.UUID key) { this(key, null); } - public NoSuchGroupException(final AccountGroup.Id key, final Throwable why) { + public NoSuchGroupException(AccountGroup.Id key, Throwable why) { super(MESSAGE + key.toString(), why); } - public NoSuchGroupException(final AccountGroup.UUID key, final Throwable why) { + public NoSuchGroupException(AccountGroup.UUID key, Throwable why) { super(MESSAGE + key.toString(), why); } - public NoSuchGroupException(final AccountGroup.NameKey k, final Throwable why) { + public NoSuchGroupException(AccountGroup.NameKey k, Throwable why) { super(MESSAGE + k.toString(), why); } @@ -46,7 +46,7 @@ this(who, null); } - public NoSuchGroupException(String who, final Throwable why) { + public NoSuchGroupException(String who, Throwable why) { super(MESSAGE + who, why); } }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/UpdateParentFailedException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/UpdateParentFailedException.java index ec8a811..16d5240 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/UpdateParentFailedException.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/UpdateParentFailedException.java
@@ -20,7 +20,7 @@ public static final String MESSAGE = "Update Parent Project Failed: "; - public UpdateParentFailedException(final String message, final Throwable why) { + public UpdateParentFailedException(String message, Throwable why) { super(MESSAGE + ": " + message, why); } }
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java index 2a46c40..c13d8d7 100644 --- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticAccountIndex.java
@@ -38,9 +38,9 @@ import com.google.gson.JsonObject; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.ResultSet; +import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import io.searchbox.client.JestResult; import io.searchbox.core.Bulk; import io.searchbox.core.Bulk.Builder; @@ -76,7 +76,7 @@ private final AccountMapping mapping; private final Provider<AccountCache> accountCache; - @AssistedInject + @Inject ElasticAccountIndex( @GerritServerConfig Config cfg, SitePaths sitePaths,
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java index 4f90de5..54d22c2 100644 --- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -34,6 +34,7 @@ import com.google.gerrit.reviewdb.client.Change.Id; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ReviewerByEmailSet; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; @@ -54,9 +55,9 @@ import com.google.gson.JsonObject; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.ResultSet; +import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import io.searchbox.client.JestResult; import io.searchbox.core.Bulk; import io.searchbox.core.Bulk.Builder; @@ -99,7 +100,7 @@ private final Provider<ReviewDb> db; private final ChangeData.Factory changeDataFactory; - @AssistedInject + @Inject ElasticChangeIndex( @GerritServerConfig Config cfg, Provider<ReviewDb> db, @@ -343,6 +344,36 @@ cd.setReviewers(ReviewerSet.empty()); } + if (source.get(ChangeField.REVIEWER_BY_EMAIL.getName()) != null) { + cd.setReviewersByEmail( + ChangeField.parseReviewerByEmailFieldValues( + FluentIterable.from( + source.get(ChangeField.REVIEWER_BY_EMAIL.getName()).getAsJsonArray()) + .transform(JsonElement::getAsString))); + } else if (fields.contains(ChangeField.REVIEWER_BY_EMAIL.getName())) { + cd.setReviewersByEmail(ReviewerByEmailSet.empty()); + } + + if (source.get(ChangeField.PENDING_REVIEWER.getName()) != null) { + cd.setPendingReviewers( + ChangeField.parseReviewerFieldValues( + FluentIterable.from(source.get(ChangeField.REVIEWER.getName()).getAsJsonArray()) + .transform(JsonElement::getAsString))); + } else if (fields.contains(ChangeField.PENDING_REVIEWER.getName())) { + cd.setPendingReviewers(ReviewerSet.empty()); + } + + if (source.get(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName()) != null) { + cd.setPendingReviewersByEmail( + ChangeField.parseReviewerByEmailFieldValues( + FluentIterable.from( + source + .get(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName()) + .getAsJsonArray()) + .transform(JsonElement::getAsString))); + } else if (fields.contains(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName())) { + cd.setPendingReviewersByEmail(ReviewerByEmailSet.empty()); + } decodeSubmitRecords( source, ChangeField.STORED_SUBMIT_RECORD_STRICT.getName(),
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java index e2c34f2..018f8d8 100644 --- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticGroupIndex.java
@@ -35,9 +35,9 @@ import com.google.gson.JsonObject; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.ResultSet; +import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import io.searchbox.client.JestResult; import io.searchbox.core.Bulk; import io.searchbox.core.Bulk.Builder; @@ -73,7 +73,7 @@ private final GroupMapping mapping; private final Provider<GroupCache> groupCache; - @AssistedInject + @Inject ElasticGroupIndex( @GerritServerConfig Config cfg, SitePaths sitePaths,
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java index 65065f9..a690136 100644 --- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticIndexModule.java
@@ -14,36 +14,51 @@ package com.google.gerrit.elasticsearch; +import static com.google.common.base.Preconditions.checkArgument; + import com.google.gerrit.lifecycle.LifecycleModule; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.index.IndexConfig; import com.google.gerrit.server.index.IndexModule; +import com.google.gerrit.server.index.OnlineUpgrader; import com.google.gerrit.server.index.SingleVersionModule; +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 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; -public class ElasticIndexModule extends LifecycleModule { - private final int threads; - private final Map<String, Integer> singleVersions; - +public class ElasticIndexModule extends AbstractModule { public static ElasticIndexModule singleVersionWithExplicitVersions( Map<String, Integer> versions, int threads) { - return new ElasticIndexModule(versions, threads); + return new ElasticIndexModule(versions, threads, false); } public static ElasticIndexModule latestVersionWithOnlineUpgrade() { - return new ElasticIndexModule(null, 0); + return new ElasticIndexModule(null, 0, true); } - private ElasticIndexModule(Map<String, Integer> singleVersions, int threads) { + public static ElasticIndexModule latestVersionWithoutOnlineUpgrade() { + return new ElasticIndexModule(null, 0, false); + } + + private final Map<String, Integer> singleVersions; + private final int threads; + private final boolean onlineUpgrade; + + private ElasticIndexModule( + Map<String, Integer> singleVersions, int threads, boolean onlineUpgrade) { + if (singleVersions != null) { + checkArgument(!onlineUpgrade, "online upgrade is incompatible with single version map"); + } this.singleVersions = singleVersions; this.threads = threads; + this.onlineUpgrade = onlineUpgrade; } @Override @@ -63,7 +78,7 @@ install(new IndexModule(threads)); if (singleVersions == null) { - listener().to(ElasticVersionManager.class); + install(new MultiVersionModule()); } else { install(new SingleVersionModule(singleVersions)); } @@ -72,6 +87,17 @@ @Provides @Singleton IndexConfig getIndexConfig(@GerritServerConfig Config cfg) { - return IndexConfig.fromConfig(cfg); + return IndexConfig.fromConfig(cfg).separateChangeSubIndexes(true).build(); + } + + private class MultiVersionModule extends LifecycleModule { + @Override + public void configure() { + bind(VersionManager.class).to(ElasticVersionManager.class); + listener().to(ElasticVersionManager.class); + if (onlineUpgrade) { + listener().to(OnlineUpgrader.class); + } + } } }
diff --git a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java index 74a6b69..609c4d9 100644 --- a/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java +++ b/gerrit-elasticsearch/src/main/java/com/google/gerrit/elasticsearch/ElasticVersionManager.java
@@ -16,14 +16,15 @@ import com.google.common.base.MoreObjects; import com.google.common.primitives.Ints; -import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; -import com.google.gerrit.server.index.AbstractVersionManager; import com.google.gerrit.server.index.GerritIndexStatus; import com.google.gerrit.server.index.Index; import com.google.gerrit.server.index.IndexDefinition; +import com.google.gerrit.server.index.OnlineUpgradeListener; import com.google.gerrit.server.index.Schema; +import com.google.gerrit.server.index.VersionManager; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; @@ -34,7 +35,7 @@ import org.slf4j.LoggerFactory; @Singleton -public class ElasticVersionManager extends AbstractVersionManager implements LifecycleListener { +public class ElasticVersionManager extends VersionManager { private static final Logger log = LoggerFactory.getLogger(ElasticVersionManager.class); private final String prefix; @@ -44,9 +45,10 @@ ElasticVersionManager( @GerritServerConfig Config cfg, SitePaths sitePaths, + DynamicSet<OnlineUpgradeListener> listeners, Collection<IndexDefinition<?, ?, ?>> defs, ElasticIndexVersionDiscovery versionDiscovery) { - super(cfg, sitePaths, defs); + super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg)); this.versionDiscovery = versionDiscovery; prefix = MoreObjects.firstNonNull(cfg.getString("index", null, "prefix"), "gerrit"); }
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml index bc028a6..9468b67 100644 --- a/gerrit-extension-api/pom.xml +++ b/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>2.14.1</version> + <version>2.15-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Extension API</name> <description>API for Gerrit Extensions</description>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java index 05fd5b2..1295ea0 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
@@ -21,5 +21,10 @@ return new ExportImpl(name); } + /** Create an annotation to export based on a cannonical class name. */ + public static Export named(Class<?> clazz) { + return named(clazz.getCanonicalName()); + } + private Exports() {} }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresAnyCapability.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresAnyCapability.java index f97abd9..1e3a2c8 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresAnyCapability.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresAnyCapability.java
@@ -33,4 +33,7 @@ /** Scope of the named capabilities. */ CapabilityScope scope() default CapabilityScope.CONTEXT; + + /** Fall back to admin credentials. Only applies to plugin capability check. */ + boolean fallBackToAdmin() default true; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java index 7717c84..b9ef7e0 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
@@ -32,4 +32,7 @@ /** Scope of the named capability. */ CapabilityScope scope() default CapabilityScope.CONTEXT; + + /** Fall back to admin credentials. Only applies to plugin capability check. */ + boolean fallBackToAdmin() default true; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/GlobalOrPluginPermission.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/GlobalOrPluginPermission.java new file mode 100644 index 0000000..deae084 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/GlobalOrPluginPermission.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.access; + +/** + * A {@link com.google.gerrit.server.permissions.GlobalPermission} or a {@link PluginPermission}. + */ +public interface GlobalOrPluginPermission { + /** @return name used in {@code project.config} permissions. */ + public String permissionName(); + + /** @return readable identifier of this permission for exception message. */ + public String describeForException(); +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PluginPermission.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PluginPermission.java new file mode 100644 index 0000000..7a467b8 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/access/PluginPermission.java
@@ -0,0 +1,77 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.access; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Objects; + +/** A global capability type permission used by a plugin. */ +public class PluginPermission implements GlobalOrPluginPermission { + private final String pluginName; + private final String capability; + private final boolean fallBackToAdmin; + + public PluginPermission(String pluginName, String capability) { + this(pluginName, capability, true); + } + + public PluginPermission(String pluginName, String capability, boolean fallBackToAdmin) { + this.pluginName = checkNotNull(pluginName, "pluginName"); + this.capability = checkNotNull(capability, "capability"); + this.fallBackToAdmin = fallBackToAdmin; + } + + public String pluginName() { + return pluginName; + } + + public String capability() { + return capability; + } + + public boolean fallBackToAdmin() { + return fallBackToAdmin; + } + + @Override + public String permissionName() { + return pluginName + '-' + capability; + } + + @Override + public String describeForException() { + return capability + " for plugin " + pluginName; + } + + @Override + public int hashCode() { + return Objects.hash(pluginName, capability); + } + + @Override + public boolean equals(Object other) { + if (other instanceof PluginPermission) { + PluginPermission b = (PluginPermission) other; + return pluginName.equals(b.pluginName) && capability.equals(b.capability); + } + return false; + } + + @Override + public String toString() { + return "PluginPermission[plugin=" + pluginName + ", capability=" + capability + ']'; + } +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AbandonInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AbandonInput.java index b3ba1e2..1d82178 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AbandonInput.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/AbandonInput.java
@@ -19,6 +19,6 @@ public class AbandonInput { @DefaultInput public String message; - public NotifyHandling notify = NotifyHandling.ALL; + public NotifyHandling notify; public Map<RecipientType, NotifyInfo> notifyDetails; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java index 8c1ebf3..aad10a9 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -14,6 +14,7 @@ package com.google.gerrit.extensions.api.changes; +import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.ChangeInfo; @@ -85,6 +86,34 @@ void move(MoveInput in) throws RestApiException; + void setPrivate(boolean value, @Nullable String message) throws RestApiException; + + void setWorkInProgress(String message) throws RestApiException; + + void setReadyForReview(String message) throws RestApiException; + + default void setWorkInProgress() throws RestApiException { + setWorkInProgress(null); + } + + default void setReadyForReview() throws RestApiException { + setReadyForReview(null); + } + + /** + * Ignore or un-ignore this change. + * + * @param ignore ignore the change if true + */ + void ignore(boolean ignore) throws RestApiException; + + /** + * Mute or un-mute this change. + * + * @param mute mute the change if true + */ + void mute(boolean mute) throws RestApiException; + /** * Create a new change that reverts this change. * @@ -129,9 +158,9 @@ IncludedInInfo includedIn() throws RestApiException; - void addReviewer(AddReviewerInput in) throws RestApiException; + AddReviewerResult addReviewer(AddReviewerInput in) throws RestApiException; - void addReviewer(String in) throws RestApiException; + AddReviewerResult addReviewer(String in) throws RestApiException; SuggestedReviewersRequest suggestReviewers() throws RestApiException; @@ -161,6 +190,9 @@ */ ChangeEditApi edit() throws RestApiException; + /** Create a new patch set with a new commit message. */ + void setMessage(String message) throws RestApiException; + /** Set hashtags on a change */ void setHashtags(HashtagsInput input) throws RestApiException; @@ -307,6 +339,21 @@ } @Override + public void setPrivate(boolean value, @Nullable String message) throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public void setWorkInProgress(String message) throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public void setReadyForReview(String message) throws RestApiException { + throw new NotImplementedException(); + } + + @Override public ChangeApi revert() throws RestApiException { throw new NotImplementedException(); } @@ -352,12 +399,12 @@ } @Override - public void addReviewer(AddReviewerInput in) throws RestApiException { + public AddReviewerResult addReviewer(AddReviewerInput in) throws RestApiException { throw new NotImplementedException(); } @Override - public void addReviewer(String in) throws RestApiException { + public AddReviewerResult addReviewer(String in) throws RestApiException { throw new NotImplementedException(); } @@ -387,6 +434,11 @@ } @Override + public void setMessage(String message) throws RestApiException { + throw new NotImplementedException(); + } + + @Override public EditInfo getEdit() throws RestApiException { throw new NotImplementedException(); } @@ -477,5 +529,15 @@ public ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException { throw new NotImplementedException(); } + + @Override + public void ignore(boolean ignore) throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public void mute(boolean mute) throws RestApiException { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java index d14ddfe..0708ef5 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/Changes.java
@@ -56,6 +56,15 @@ */ ChangeApi id(String project, String branch, String id) throws RestApiException; + /** + * Look up a change by project and numeric ID. + * + * @param project project name. + * @param id change number. + * @see #id(int) + */ + ChangeApi id(String project, int id) throws RestApiException; + ChangeApi create(ChangeInput in) throws RestApiException; QueryRequest query(); @@ -153,6 +162,11 @@ } @Override + public ChangeApi id(String project, int id) throws RestApiException { + throw new NotImplementedException(); + } + + @Override public ChangeApi create(ChangeInput in) throws RestApiException { throw new NotImplementedException(); }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java index 2e1bb13..75881d1 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CherryPickInput.java
@@ -14,8 +14,16 @@ package com.google.gerrit.extensions.api.changes; +import java.util.Map; + public class CherryPickInput { public String message; + // Cherry-pick destination branch, which will be the destination of the newly created change. public String destination; + // 40-hex digit SHA-1 of the commit which will be the parent commit of the newly created change. + public String base; public Integer parent; + + public NotifyHandling notify = NotifyHandling.NONE; + public Map<RecipientType, NotifyInfo> notifyDetails; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java index a6d64a6..889175e 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
@@ -22,6 +22,17 @@ CommentInfo get() throws RestApiException; /** + * Deletes a published comment of a revision. For NoteDb, it deletes the comment by rewriting the + * commit history. + * + * <p>Note instead of deleting the whole comment, this endpoint just replaces the comment's + * message. + * + * @return the comment with its message updated. + */ + CommentInfo delete(DeleteCommentInput input) throws RestApiException; + + /** * A default implementation which allows source compatibility when adding new methods to the * interface. */ @@ -30,5 +41,10 @@ public CommentInfo get() throws RestApiException { throw new NotImplementedException(); } + + @Override + public CommentInfo delete(DeleteCommentInput input) throws RestApiException { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteCommentInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteCommentInput.java new file mode 100644 index 0000000..75fd16b --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteCommentInput.java
@@ -0,0 +1,30 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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; + +import com.google.common.base.Strings; +import com.google.gerrit.extensions.restapi.DefaultInput; + +public class DeleteCommentInput { + @DefaultInput public String reason; + + public DeleteCommentInput() { + reason = ""; + } + + public DeleteCommentInput(String reason) { + this.reason = Strings.nullToEmpty(reason); + } +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteReviewerInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteReviewerInput.java index 34f550b..5be5f33 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteReviewerInput.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteReviewerInput.java
@@ -19,7 +19,7 @@ /** Input passed to {@code DELETE /changes/[id]/reviewers/[id]}. */ public class DeleteReviewerInput { /** Who to send email notifications to after the reviewer is deleted. */ - public NotifyHandling notify = NotifyHandling.ALL; + public NotifyHandling notify = null; public Map<RecipientType, NotifyInfo> notifyDetails; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java index 0eb076e..6b23419 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -54,7 +54,7 @@ public DraftHandling drafts; /** Who to send email notifications to after review is stored. */ - public NotifyHandling notify = NotifyHandling.ALL; + public NotifyHandling notify; public Map<RecipientType, NotifyInfo> notifyDetails;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java index af61481..3a33de9 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerInfo.java
@@ -25,6 +25,13 @@ */ @Nullable public Map<String, String> approvals; + public static ReviewerInfo byEmail(@Nullable String name, String email) { + ReviewerInfo info = new ReviewerInfo(); + info.name = name; + info.email = email; + return info; + } + public ReviewerInfo(Integer id) { super(id); } @@ -33,4 +40,6 @@ public String toString() { return username; } + + private ReviewerInfo() {} }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java index f5f6fbf..7a7444f 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/RevisionApi.java
@@ -18,6 +18,7 @@ import com.google.gerrit.extensions.common.ActionInfo; import com.google.gerrit.extensions.common.CommentInfo; import com.google.gerrit.extensions.common.CommitInfo; +import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.extensions.common.FileInfo; import com.google.gerrit.extensions.common.MergeableInfo; import com.google.gerrit.extensions.common.RobotCommentInfo; @@ -36,7 +37,7 @@ void description(String description) throws RestApiException; - void review(ReviewInput in) throws RestApiException; + ReviewResult review(ReviewInput in) throws RestApiException; void submit() throws RestApiException; @@ -70,6 +71,8 @@ FileApi file(String path); + CommitInfo commit(boolean addLinks) throws RestApiException; + MergeableInfo mergeable() throws RestApiException; MergeableInfo mergeableOtherBranches() throws RestApiException; @@ -86,6 +89,17 @@ List<RobotCommentInfo> robotCommentsAsList() throws RestApiException; + /** + * Applies the indicated fix by creating a new change edit or integrating the fix with the + * existing change edit. If no change edit exists before this call, the fix must refer to the + * current patch set. If a change edit exists, the fix must refer to the patch set on which the + * change edit is based. + * + * @param fixId the ID of the fix which should be applied + * @throws RestApiException if the fix couldn't be applied + */ + EditInfo applyFix(String fixId) throws RestApiException; + DraftApi createDraft(DraftInput in) throws RestApiException; DraftApi draft(String id) throws RestApiException; @@ -145,7 +159,7 @@ } @Override - public void review(ReviewInput in) throws RestApiException { + public ReviewResult review(ReviewInput in) throws RestApiException { throw new NotImplementedException(); } @@ -230,6 +244,11 @@ } @Override + public CommitInfo commit(boolean addLinks) throws RestApiException { + throw new NotImplementedException(); + } + + @Override public Map<String, List<CommentInfo>> comments() throws RestApiException { throw new NotImplementedException(); } @@ -255,6 +274,11 @@ } @Override + public EditInfo applyFix(String fixId) throws RestApiException { + throw new NotImplementedException(); + } + + @Override public Map<String, List<CommentInfo>> drafts() throws RestApiException { throw new NotImplementedException(); }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInfo.java new file mode 100644 index 0000000..fab2ec4 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInfo.java
@@ -0,0 +1,23 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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; + +public class AccessCheckInfo { + public String message; + // HTTP status code + public int status; + + // for future extension, we may add inputs / results for bulk checks. +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInput.java new file mode 100644 index 0000000..80a537c --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/AccessCheckInput.java
@@ -0,0 +1,32 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.Nullable; + +public class AccessCheckInput { + public String account; + public String project; + + @Nullable public String ref; + + public AccessCheckInput(String account, String project, @Nullable String ref) { + this.account = account; + this.project = project; + this.ref = ref; + } + + public AccessCheckInput() {} +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java new file mode 100644 index 0000000..e44eb28 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInfo.java
@@ -0,0 +1,73 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 java.util.List; +import java.util.Objects; + +public class ConsistencyCheckInfo { + public CheckAccountsResultInfo checkAccountsResult; + public CheckAccountExternalIdsResultInfo checkAccountExternalIdsResult; + + public static class CheckAccountsResultInfo { + public List<ConsistencyProblemInfo> problems; + + public CheckAccountsResultInfo(List<ConsistencyProblemInfo> problems) { + this.problems = problems; + } + } + + public static class CheckAccountExternalIdsResultInfo { + public List<ConsistencyProblemInfo> problems; + + public CheckAccountExternalIdsResultInfo(List<ConsistencyProblemInfo> problems) { + this.problems = problems; + } + } + + public static class ConsistencyProblemInfo { + public enum Status { + ERROR, + WARNING, + } + + public final Status status; + public final String message; + + public ConsistencyProblemInfo(Status status, String message) { + this.status = status; + this.message = message; + } + + @Override + public boolean equals(Object o) { + if (o instanceof ConsistencyProblemInfo) { + ConsistencyProblemInfo other = ((ConsistencyProblemInfo) o); + return Objects.equals(status, other.status) && Objects.equals(message, other.message); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(status, message); + } + + @Override + public String toString() { + return status.name() + ": " + message; + } + } +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInput.java new file mode 100644 index 0000000..f3d927e --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/ConsistencyCheckInput.java
@@ -0,0 +1,24 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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; + +public class ConsistencyCheckInput { + public CheckAccountsInput checkAccounts; + public CheckAccountExternalIdsInput checkAccountExternalIds; + + public static class CheckAccountsInput {} + + public static class CheckAccountExternalIdsInput {} +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java index 07b3ab2..de59cee 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/config/Server.java
@@ -34,6 +34,10 @@ DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in) throws RestApiException; + ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException; + + AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException; + /** * A default implementation which allows source compatibility when adding new methods to the * interface. @@ -70,5 +74,15 @@ throws RestApiException { throw new NotImplementedException(); } + + @Override + public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java index 995f41a..a1f7327 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/BranchApi.java
@@ -17,6 +17,7 @@ import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.NotImplementedException; import com.google.gerrit.extensions.restapi.RestApiException; +import java.util.List; public interface BranchApi { BranchApi create(BranchInput in) throws RestApiException; @@ -28,6 +29,8 @@ /** Returns the content of a file from the HEAD revision. */ BinaryResult file(String path) throws RestApiException; + List<ReflogEntryInfo> reflog() throws RestApiException; + /** * A default implementation which allows source compatibility when adding new methods to the * interface. @@ -52,5 +55,10 @@ public BinaryResult file(String path) throws RestApiException { throw new NotImplementedException(); } + + @Override + public List<ReflogEntryInfo> reflog() throws RestApiException { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommitApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommitApi.java new file mode 100644 index 0000000..6084962 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/CommitApi.java
@@ -0,0 +1,33 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.projects; + +import com.google.gerrit.extensions.api.changes.ChangeApi; +import com.google.gerrit.extensions.api.changes.CherryPickInput; +import com.google.gerrit.extensions.restapi.NotImplementedException; +import com.google.gerrit.extensions.restapi.RestApiException; + +public interface CommitApi { + + ChangeApi cherryPick(CherryPickInput input) throws RestApiException; + + /** A default implementation for source compatibility when adding new methods to the interface. */ + class NotImplemented implements CommitApi { + @Override + public ChangeApi cherryPick(CherryPickInput input) throws RestApiException { + throw new NotImplementedException(); + } + } +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java index cc91a4a..23c1f8e 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInfo.java
@@ -31,6 +31,8 @@ public InheritedBooleanInfo enableSignedPush; public InheritedBooleanInfo requireSignedPush; public InheritedBooleanInfo rejectImplicitMerges; + public InheritedBooleanInfo enableReviewerByEmail; + public InheritedBooleanInfo matchAuthorToCommitterDate; public MaxObjectSizeLimitInfo maxObjectSizeLimit; public SubmitType submitType; public ProjectState state; @@ -40,6 +42,8 @@ public Map<String, CommentLinkInfo> commentlinks; public ThemeInfo theme; + public Map<String, List<String>> extensionPanelNames; + public static class InheritedBooleanInfo { public Boolean value; public InheritableBoolean configuredValue;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java index ae81ea5..65e056b 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ConfigInput.java
@@ -29,6 +29,8 @@ public InheritableBoolean enableSignedPush; public InheritableBoolean requireSignedPush; public InheritableBoolean rejectImplicitMerges; + public InheritableBoolean enableReviewerByEmail; + public InheritableBoolean matchAuthorToCommitterDate; public String maxObjectSizeLimit; public SubmitType submitType; public ProjectState state;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java index d53e096..1401e06 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ProjectApi.java
@@ -125,6 +125,14 @@ TagApi tag(String ref) throws RestApiException; /** + * Lookup a commit by its {@code ObjectId} string. + * + * @param commit the {@code ObjectId} string. + * @return API for accessing the commit. + */ + CommitApi commit(String commit) throws RestApiException; + + /** * A default implementation which allows source compatibility when adding new methods to the * interface. */ @@ -218,5 +226,10 @@ public void deleteTags(DeleteTagsInput in) throws RestApiException { throw new NotImplementedException(); } + + @Override + public CommitApi commit(String commit) throws RestApiException { + throw new NotImplementedException(); + } } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ReflogEntryInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ReflogEntryInfo.java new file mode 100644 index 0000000..a0984ec --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/projects/ReflogEntryInfo.java
@@ -0,0 +1,31 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.projects; + +import com.google.gerrit.extensions.common.GitPerson; + +public class ReflogEntryInfo { + public String oldId; + public String newId; + public GitPerson who; + public String comment; + + public ReflogEntryInfo(String oldId, String newId, GitPerson who, String comment) { + this.oldId = oldId; + this.newId = newId; + this.who = who; + this.comment = comment; + } +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java index 2225a99..3307997 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Comment.java
@@ -15,6 +15,7 @@ package com.google.gerrit.extensions.client; import java.sql.Timestamp; +import java.util.Comparator; import java.util.Objects; public abstract class Comment { @@ -36,7 +37,13 @@ public String message; public Boolean unresolved; - public static class Range { + public static class Range implements Comparable<Range> { + private static final Comparator<Range> RANGE_COMPARATOR = + Comparator.<Range>comparingInt(range -> range.startLine) + .thenComparingInt(range -> range.startCharacter) + .thenComparingInt(range -> range.endLine) + .thenComparingInt(range -> range.endCharacter); + public int startLine; // 1-based, inclusive public int startCharacter; // 0-based, inclusive public int endLine; // 1-based, exclusive @@ -81,6 +88,11 @@ + endCharacter + '}'; } + + @Override + public int compareTo(Range otherRange) { + return RANGE_COMPARATOR.compare(this, otherRange); + } } public short side() {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java index 7192ff9..9dcba5e 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/GeneralPreferencesInfo.java
@@ -157,6 +157,7 @@ public EmailStrategy emailStrategy; public EmailFormat emailFormat; public DefaultBase defaultBaseForMerges; + public Boolean publishCommentsOnPush; public boolean isShowInfoInReviewCategory() { return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE; @@ -225,6 +226,7 @@ p.muteCommonPathPrefixes = true; p.signedOffBy = false; p.defaultBaseForMerges = DefaultBase.FIRST_PARENT; + p.publishCommentsOnPush = false; return p; } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java index 2fb32d7..f20509b 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AccountInfo.java
@@ -15,6 +15,7 @@ package com.google.gerrit.extensions.common; import java.util.List; +import java.util.Objects; public class AccountInfo { public Integer _accountId; @@ -29,4 +30,34 @@ public AccountInfo(Integer id) { this._accountId = id; } + + /** To be used ONLY in connection with unregistered reviewers and CCs. */ + public AccountInfo(String name, String email) { + this.name = name; + this.email = email; + } + + @Override + public boolean equals(Object o) { + if (o instanceof AccountInfo) { + AccountInfo accountInfo = (AccountInfo) o; + return Objects.equals(_accountId, accountInfo._accountId) + && Objects.equals(name, accountInfo.name) + && Objects.equals(email, accountInfo.email) + && Objects.equals(secondaryEmails, accountInfo.secondaryEmails) + && Objects.equals(username, accountInfo.username) + && Objects.equals(avatars, accountInfo.avatars) + && Objects.equals(_moreAccounts, accountInfo._moreAccounts) + && Objects.equals(status, accountInfo.status); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash( + _accountId, name, email, secondaryEmails, username, avatars, _moreAccounts, status); + } + + protected AccountInfo() {} }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java index 3803714..f6d9f4c 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -36,6 +36,7 @@ public Timestamp updated; public Timestamp submitted; public Boolean starred; + public Boolean muted; public Collection<String> stars; public Boolean reviewed; public SubmitType submitType; @@ -44,6 +45,9 @@ public Integer insertions; public Integer deletions; public Integer unresolvedCommentCount; + public Boolean isPrivate; + public Boolean workInProgress; + public Boolean hasReviewStarted; public int _number; @@ -54,6 +58,7 @@ public Map<String, Collection<String>> permittedLabels; public Collection<AccountInfo> removableReviewers; public Map<ReviewerState, Collection<AccountInfo>> reviewers; + public Map<ReviewerState, Collection<AccountInfo>> pendingReviewers; public Collection<ReviewerUpdateInfo> reviewerUpdates; public Collection<ChangeMessageInfo> messages; @@ -62,4 +67,5 @@ public Boolean _moreChanges; public List<ProblemInfo> problems; + public List<PluginDefinedInfo> plugins; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java index b50bcf3..c8e7bca 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeInput.java
@@ -27,10 +27,28 @@ public String topic; public ChangeStatus status; + public Boolean isPrivate; + public Boolean workInProgress; public String baseChange; public Boolean newBranch; public MergeInput merge; + public ChangeInput() {} + + /** + * Creates a new {@code ChangeInput} with the minimal attributes required for a successful + * creation of a new change. + * + * @param project the project name for the new change + * @param branch the branch name for the new change + * @param subject the subject (commit message) for the new change + */ + public ChangeInput(String project, String branch, String subject) { + this.project = project; + this.branch = branch; + this.subject = subject; + } + /** Who to send email notifications to after change is created. */ public NotifyHandling notify = NotifyHandling.ALL;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java index e79918f..735b84f 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
@@ -20,6 +20,7 @@ public String id; public String tag; public AccountInfo author; + public AccountInfo realAuthor; public Timestamp date; public String message; public Integer _revisionNumber;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java index 3df4b86..2511e96 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DiffInfo.java
@@ -69,6 +69,10 @@ public List<List<Integer>> editA; public List<List<Integer>> editB; + // Indicates that this entry only exists because of a rebase (and not because of a real change + // between 'a' and 'b'). + public Boolean dueToRebase; + // a and b are actually common with this whitespace ignore setting. public Boolean common;
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupInfo.java index 55fb92a..b21475c 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GroupInfo.java
@@ -14,6 +14,7 @@ package com.google.gerrit.extensions.common; +import java.sql.Timestamp; import java.util.List; public class GroupInfo extends GroupBaseInfo { @@ -25,6 +26,7 @@ public Integer groupId; public String owner; public String ownerId; + public Timestamp createdOn; public Boolean _moreGroups; // These fields are only supplied for internal groups, and only if requested.
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java index 2d1d840..13fc9ec 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.java
@@ -19,4 +19,5 @@ public class PluginConfigInfo { public Boolean hasAvatars; public List<String> jsResourcePaths; + public List<String> htmlResourcePaths; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java new file mode 100644 index 0000000..e6fef0f --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginDefinedInfo.java
@@ -0,0 +1,19 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 PluginDefinedInfo { + public String name; +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/FactoryModule.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/FactoryModule.java index 793a372..1630ff8 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/FactoryModule.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/config/FactoryModule.java
@@ -39,7 +39,7 @@ * * @param factory interface which specifies the bean factory method. */ - protected void factory(final Class<?> factory) { + protected void factory(Class<?> factory) { install(new FactoryModuleBuilder().build(factory)); } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java index 926818e..5cdf267 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -186,7 +186,7 @@ * @param item item to check whether or not it is contained. * @return {@code true} if this set contains the given item. */ - public boolean contains(final T item) { + public boolean contains(T item) { Iterator<T> iterator = iterator(); while (iterator.hasNext()) { T candidate = iterator.next(); @@ -203,7 +203,7 @@ * @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. */ - public RegistrationHandle add(final T item) { + public RegistrationHandle add(T item) { return add(Providers.of(item)); } @@ -213,7 +213,7 @@ * @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. */ - public RegistrationHandle add(final Provider<T> item) { + public RegistrationHandle add(Provider<T> item) { final AtomicReference<Provider<T>> ref = new AtomicReference<>(item); items.add(ref); return new RegistrationHandle() {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java index 5057529..50aed7d 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -30,7 +30,7 @@ * @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. */ - public RegistrationHandle put(String pluginName, String exportName, final Provider<T> item) { + public RegistrationHandle put(String pluginName, String exportName, Provider<T> item) { final NamePair key = new NamePair(pluginName, exportName); items.put(key, item); return new RegistrationHandle() {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AuthException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AuthException.java index 1d4cda7..0b4f459 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AuthException.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AuthException.java
@@ -22,4 +22,12 @@ public AuthException(String msg) { super(msg); } + + /** + * @param msg message to return to the client. + * @param cause cause of this exception. + */ + public AuthException(String msg, Throwable cause) { + super(msg, cause); + } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java index 58be322..736c3ba 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java
@@ -60,8 +60,6 @@ public boolean equals(Object other) { if (other instanceof IdString) { return urlEncoded.equals(((IdString) other).urlEncoded); - } else if (other instanceof String) { - return urlEncoded.equals(other); } return false; }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java index 8e503ee..ffedcfb 100644 --- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java +++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -15,7 +15,7 @@ package com.google.gerrit.gpg; import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString; -import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY; import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableMap; @@ -24,7 +24,7 @@ import com.google.gerrit.common.PageLinks; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountState; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.query.account.InternalAccountQuery; @@ -176,7 +176,6 @@ private boolean hasAllowedUserId(PGPPublicKey key, Set<String> allowedUserIds) throws PGPException { - @SuppressWarnings("unchecked") Iterator<String> userIds = key.getUserIDs(); while (userIds.hasNext()) { String userId = userIds.next();
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java index 62d0df7..c3dec61 100644 --- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java +++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
@@ -17,8 +17,8 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.io.IOException; import org.eclipse.jgit.lib.Repository; @@ -30,7 +30,7 @@ private final GitRepositoryManager repoManager; private final AllUsersName allUsers; - @AssistedInject + @Inject GerritPushCertificateChecker( GerritPublicKeyChecker.Factory keyCheckerFactory, GitRepositoryManager repoManager,
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java index c0ab26c..70e9a24 100644 --- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java +++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
@@ -381,7 +381,6 @@ } List<CheckResult> signerResults = new ArrayList<>(); - @SuppressWarnings("unchecked") Iterator<String> userIds = key.getUserIDs(); while (userIds.hasNext()) { String userId = userIds.next();
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java index 144606a..8ab5fbd 100644 --- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java +++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -398,7 +398,6 @@ } public static String keyToString(PGPPublicKey key) { - @SuppressWarnings("unchecked") Iterator<String> it = key.getUserIDs(); return String.format( "%s %s(%s)",
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java index 9aa18fe..14a4c6d 100644 --- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java +++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgKeyApiImpl.java
@@ -21,8 +21,8 @@ import com.google.gerrit.gpg.server.GpgKey; import com.google.gerrit.gpg.server.GpgKeys; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.io.IOException; import org.bouncycastle.openpgp.PGPException; import org.eclipse.jgit.errors.ConfigInvalidException; @@ -36,7 +36,7 @@ private final DeleteGpgKey delete; private final GpgKey rsrc; - @AssistedInject + @Inject GpgKeyApiImpl(GpgKeys.Get get, DeleteGpgKey delete, @Assisted GpgKey rsrc) { this.get = get; this.delete = delete;
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java index f95cee2..779d5d4 100644 --- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java +++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -15,7 +15,7 @@ package com.google.gerrit.gpg.server; import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString; -import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY; import com.google.common.io.BaseEncoding; import com.google.gerrit.extensions.restapi.ResourceConflictException; @@ -23,10 +23,9 @@ import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.gpg.PublicKeyStore; import com.google.gerrit.gpg.server.DeleteGpgKey.Input; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.GerritPersonIdent; -import com.google.gerrit.server.account.ExternalId; -import com.google.gerrit.server.account.ExternalIdsUpdate; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -42,18 +41,15 @@ public static class Input {} private final Provider<PersonIdent> serverIdent; - private final Provider<ReviewDb> db; private final Provider<PublicKeyStore> storeProvider; private final ExternalIdsUpdate.User externalIdsUpdateFactory; @Inject DeleteGpgKey( @GerritPersonIdent Provider<PersonIdent> serverIdent, - Provider<ReviewDb> db, Provider<PublicKeyStore> storeProvider, ExternalIdsUpdate.User externalIdsUpdateFactory) { this.serverIdent = serverIdent; - this.db = db; this.storeProvider = storeProvider; this.externalIdsUpdateFactory = externalIdsUpdateFactory; } @@ -66,7 +62,6 @@ externalIdsUpdateFactory .create() .delete( - db.get(), rsrc.getUser().getAccountId(), ExternalId.Key.create( SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint())));
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java index 819ad96..303499e 100644 --- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java +++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -14,12 +14,10 @@ package com.google.gerrit.gpg.server; -import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY; import static java.nio.charset.StandardCharsets.UTF_8; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CharMatcher; -import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.io.BaseEncoding; import com.google.gerrit.extensions.common.GpgKeyInfo; @@ -36,11 +34,10 @@ import com.google.gerrit.gpg.GerritPublicKeyChecker; import com.google.gerrit.gpg.PublicKeyChecker; import com.google.gerrit.gpg.PublicKeyStore; -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.AccountResource; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -66,23 +63,23 @@ public static final String MIME_TYPE = "application/pgp-keys"; private final DynamicMap<RestView<GpgKey>> views; - private final Provider<ReviewDb> db; private final Provider<CurrentUser> self; private final Provider<PublicKeyStore> storeProvider; private final GerritPublicKeyChecker.Factory checkerFactory; + private final ExternalIds externalIds; @Inject GpgKeys( DynamicMap<RestView<GpgKey>> views, - Provider<ReviewDb> db, Provider<CurrentUser> self, Provider<PublicKeyStore> storeProvider, - GerritPublicKeyChecker.Factory checkerFactory) { + GerritPublicKeyChecker.Factory checkerFactory, + ExternalIds externalIds) { this.views = views; - this.db = db; this.self = self; this.storeProvider = storeProvider; this.checkerFactory = checkerFactory; + this.externalIds = externalIds; } @Override @@ -198,16 +195,8 @@ } } - @VisibleForTesting - public static FluentIterable<ExternalId> getGpgExtIds(ReviewDb db, Account.Id accountId) - throws OrmException { - return FluentIterable.from( - ExternalId.from(db.accountExternalIds().byAccount(accountId).toList())) - .filter(in -> in.isScheme(SCHEME_GPGKEY)); - } - - private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws OrmException { - return getGpgExtIds(db.get(), rsrc.getUser().getAccountId()); + private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws IOException { + return externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY); } private static long keyId(byte[] fp) { @@ -230,7 +219,6 @@ if (key != null) { info.id = PublicKeyStore.keyIdToString(key.getKeyID()); info.fingerprint = Fingerprint.toString(key.getFingerprint()); - @SuppressWarnings("unchecked") Iterator<String> userIds = key.getUserIDs(); info.userIds = ImmutableList.copyOf(userIds);
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java index 165402c..41fcd04 100644 --- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java +++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -16,7 +16,7 @@ import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString; import static com.google.gerrit.gpg.PublicKeyStore.keyToString; -import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.toList; @@ -40,14 +40,14 @@ import com.google.gerrit.gpg.PublicKeyStore; import com.google.gerrit.gpg.server.PostGpgKeys.Input; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountResource; import com.google.gerrit.server.account.AccountState; -import com.google.gerrit.server.account.ExternalId; -import com.google.gerrit.server.account.ExternalIdsUpdate; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; import com.google.gerrit.server.mail.send.AddKeySender; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gwtorm.server.OrmException; @@ -83,31 +83,31 @@ private final Logger log = LoggerFactory.getLogger(getClass()); private final Provider<PersonIdent> serverIdent; - private final Provider<ReviewDb> db; private final Provider<CurrentUser> self; private final Provider<PublicKeyStore> storeProvider; private final GerritPublicKeyChecker.Factory checkerFactory; private final AddKeySender.Factory addKeyFactory; private final Provider<InternalAccountQuery> accountQueryProvider; + private final ExternalIds externalIds; private final ExternalIdsUpdate.User externalIdsUpdateFactory; @Inject PostGpgKeys( @GerritPersonIdent Provider<PersonIdent> serverIdent, - Provider<ReviewDb> db, Provider<CurrentUser> self, Provider<PublicKeyStore> storeProvider, GerritPublicKeyChecker.Factory checkerFactory, AddKeySender.Factory addKeyFactory, Provider<InternalAccountQuery> accountQueryProvider, + ExternalIds externalIds, ExternalIdsUpdate.User externalIdsUpdateFactory) { this.serverIdent = serverIdent; - this.db = db; this.self = self; this.storeProvider = storeProvider; this.checkerFactory = checkerFactory; this.addKeyFactory = addKeyFactory; this.accountQueryProvider = accountQueryProvider; + this.externalIds = externalIds; this.externalIdsUpdateFactory = externalIdsUpdateFactory; } @@ -118,7 +118,7 @@ GpgKeys.checkVisible(self, rsrc); Collection<ExternalId> existingExtIds = - GpgKeys.getGpgExtIds(db.get(), rsrc.getUser().getAccountId()).toList(); + externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY); try (PublicKeyStore store = storeProvider.get()) { Set<Fingerprint> toRemove = readKeysToRemove(input, existingExtIds); List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, toRemove); @@ -143,7 +143,7 @@ toRemove.stream().map(fp -> toExtIdKey(fp.get())).collect(toList()); externalIdsUpdateFactory .create() - .replace(db.get(), rsrc.getUser().getAccountId(), extIdKeysToRemove, newExtIds); + .replace(rsrc.getUser().getAccountId(), extIdKeysToRemove, newExtIds); return toJson(newKeys, toRemove, store, rsrc.getUser()); } }
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java index d1bbc5e..669b610 100644 --- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java +++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -36,11 +36,11 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountManager; +import com.google.gerrit.server.account.AccountsUpdate; import com.google.gerrit.server.account.AuthRequest; -import com.google.gerrit.server.account.ExternalId; -import com.google.gerrit.server.account.ExternalIdsUpdate; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; import com.google.gerrit.server.schema.SchemaCreator; import com.google.gerrit.server.util.RequestContext; import com.google.gerrit.server.util.ThreadLocalRequestContext; @@ -52,7 +52,6 @@ import com.google.inject.Injector; import com.google.inject.Provider; import com.google.inject.util.Providers; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -71,7 +70,7 @@ /** Unit tests for {@link GerritPublicKeyChecker}. */ public class GerritPublicKeyCheckerTest { - @Inject private AccountCache accountCache; + @Inject private AccountsUpdate.Server accountsUpdate; @Inject private AccountManager accountManager; @@ -115,10 +114,8 @@ db = schemaFactory.open(); schemaCreator.create(db); userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId(); - Account userAccount = db.accounts().get(userId); // Note: does not match any key in TestKeys. - userAccount.setPreferredEmail("user@example.com"); - db.accounts().update(ImmutableList.of(userAccount)); + accountsUpdate.create().update(db, userId, a -> a.setPreferredEmail("user@example.com")); user = reloadUser(); requestContext.setContext( @@ -150,8 +147,7 @@ return userFactory.create(id); } - private IdentifiedUser reloadUser() throws IOException { - accountCache.evict(userId); + private IdentifiedUser reloadUser() { user = userFactory.create(userId); return user; } @@ -223,7 +219,7 @@ @Test public void noExternalIds() throws Exception { ExternalIdsUpdate externalIdsUpdate = externalIdsUpdateFactory.create(); - externalIdsUpdate.deleteAll(db, user.getAccountId()); + externalIdsUpdate.deleteAll(user.getAccountId()); reloadUser(); TestKey key = validKeyWithSecondUserId(); @@ -237,7 +233,7 @@ assertProblems( checker.check(key.getPublicKey()), Status.BAD, "Key is not associated with any users"); externalIdsUpdate.insert( - db, ExternalId.create(toExtIdKey(key.getPublicKey()), user.getAccountId())); + ExternalId.create(toExtIdKey(key.getPublicKey()), user.getAccountId())); reloadUser(); assertProblems(checker.check(key.getPublicKey()), Status.BAD, "No identities found for user"); } @@ -373,7 +369,7 @@ PGPPublicKeyRing keyRingB = keyB().getPublicKeyRing(); PGPPublicKey keyB = keyRingB.getPublicKey(); - keyB = PGPPublicKey.removeCertification(keyB, (String) keyB.getUserIDs().next()); + keyB = PGPPublicKey.removeCertification(keyB, keyB.getUserIDs().next()); keyRingB = PGPPublicKeyRing.insertPublicKey(keyRingB, keyB); add(keyRingB, addUser("userB")); @@ -391,8 +387,7 @@ List<ExternalId> newExtIds = new ArrayList<>(2); newExtIds.add(ExternalId.create(toExtIdKey(kr.getPublicKey()), id)); - @SuppressWarnings("unchecked") - String userId = (String) Iterators.getOnlyElement(kr.getPublicKey().getUserIDs(), null); + String userId = Iterators.getOnlyElement(kr.getPublicKey().getUserIDs(), null); if (userId != null) { String email = PushCertificateIdent.parse(userId).getEmailAddress(); assertThat(email).contains("@"); @@ -406,7 +401,7 @@ cb.setCommitter(ident); assertThat(store.save(cb)).isAnyOf(NEW, FAST_FORWARD, FORCED); - externalIdsUpdateFactory.create().insert(db, newExtIds); + externalIdsUpdateFactory.create().insert(newExtIds); } private TestKey add(TestKey k, IdentifiedUser user) throws Exception { @@ -431,7 +426,7 @@ private void addExternalId(String scheme, String id, String email) throws Exception { externalIdsUpdateFactory .create() - .insert(db, ExternalId.createWithEmail(scheme, id, user.getAccountId(), email)); + .insert(ExternalId.createWithEmail(scheme, id, user.getAccountId(), email)); reloadUser(); } }
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java index c9c0b18..94eff06 100644 --- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java +++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
@@ -231,7 +231,6 @@ private void assertUserIds(PGPPublicKeyRing keyRing, String... expected) throws Exception { List<String> actual = new ArrayList<>(); - @SuppressWarnings("unchecked") Iterator<String> userIds = store.get(keyRing.getPublicKey().getKeyID()).iterator().next().getPublicKey().getUserIDs(); while (userIds.hasNext()) {
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKey.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKey.java index 420dedf..b2ef65d 100644 --- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKey.java +++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKey.java
@@ -77,7 +77,7 @@ } public String getFirstUserId() { - return (String) getPublicKey().getUserIDs().next(); + return getPublicKey().getUserIDs().next(); } public PGPPrivateKey getPrivateKey() throws PGPException {
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java index 2d0f833..7b70c6a 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
@@ -62,7 +62,7 @@ return flashEnabled; } - public static void setFlashEnabled(final boolean on) { + public static void setFlashEnabled(boolean on) { flashEnabled = on; } @@ -87,7 +87,7 @@ * * @param str initial content */ - public CopyableLabel(final String str) { + public CopyableLabel(String str) { this(str, true); } @@ -98,7 +98,7 @@ * @param showLabel if true, the content is shown, if false it is hidden from view and only the * copy icon is displayed. */ - public CopyableLabel(final String str, final boolean showLabel) { + public CopyableLabel(String str, boolean showLabel) { content = new FlowPanel(); initWidget(content); @@ -111,7 +111,7 @@ textLabel.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { showTextBox(); } }); @@ -160,7 +160,7 @@ * @param text the new preview text, should be shorter than the original text which would be * copied to the clipboard. */ - public void setPreviewText(final String text) { + public void setPreviewText(String text) { if (textLabel != null) { textLabel.setText(text); } @@ -206,7 +206,7 @@ } @Override - public void setText(final String newText) { + public void setText(String newText) { text = newText; visibleLen = newText.length(); @@ -229,7 +229,7 @@ textBox.addKeyPressHandler( new KeyPressHandler() { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { if (event.isControlKeyDown() || event.isMetaKeyDown()) { switch (event.getCharCode()) { case 'c': @@ -237,7 +237,7 @@ textBox.addKeyUpHandler( new KeyUpHandler() { @Override - public void onKeyUp(final KeyUpEvent event) { + public void onKeyUp(KeyUpEvent event) { Scheduler.get() .scheduleDeferred( new Command() { @@ -256,7 +256,7 @@ textBox.addBlurHandler( new BlurHandler() { @Override - public void onBlur(final BlurEvent event) { + public void onBlur(BlurEvent event) { hideTextBox(); } });
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java index 1066dd4..6ef5d7b 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/css/rebind/CssLinker.java
@@ -38,19 +38,18 @@ } @Override - public ArtifactSet link( - final TreeLogger logger, final LinkerContext context, final ArtifactSet artifacts) + public ArtifactSet link(final TreeLogger logger, LinkerContext context, ArtifactSet artifacts) throws UnableToCompleteException { final ArtifactSet returnTo = new ArtifactSet(); int index = 0; final HashMap<String, PublicResource> css = new HashMap<>(); - for (final StandardStylesheetReference ssr : + for (StandardStylesheetReference ssr : artifacts.<StandardStylesheetReference>find(StandardStylesheetReference.class)) { css.put(ssr.getSrc(), null); } - for (final PublicResource pr : artifacts.<PublicResource>find(PublicResource.class)) { + for (PublicResource pr : artifacts.<PublicResource>find(PublicResource.class)) { if (css.containsKey(pr.getPartialPath())) { css.put(pr.getPartialPath(), new CssPubRsrc(name(logger, pr), pr)); } @@ -74,8 +73,7 @@ return returnTo; } - private String name(final TreeLogger logger, final PublicResource r) - throws UnableToCompleteException { + private String name(TreeLogger logger, PublicResource r) throws UnableToCompleteException { byte[] out; try (ByteArrayOutputStream tmp = new ByteArrayOutputStream(); InputStream in = r.getContents(logger)) { @@ -105,13 +103,13 @@ private static final long serialVersionUID = 1L; private final PublicResource src; - CssPubRsrc(final String partialPath, final PublicResource r) { + CssPubRsrc(String partialPath, PublicResource r) { super(StandardLinkerContext.class, partialPath); src = r; } @Override - public InputStream getContents(final TreeLogger logger) throws UnableToCompleteException { + public InputStream getContents(TreeLogger logger) throws UnableToCompleteException { return src.getContents(logger); }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java index 304d56e..5a4f6aa 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/CompoundKeyCommand.java
@@ -34,7 +34,7 @@ } @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { GlobalKey.temporaryWithTimeout(set); } }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java index 3961313..3eac789 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/GlobalKey.java
@@ -30,7 +30,7 @@ public static final KeyPressHandler STOP_PROPAGATION = new KeyPressHandler() { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { event.stopPropagation(); } }; @@ -50,7 +50,7 @@ .addKeyPressHandler( new KeyPressHandler() { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { final KeyCommandSet s = active.live; if (s != active.all) { active.live = active.all; @@ -78,19 +78,19 @@ restoreGlobal = new CloseHandler<PopupPanel>() { @Override - public void onClose(final CloseEvent<PopupPanel> event) { + public void onClose(CloseEvent<PopupPanel> event) { active = global; } }; } } - static void temporaryWithTimeout(final KeyCommandSet s) { + static void temporaryWithTimeout(KeyCommandSet s) { active.live = s; restoreTimer.schedule(250); } - public static void dialog(final PopupPanel panel) { + public static void dialog(PopupPanel panel) { initEvents(); initDialog(); assert panel.isShowing(); @@ -110,7 +110,7 @@ KeyDownEvent.getType()); } - public static HandlerRegistration addApplication(final Widget widget, final KeyCommand appKey) { + public static HandlerRegistration addApplication(Widget widget, KeyCommand appKey) { initEvents(); final State state = stateFor(widget); state.add(appKey); @@ -122,7 +122,7 @@ }; } - public static HandlerRegistration add(final Widget widget, final KeyCommandSet cmdSet) { + public static HandlerRegistration add(Widget widget, KeyCommandSet cmdSet) { initEvents(); final State state = stateFor(widget); state.add(cmdSet); @@ -144,7 +144,7 @@ return global; } - public static void filter(final KeyCommandFilter filter) { + public static void filter(KeyCommandFilter filter) { active.filter(filter); if (active != global) { global.filter(filter); @@ -159,7 +159,7 @@ final KeyCommandSet all; KeyCommandSet live; - State(final Widget r) { + State(Widget r) { root = r; app = new KeyCommandSet(KeyConstants.I.applicationSection()); @@ -171,25 +171,25 @@ live = all; } - void add(final KeyCommand k) { + void add(KeyCommand k) { app.add(k); all.add(k); } - void remove(final KeyCommand k) { + void remove(KeyCommand k) { app.remove(k); all.remove(k); } - void add(final KeyCommandSet s) { + void add(KeyCommandSet s) { all.add(s); } - void remove(final KeyCommandSet s) { + void remove(KeyCommandSet s) { all.remove(s); } - void filter(final KeyCommandFilter f) { + void filter(KeyCommandFilter f) { all.filter(f); } }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java index 0274b9d..8222f8b 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/HidePopupPanelCommand.java
@@ -27,7 +27,7 @@ } @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { panel.hide(); } }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java index 2e9b652..f1c92e0 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommand.java
@@ -25,7 +25,7 @@ public static final int M_META = 4 << 16; public static final int M_SHIFT = 8 << 16; - public static boolean same(final KeyCommand a, final KeyCommand b) { + public static boolean same(KeyCommand a, KeyCommand b) { return a.getClass() == b.getClass() && a.helpText.equals(b.helpText) && a.sibling == b.sibling; } @@ -33,11 +33,11 @@ private final String helpText; KeyCommand sibling; - public KeyCommand(final int mask, final int key, final String help) { + public KeyCommand(int mask, int key, String help) { this(mask, (char) key, help); } - public KeyCommand(final int mask, final char key, final String help) { + public KeyCommand(int mask, char key, String help) { assert help != null; keyMask = mask | key; helpText = help; @@ -88,12 +88,12 @@ return b; } - private void modifier(final SafeHtmlBuilder b, final String name) { + private void modifier(SafeHtmlBuilder b, String name) { namedKey(b, name); b.append(" + "); } - private void namedKey(final SafeHtmlBuilder b, final String name) { + private void namedKey(SafeHtmlBuilder b, String name) { b.append('<'); b.openSpan(); b.setStyleName(KeyResources.I.css().helpKey());
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java index 734dd4e..90aa419 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyCommandSet.java
@@ -33,7 +33,7 @@ this(""); } - public KeyCommandSet(final String setName) { + public KeyCommandSet(String setName) { map = new HashMap<>(); name = setName; } @@ -42,7 +42,7 @@ return name; } - public void setName(final String setName) { + public void setName(String setName) { assert setName != null; name = setName; } @@ -62,7 +62,7 @@ b.sibling = a; } - public void add(final KeyCommand k) { + public void add(KeyCommand k) { assert !map.containsKey(k.keyMask) : "Key " + k.describeKeyStroke().asString() + " already registered"; if (!map.containsKey(k.keyMask)) { @@ -70,38 +70,38 @@ } } - public void remove(final KeyCommand k) { + public void remove(KeyCommand k) { assert map.get(k.keyMask) == k; map.remove(k.keyMask); } - public void add(final KeyCommandSet set) { + public void add(KeyCommandSet set) { if (sets == null) { sets = new ArrayList<>(); } assert !sets.contains(set); sets.add(set); - for (final KeyCommand k : set.map.values()) { + for (KeyCommand k : set.map.values()) { add(k); } } - public void remove(final KeyCommandSet set) { + public void remove(KeyCommandSet set) { assert sets != null; assert sets.contains(set); sets.remove(set); - for (final KeyCommand k : set.map.values()) { + for (KeyCommand k : set.map.values()) { remove(k); } } - public void filter(final KeyCommandFilter filter) { + public void filter(KeyCommandFilter filter) { if (sets != null) { - for (final KeyCommandSet s : sets) { + for (KeyCommandSet s : sets) { s.filter(filter); } } - for (final Iterator<KeyCommand> i = map.values().iterator(); i.hasNext(); ) { + for (Iterator<KeyCommand> i = map.values().iterator(); i.hasNext(); ) { final KeyCommand kc = i.next(); if (!filter.include(kc)) { i.remove(); @@ -120,7 +120,7 @@ } @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { final KeyCommand k = map.get(toMask(event)); if (k != null) { event.preventDefault(); @@ -129,7 +129,7 @@ } } - static int toMask(final KeyPressEvent event) { + static int toMask(KeyPressEvent event) { int mask = event.getUnicodeCharCode(); if (mask == 0) { mask = event.getNativeEvent().getKeyCode();
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java index 0ec9d10..1318125 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/KeyHelpPopup.java
@@ -51,7 +51,7 @@ closer.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { hide(); } }); @@ -84,7 +84,7 @@ } @Override - public void setVisible(final boolean show) { + public void setVisible(boolean show) { super.setVisible(show); if (show) { focus.setFocus(true); @@ -92,7 +92,7 @@ } @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { if (KeyCommandSet.toMask(event) == ShowHelpCommand.INSTANCE.keyMask) { // Block the '?' key from triggering us to show right after // we just hide ourselves. @@ -104,16 +104,16 @@ } @Override - public void onKeyDown(final KeyDownEvent event) { + public void onKeyDown(KeyDownEvent event) { if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) { hide(); } } - private void populate(final Grid lists) { + private void populate(Grid lists) { int[] end = new int[5]; int column = 0; - for (final KeyCommandSet set : combinedSetsByName()) { + for (KeyCommandSet set : combinedSetsByName()) { int row = end[column]; row = formatGroup(lists, row, column, set); end[column] = row; @@ -131,7 +131,7 @@ */ private static Collection<KeyCommandSet> combinedSetsByName() { LinkedHashMap<String, KeyCommandSet> byName = new LinkedHashMap<>(); - for (final KeyCommandSet set : GlobalKey.active.all.getSets()) { + for (KeyCommandSet set : GlobalKey.active.all.getSets()) { KeyCommandSet v = byName.get(set.getName()); if (v == null) { v = new KeyCommandSet(set.getName()); @@ -142,7 +142,7 @@ return byName.values(); } - private int formatGroup(final Grid lists, int row, final int col, final KeyCommandSet set) { + private int formatGroup(Grid lists, int row, int col, KeyCommandSet set) { if (set.isEmpty()) { return row; } @@ -157,8 +157,7 @@ return formatKeys(lists, row, col, set, null); } - private int formatKeys( - final Grid lists, int row, final int col, final KeyCommandSet set, final SafeHtml prefix) { + private int formatKeys(final Grid lists, int row, int col, KeyCommandSet set, SafeHtml prefix) { final CellFormatter fmt = lists.getCellFormatter(); final List<KeyCommand> keys = sort(set); if (lists.getRowCount() < row + keys.size()) { @@ -228,7 +227,7 @@ return row; } - private List<KeyCommand> sort(final KeyCommandSet set) { + private List<KeyCommand> sort(KeyCommandSet set) { final List<KeyCommand> keys = new ArrayList<>(set.getKeys()); Collections.sort( keys,
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java index 86402e1..1392675 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/NpTextBox.java
@@ -22,7 +22,7 @@ addKeyPressHandler(GlobalKey.STOP_PROPAGATION); } - public NpTextBox(final Element element) { + public NpTextBox(Element element) { super(element); addKeyPressHandler(GlobalKey.STOP_PROPAGATION); }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java index c2272c5..08217f4 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/globalkey/client/ShowHelpCommand.java
@@ -40,7 +40,7 @@ } @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { if (current != null) { // Already open? Close the dialog. // @@ -52,7 +52,7 @@ help.addCloseHandler( new CloseHandler<PopupPanel>() { @Override - public void onClose(final CloseEvent<PopupPanel> event) { + public void onClose(CloseEvent<PopupPanel> event) { current = null; BUS.fireEvent(new FocusEvent() {}); } @@ -61,7 +61,7 @@ help.setPopupPositionAndShow( new PositionCallback() { @Override - public void setPosition(final int pWidth, final int pHeight) { + public void setPosition(int pWidth, int pHeight) { final int left = (Window.getClientWidth() - pWidth) >> 1; final int wLeft = Window.getScrollLeft(); final int wTop = Window.getScrollTop();
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java index bc18323..f133e4d 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/progress/client/ProgressBar.java
@@ -41,7 +41,7 @@ } /** Create a bar displaying the specified message. */ - public ProgressBar(final String text) { + public ProgressBar(String text) { if (text == null || text.length() == 0) { callerText = ""; } else { @@ -68,7 +68,7 @@ } /** Update the bar's percent completion. */ - public void setValue(final int pComplete) { + public void setValue(int pComplete) { assert 0 <= pComplete && pComplete <= 100; value = pComplete; bar.setWidth(2 * pComplete + "px");
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java index eb141f15..c93a78b 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/AttMap.java
@@ -38,7 +38,7 @@ private Tag tag = ANY; private int live; - void reset(final String tagName) { + void reset(String tagName) { tag = TAGS.get(tagName.toLowerCase()); if (tag == null) { tag = ANY; @@ -46,7 +46,7 @@ live = 0; } - void onto(final Buffer raw, final SafeHtmlBuilder esc) { + void onto(Buffer raw, SafeHtmlBuilder esc) { for (int i = 0; i < live; i++) { final String v = values.get(i); if (v.length() > 0) { @@ -70,7 +70,7 @@ return ""; } - void set(String name, final String value) { + void set(String name, String value) { name = name.toLowerCase(); tag.assertSafe(name, value); @@ -91,7 +91,7 @@ } } - private static void assertNotJavascriptUrl(final String value) { + private static void assertNotJavascriptUrl(String value) { if (value.startsWith("#")) { // common in GWT, and safe, so bypass further checks
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java index 83abd5d..c6e1d30 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferDirect.java
@@ -22,37 +22,37 @@ } @Override - public void append(final boolean v) { + public void append(boolean v) { strbuf.append(v); } @Override - public void append(final char v) { + public void append(char v) { strbuf.append(v); } @Override - public void append(final int v) { + public void append(int v) { strbuf.append(v); } @Override - public void append(final long v) { + public void append(long v) { strbuf.append(v); } @Override - public void append(final float v) { + public void append(float v) { strbuf.append(v); } @Override - public void append(final double v) { + public void append(double v) { strbuf.append(v); } @Override - public void append(final String v) { + public void append(String v) { strbuf.append(v); }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java index e3aed55..bdd9801 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/BufferSealElement.java
@@ -17,42 +17,42 @@ final class BufferSealElement implements Buffer { private final SafeHtmlBuilder shb; - BufferSealElement(final SafeHtmlBuilder safeHtmlBuilder) { + BufferSealElement(SafeHtmlBuilder safeHtmlBuilder) { shb = safeHtmlBuilder; } @Override - public void append(final boolean v) { + public void append(boolean v) { shb.sealElement().append(v); } @Override - public void append(final char v) { + public void append(char v) { shb.sealElement().append(v); } @Override - public void append(final double v) { + public void append(double v) { shb.sealElement().append(v); } @Override - public void append(final float v) { + public void append(float v) { shb.sealElement().append(v); } @Override - public void append(final int v) { + public void append(int v) { shb.sealElement().append(v); } @Override - public void append(final long v) { + public void append(long v) { shb.sealElement().append(v); } @Override - public void append(final String v) { + public void append(String v) { shb.sealElement().append(v); }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java index 25cad1d..ef80cdb 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
@@ -45,11 +45,11 @@ request, new Callback() { @Override - public void onSuggestionsReady(final Request request, final Response response) { + public void onSuggestionsReady(Request request, Response response) { final String qpat = getQueryPattern(request.getQuery()); final boolean html = isHTML(); final ArrayList<Suggestion> r = new ArrayList<>(); - for (final Suggestion s : response.getSuggestions()) { + for (Suggestion s : response.getSuggestions()) { r.add(new BoldSuggestion(qpat, s, html)); } cb.onSuggestionsReady(request, new Response(r)); @@ -57,7 +57,7 @@ }); } - protected String getQueryPattern(final String query) { + protected String getQueryPattern(String query) { return query; } @@ -77,7 +77,7 @@ private final Suggestion suggestion; private final String displayString; - BoldSuggestion(final String qstr, final Suggestion s, final boolean html) { + BoldSuggestion(String qstr, Suggestion s, boolean html) { suggestion = s; String ds = s.getDisplayString();
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java index 9161652a..2a1ddc0 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
@@ -79,17 +79,17 @@ } /** @return the existing HTML property of a widget. */ - public static SafeHtml get(final HasHTML t) { + public static SafeHtml get(HasHTML t) { return new SafeHtmlString(t.getHTML()); } /** @return the existing HTML text, wrapped in a safe buffer. */ - public static SafeHtml asis(final String htmlText) { + public static SafeHtml asis(String htmlText) { return new SafeHtmlString(htmlText); } /** Set the HTML property of a widget. */ - public static <T extends HasHTML> T set(final T e, final SafeHtml str) { + public static <T extends HasHTML> T set(T e, SafeHtml str) { e.setHTML(str.asString()); return e; } @@ -106,13 +106,12 @@ } /** @return the existing inner HTML of a table cell. */ - public static SafeHtml get(final HTMLTable t, final int row, final int col) { + public static SafeHtml get(HTMLTable t, int row, int col) { return new SafeHtmlString(t.getHTML(row, col)); } /** Set the inner HTML of a table cell. */ - public static <T extends HTMLTable> T set( - final T t, final int row, final int col, final SafeHtml str) { + public static <T extends HTMLTable> T set(final T t, int row, int col, SafeHtml str) { t.setHTML(row, col, str.asString()); return t; } @@ -140,13 +139,13 @@ */ public SafeHtml wikify() { final SafeHtmlBuilder r = new SafeHtmlBuilder(); - for (final String p : linkify().asString().split("\n\n")) { + for (String p : linkify().asString().split("\n\n")) { if (isQuote(p)) { wikifyQuote(r, p); } else if (isPreFormat(p)) { r.openElement("p"); - for (final String line : p.split("\n")) { + for (String line : p.split("\n")) { r.openSpan(); r.setStyleName(RESOURCES.css().wikiPreFormat()); r.append(asis(line)); @@ -167,7 +166,7 @@ return r.toSafeHtml(); } - private void wikifyList(final SafeHtmlBuilder r, final String p) { + private void wikifyList(SafeHtmlBuilder r, String p) { boolean in_ul = false; boolean in_p = false; for (String line : p.split("\n")) { @@ -232,11 +231,11 @@ return p.startsWith("> ") || p.startsWith(" > "); } - private static boolean isPreFormat(final String p) { + private static boolean isPreFormat(String p) { return p.contains("\n ") || p.contains("\n\t") || p.startsWith(" ") || p.startsWith("\t"); } - private static boolean isList(final String p) { + private static boolean isList(String p) { return p.contains("\n- ") || p.contains("\n* ") || p.startsWith("- ") || p.startsWith("* "); } @@ -252,7 +251,7 @@ * {@code $<i>n</i>}. * @return a new string, after the replacement has been made. */ - public SafeHtml replaceFirst(final String regex, final String repl) { + public SafeHtml replaceFirst(String regex, String repl) { return new SafeHtmlString(asString().replaceFirst(regex, repl)); } @@ -268,7 +267,7 @@ * {@code $<i>n</i>}. * @return a new string, after the replacements have been made. */ - public SafeHtml replaceAll(final String regex, final String repl) { + public SafeHtml replaceAll(String regex, String repl) { return new SafeHtmlString(asString().replaceAll(regex, repl)); }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java index f54149b..a926906 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilder.java
@@ -49,12 +49,12 @@ return !isEmpty(); } - public SafeHtmlBuilder append(final boolean in) { + public SafeHtmlBuilder append(boolean in) { cb.append(in); return this; } - public SafeHtmlBuilder append(final char in) { + public SafeHtmlBuilder append(char in) { switch (in) { case '&': cb.append("&"); @@ -83,22 +83,22 @@ return this; } - public SafeHtmlBuilder append(final int in) { + public SafeHtmlBuilder append(int in) { cb.append(in); return this; } - public SafeHtmlBuilder append(final long in) { + public SafeHtmlBuilder append(long in) { cb.append(in); return this; } - public SafeHtmlBuilder append(final float in) { + public SafeHtmlBuilder append(float in) { cb.append(in); return this; } - public SafeHtmlBuilder append(final double in) { + public SafeHtmlBuilder append(double in) { cb.append(in); return this; } @@ -112,7 +112,7 @@ } /** Append already safe HTML as-is, avoiding double escaping. */ - public SafeHtmlBuilder append(final SafeHtml in) { + public SafeHtmlBuilder append(SafeHtml in) { if (in != null) { cb.append(in.asString()); } @@ -120,7 +120,7 @@ } /** Append the string, escaping unsafe characters. */ - public SafeHtmlBuilder append(final String in) { + public SafeHtmlBuilder append(String in) { if (in != null) { impl.escapeStr(this, in); } @@ -128,7 +128,7 @@ } /** Append the string, escaping unsafe characters. */ - public SafeHtmlBuilder append(final StringBuilder in) { + public SafeHtmlBuilder append(StringBuilder in) { if (in != null) { append(in.toString()); } @@ -136,7 +136,7 @@ } /** Append the string, escaping unsafe characters. */ - public SafeHtmlBuilder append(final StringBuffer in) { + public SafeHtmlBuilder append(StringBuffer in) { if (in != null) { append(in.toString()); } @@ -144,7 +144,7 @@ } /** Append the result of toString(), escaping unsafe characters. */ - public SafeHtmlBuilder append(final Object in) { + public SafeHtmlBuilder append(Object in) { if (in != null) { append(in.toString()); } @@ -152,7 +152,7 @@ } /** Append the string, escaping unsafe characters. */ - public SafeHtmlBuilder append(final CharSequence in) { + public SafeHtmlBuilder append(CharSequence in) { if (in != null) { escapeCS(this, in); } @@ -167,7 +167,7 @@ * * @param tagName name of the HTML element to open. */ - public SafeHtmlBuilder openElement(final String tagName) { + public SafeHtmlBuilder openElement(String tagName) { assert isElementName(tagName); cb.append("<"); cb.append(tagName); @@ -187,7 +187,7 @@ * @return the attribute value, as a string. The empty string if the attribute has not been * assigned a value. The returned string is the raw (unescaped) value. */ - public String getAttribute(final String name) { + public String getAttribute(String name) { assert isAttributeName(name); assert cb == sBuf; return att.get(name); @@ -200,7 +200,7 @@ * @param value value to assign; any existing value is replaced. The value is escaped (if * necessary) during the assignment. */ - public SafeHtmlBuilder setAttribute(final String name, final String value) { + public SafeHtmlBuilder setAttribute(String name, String value) { assert isAttributeName(name); assert cb == sBuf; att.set(name, value != null ? value : ""); @@ -213,7 +213,7 @@ * @param name name of the attribute to set. * @param value value to assign, any existing value is replaced. */ - public SafeHtmlBuilder setAttribute(final String name, final int value) { + public SafeHtmlBuilder setAttribute(String name, int value) { return setAttribute(name, String.valueOf(value)); } @@ -227,7 +227,7 @@ * @param name name of the attribute to append onto. * @param value additional value to append. */ - public SafeHtmlBuilder appendAttribute(final String name, String value) { + public SafeHtmlBuilder appendAttribute(String name, String value) { if (value != null && value.length() > 0) { final String e = getAttribute(name); return setAttribute(name, e.length() > 0 ? e + " " + value : value); @@ -236,17 +236,17 @@ } /** Set the height attribute of the current element. */ - public SafeHtmlBuilder setHeight(final int height) { + public SafeHtmlBuilder setHeight(int height) { return setAttribute("height", height); } /** Set the width attribute of the current element. */ - public SafeHtmlBuilder setWidth(final int width) { + public SafeHtmlBuilder setWidth(int width) { return setAttribute("width", width); } /** Set the CSS class name for this element. */ - public SafeHtmlBuilder setStyleName(final String style) { + public SafeHtmlBuilder setStyleName(String style) { assert isCssName(style); return setAttribute("class", style); } @@ -256,7 +256,7 @@ * * <p>If no CSS class name has been specified yet, this method initializes it to the single name. */ - public SafeHtmlBuilder addStyleName(final String style) { + public SafeHtmlBuilder addStyleName(String style) { assert isCssName(style); return appendAttribute("class", style); } @@ -281,7 +281,7 @@ } /** Append a closing tag for the named element. */ - public SafeHtmlBuilder closeElement(final String name) { + public SafeHtmlBuilder closeElement(String name) { assert isElementName(name); cb.append("</"); cb.append(name); @@ -362,7 +362,7 @@ } /** Append "<param name=... value=... />". */ - public SafeHtmlBuilder paramElement(final String name, final String value) { + public SafeHtmlBuilder paramElement(String name, String value) { openElement("param"); setAttribute("name", name); setAttribute("value", value); @@ -379,21 +379,21 @@ return cb.toString(); } - private static void escapeCS(final SafeHtmlBuilder b, final CharSequence in) { + private static void escapeCS(SafeHtmlBuilder b, CharSequence in) { for (int i = 0; i < in.length(); i++) { b.append(in.charAt(i)); } } - private static boolean isElementName(final String name) { + private static boolean isElementName(String name) { return name.matches("^[a-zA-Z][a-zA-Z0-9_-]*$"); } - private static boolean isAttributeName(final String name) { + private static boolean isAttributeName(String name) { return isElementName(name); } - private static boolean isCssName(final String name) { + private static boolean isCssName(String name) { return isElementName(name); } @@ -403,14 +403,14 @@ private static class ServerImpl extends Impl { @Override - void escapeStr(final SafeHtmlBuilder b, final String in) { + void escapeStr(SafeHtmlBuilder b, String in) { SafeHtmlBuilder.escapeCS(b, in); } } private static class ClientImpl extends Impl { @Override - void escapeStr(final SafeHtmlBuilder b, final String in) { + void escapeStr(SafeHtmlBuilder b, String in) { b.cb.append(escape(in)); }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java index 57392bf..889509a 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtmlString.java
@@ -18,7 +18,7 @@ class SafeHtmlString extends SafeHtml { private final String html; - SafeHtmlString(final String h) { + SafeHtmlString(String h) { html = h; }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java index 4e39c1f..571f72d 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/server/CacheControlFilter.java
@@ -48,14 +48,13 @@ */ public class CacheControlFilter implements Filter { @Override - public void init(final FilterConfig config) {} + public void init(FilterConfig config) {} @Override public void destroy() {} @Override - public void doFilter( - final ServletRequest sreq, final ServletResponse srsp, final FilterChain chain) + public void doFilter(final ServletRequest sreq, ServletResponse srsp, FilterChain chain) throws IOException, ServletException { final HttpServletRequest req = (HttpServletRequest) sreq; final HttpServletResponse rsp = (HttpServletResponse) srsp; @@ -70,7 +69,7 @@ chain.doFilter(req, rsp); } - private static boolean cacheForever(final String pathInfo, final HttpServletRequest req) { + private static boolean cacheForever(String pathInfo, HttpServletRequest req) { if (pathInfo.endsWith(".cache.html") || pathInfo.endsWith(".cache.gif") || pathInfo.endsWith(".cache.png") @@ -87,14 +86,14 @@ return false; } - private static boolean nocache(final String pathInfo) { + private static boolean nocache(String pathInfo) { if (pathInfo.endsWith(".nocache.js")) { return true; } return false; } - private static String pathInfo(final HttpServletRequest req) { + private static String pathInfo(HttpServletRequest req) { final String uri = req.getRequestURI(); final String ctx = req.getContextPath(); return uri.startsWith(ctx) ? uri.substring(ctx.length()) : uri;
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java index 7c165e5..fdaf861 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/AutoCenterDialogBox.java
@@ -28,11 +28,11 @@ this(false); } - public AutoCenterDialogBox(final boolean autoHide) { + public AutoCenterDialogBox(boolean autoHide) { this(autoHide, true); } - public AutoCenterDialogBox(final boolean autoHide, final boolean modal) { + public AutoCenterDialogBox(boolean autoHide, boolean modal) { super(autoHide, modal); } @@ -43,7 +43,7 @@ Window.addResizeHandler( new ResizeHandler() { @Override - public void onResize(final ResizeEvent event) { + public void onResize(ResizeEvent event) { final int w = event.getWidth(); final int h = event.getHeight(); AutoCenterDialogBox.this.onResize(w, h); @@ -71,7 +71,7 @@ * @param width new browser window width * @param height new browser window height */ - protected void onResize(final int width, final int height) { + protected void onResize(int width, int height) { if (isAttached()) { center(); }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java index ca712c3..4614546 100644 --- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java +++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/user/client/ViewSite.java
@@ -51,7 +51,7 @@ * * @param view the next view to display. */ - public void setView(final V view) { + public void setView(V view) { if (next != null) { main.remove(next); } @@ -67,10 +67,10 @@ * * @param view the view being displayed. */ - protected void onShowView(final V view) {} + protected void onShowView(V view) {} @SuppressWarnings("unchecked") - final void swap(final View v) { + final void swap(View v) { if (next != null && next.getWidget() == v) { if (current != null) { main.remove(current);
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java index 17b0a4d..9a2dbe3 100644 --- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java +++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtmlBuilderTest.java
@@ -280,11 +280,11 @@ new SafeHtmlBuilder().openElement("form").setAttribute("action", href); } - private static String escape(final char c) { + private static String escape(char c) { return new SafeHtmlBuilder().append(c).asString(); } - private static String escape(final String c) { + private static String escape(String c) { return new SafeHtmlBuilder().append(c).asString(); } }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java index 32f79d7..4df2f5f 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/DateFormatter.java
@@ -90,7 +90,7 @@ } /** Format a date using the locale's medium length format. */ - public String mediumFormat(final Date dt) { + public String mediumFormat(Date dt) { if (dt == null) { return ""; }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java index 3045180..7c62ed7 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/RelativeDateFormatter.java
@@ -37,7 +37,6 @@ * @return age of given {@link Date} compared to now formatted in the same relative format as * returned by {@code git log --relative-date} */ - @SuppressWarnings("boxing") public static String format(Date when) { long ageMillis = (new Date()).getTime() - when.getTime();
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java index 0de8b68..9149230 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/ChangeInfo.java
@@ -136,8 +136,15 @@ public final native boolean starred() /*-{ return this.starred ? true : false; }-*/; + public final native boolean muted() /*-{ return this.muted ? true : false; }-*/; + public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/; + public final native boolean isPrivate() /*-{ return this.is_private ? true : false; }-*/; + + public final native boolean + isWorkInProgress() /*-{ return this.work_in_progress ? true : false; }-*/; + public final native NativeMap<LabelInfo> allLabels() /*-{ return this.labels; }-*/; public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java index 23e1a93..1dcb284 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GeneralPreferences.java
@@ -148,6 +148,9 @@ private native String defaultBaseForMergesRaw() /*-{ return this.default_base_for_merges }-*/; + public final native boolean + publishCommentsOnPush() /*-{ return this.publish_comments_on_push || false }-*/; + public final native JsArray<TopMenuItem> my() /*-{ return this.my; }-*/; public final native void changesPerPage(int n) /*-{ this.changes_per_page = n }-*/; @@ -224,6 +227,9 @@ private native void defaultBaseForMergesRaw(String b) /*-{ this.default_base_for_merges = b }-*/; + public final native void publishCommentsOnPush( + boolean p) /*-{ this.publish_comments_on_push = p }-*/; + public final void setMyMenus(List<TopMenuItem> myMenus) { initMy(); for (TopMenuItem n : myMenus) {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java index 43ff60c..4b17068 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
@@ -40,7 +40,7 @@ /** Loop through the result map and set asProperty on the children. */ public static <T extends JavaScriptObject, M extends NativeMap<T>> - AsyncCallback<M> copyKeysIntoChildren(final String asProperty, AsyncCallback<M> callback) { + AsyncCallback<M> copyKeysIntoChildren(String asProperty, AsyncCallback<M> callback) { return new TransformCallback<M, M>(callback) { @Override protected M transform(M result) {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java index a4b90c3..e0bca0e 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/NativeString.java
@@ -38,7 +38,7 @@ public native String asString() /*-{ return this.s; }-*/; - public static AsyncCallback<NativeString> unwrap(final AsyncCallback<String> cb) { + public static AsyncCallback<NativeString> unwrap(AsyncCallback<String> cb) { return new AsyncCallback<NativeString>() { @Override public void onSuccess(NativeString result) {
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java index ebaa63b..1421386 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/rpc/Natives.java
@@ -35,7 +35,7 @@ return Collections.emptySet(); } - public static List<String> asList(final JsArrayString arr) { + public static List<String> asList(JsArrayString arr) { if (arr == null) { return null; } @@ -59,7 +59,7 @@ }; } - public static <T extends JavaScriptObject> List<T> asList(final JsArray<T> arr) { + public static <T extends JavaScriptObject> List<T> asList(JsArray<T> arr) { if (arr == null) { return null; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java index 58865fa..438df34 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
@@ -29,7 +29,7 @@ private Button okButton; public ConfirmationDialog( - final String dialogTitle, final SafeHtml message, final ConfirmationCallback callback) { + final String dialogTitle, SafeHtml message, ConfirmationCallback callback) { super(/* auto hide */ false, /* modal */ true); setGlassEnabled(true); setText(dialogTitle);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java index e4dc40d..0081783 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -170,7 +170,7 @@ return p.toString(); } - public static String toGroup(final AccountGroup.Id id) { + public static String toGroup(AccountGroup.Id id) { return ADMIN_GROUPS + id.toString(); } @@ -293,7 +293,7 @@ return r; } - private static void dashboard(final String token) { + private static void dashboard(String token) { String rest = skip(token); if (rest.matches("[0-9]+")) { Gerrit.display(token, new AccountDashboardScreen(Account.Id.parse(rest))); @@ -319,7 +319,7 @@ Gerrit.display(token, new NotFoundScreen()); } - private static void projects(final String token) { + private static void projects(String token) { String rest = skip(token); int c = rest.indexOf(DASHBOARDS); if (0 <= c) { @@ -366,7 +366,7 @@ Gerrit.display(token, new NotFoundScreen()); } - private static void change(final String token) { + private static void change(String token) { String rest = skip(token); int c = rest.lastIndexOf(','); String panel = null; @@ -456,7 +456,7 @@ return new PatchSet.Id(id, psIdStr.equals("edit") ? 0 : Integer.parseInt(psIdStr)); } - private static void extension(final String token) { + private static void extension(String token) { ExtensionScreen view = new ExtensionScreen(skip(token)); if (view.isFound()) { Gerrit.display(token, view); @@ -533,7 +533,7 @@ }); } - private static void codemirrorForEdit(final String token, final Patch.Key id, final int line) { + private static void codemirrorForEdit(String token, Patch.Key id, int line) { GWT.runAsync( new AsyncSplit(token) { @Override @@ -839,7 +839,7 @@ } } - private static void docSearch(final String token) { + private static void docSearch(String token) { GWT.runAsync( new AsyncSplit(token) { @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java index e880712..d793082 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
@@ -86,19 +86,19 @@ } /** Create a dialog box to show a single message string. */ - public ErrorDialog(final String message) { + public ErrorDialog(String message) { this(); body.add(new Label(message)); } /** Create a dialog box to show a single message string. */ - public ErrorDialog(final SafeHtml message) { + public ErrorDialog(SafeHtml message) { this(); body.add(message.toBlockWidget()); } /** Create a dialog box to nicely format an exception. */ - public ErrorDialog(final Throwable what) { + public ErrorDialog(Throwable what) { this(); String hdr; @@ -151,7 +151,7 @@ } } - public ErrorDialog setText(final String t) { + public ErrorDialog setText(String t) { text.setText(t); return this; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java index 751302e..e02c4e0 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -170,7 +170,7 @@ * * @param token location to parse, load, and render. */ - public static void display(final String token) { + public static void display(String token) { if (body.getView() == null || !body.getView().displayToken(token)) { dispatcher.display(token); updateUiLink(token); @@ -191,7 +191,7 @@ * @param token location that refers to {@code view}. * @param view the view to load. */ - public static void display(final String token, final Screen view) { + public static void display(String token, Screen view) { if (view.isRequiresSignIn() && !isSignedIn()) { doSignIn(token); } else { @@ -217,7 +217,7 @@ * * @param token new location that is already visible. */ - public static void updateImpl(final String token) { + public static void updateImpl(String token) { History.newItem(token, false); dispatchHistoryHooks(token); } @@ -226,7 +226,7 @@ searchPanel.setText(query); } - public static void setWindowTitle(final Screen screen, final String text) { + public static void setWindowTitle(Screen screen, String text) { if (screen == body.getView()) { if (text == null || text.length() == 0) { Window.setTitle(M.windowTitle1(myHost)); @@ -428,7 +428,7 @@ } @Override - public String decode(final String e) { + public String decode(String e) { return URL.decodeQueryString(e); } @@ -476,7 +476,7 @@ cbg.addFinal( new GerritCallback<HostPageData>() { @Override - public void onSuccess(final HostPageData result) { + public void onSuccess(HostPageData result) { Document.get().getElementById("gerrit_hostpagedata").removeFromParent(); myTheme = result.theme; isNoteDbEnabled = result.isNoteDbEnabled; @@ -957,7 +957,7 @@ return docSearch; } - private static void getDocIndex(final AsyncCallback<DocInfo> cb) { + private static void getDocIndex(AsyncCallback<DocInfo> cb) { RequestBuilder req = new RequestBuilder(RequestBuilder.HEAD, GWT.getHostPageBaseURL() + INDEX); req.setCallback( new RequestCallback() { @@ -1031,22 +1031,21 @@ menuRight.add(fp); } - private static Anchor anchor(final String text, final String to) { + private static Anchor anchor(String text, String to) { final Anchor a = new Anchor(text, to); a.setStyleName(RESOURCES.css().menuItem()); Roles.getMenuitemRole().set(a.getElement()); return a; } - private static LinkMenuItem addLink( - final LinkMenuBar m, final String text, final String historyToken) { + private static LinkMenuItem addLink(final LinkMenuBar m, String text, String historyToken) { LinkMenuItem i = new LinkMenuItem(text, historyToken); m.addItem(i); return i; } private static void insertLink( - final LinkMenuBar m, final String text, final String historyToken, final int beforeIndex) { + final LinkMenuBar m, String text, String historyToken, int beforeIndex) { m.insertItem(new LinkMenuItem(text, historyToken), beforeIndex); } @@ -1090,7 +1089,7 @@ return i; } - private static void addDocLink(final LinkMenuBar m, final String text, final String href) { + private static void addDocLink(LinkMenuBar m, String text, String href) { final Anchor atag = anchor(text, docUrl + href); atag.setTarget("_blank"); m.add(atag);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java index b8195805..cc05e12 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
@@ -37,27 +37,27 @@ } } - static void register(final Widget body) { + static void register(Widget body) { final KeyCommandSet jumps = new KeyCommandSet(); jumps.add( new KeyCommand(0, 'o', Gerrit.C.jumpAllOpen()) { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { Gerrit.display(PageLinks.toChangeQuery("status:open")); } }); jumps.add( new KeyCommand(0, 'm', Gerrit.C.jumpAllMerged()) { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { Gerrit.display(PageLinks.toChangeQuery("status:merged")); } }); jumps.add( new KeyCommand(0, 'a', Gerrit.C.jumpAllAbandoned()) { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { Gerrit.display(PageLinks.toChangeQuery("status:abandoned")); } }); @@ -66,35 +66,35 @@ jumps.add( new KeyCommand(0, 'i', Gerrit.C.jumpMine()) { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { Gerrit.display(PageLinks.MINE); } }); jumps.add( new KeyCommand(0, 'd', Gerrit.C.jumpMineDrafts()) { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { Gerrit.display(PageLinks.toChangeQuery("owner:self is:draft")); } }); jumps.add( new KeyCommand(0, 'c', Gerrit.C.jumpMineDraftComments()) { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { Gerrit.display(PageLinks.toChangeQuery("has:draft")); } }); jumps.add( new KeyCommand(0, 'w', Gerrit.C.jumpMineWatched()) { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { Gerrit.display(PageLinks.toChangeQuery("is:watched status:open")); } }); jumps.add( new KeyCommand(0, 's', Gerrit.C.jumpMineStarred()) { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { Gerrit.display(PageLinks.toChangeQuery("is:starred")); } });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java index cd715c6..4153439 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
@@ -28,7 +28,7 @@ private static int hideDepth; /** Execute code, hiding the RPCs they execute from being shown visually. */ - public static void hide(final Runnable run) { + public static void hide(Runnable run) { try { hideDepth++; run.run(); @@ -49,7 +49,7 @@ } @Override - public void onRpcStart(final RpcStartEvent event) { + public void onRpcStart(RpcStartEvent event) { onRpcStart(); } @@ -62,7 +62,7 @@ } @Override - public void onRpcComplete(final RpcCompleteEvent event) { + public void onRpcComplete(RpcCompleteEvent event) { onRpcComplete(); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java index 37c6a0b..dc3c043 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
@@ -48,7 +48,7 @@ searchBox.addKeyPressHandler( new KeyPressHandler() { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) { if (!suggestionDisplay.isSuggestionSelected) { doSearch(); @@ -92,7 +92,7 @@ body.add(searchButton); } - void setText(final String query) { + void setText(String query) { searchBox.setText(query); } @@ -105,7 +105,7 @@ this, new KeyCommand(0, '/', Gerrit.C.keySearch()) { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { event.preventDefault(); searchBox.setFocus(true); searchBox.selectAll();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java index 20bc2746..753d421 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -44,13 +44,12 @@ "cc:"), new AccountSuggestOracle() { @Override - public void onRequestSuggestions(final Request request, final Callback done) { + public void onRequestSuggestions(Request request, Callback done) { super.onRequestSuggestions( request, new Callback() { @Override - public void onSuggestionsReady( - final Request request, final Response response) { + public void onSuggestionsReady(final Request request, Response response) { if ("self".startsWith(request.getQuery())) { final ArrayList<SuggestOracle.Suggestion> r = new ArrayList<>(response.getSuggestions().size() + 1); @@ -131,10 +130,13 @@ suggestions.add("is:open"); suggestions.add("is:pending"); suggestions.add("is:draft"); + suggestions.add("is:private"); suggestions.add("is:closed"); suggestions.add("is:merged"); suggestions.add("is:abandoned"); suggestions.add("is:mergeable"); + suggestions.add("is:ignored"); + suggestions.add("is:wip"); suggestions.add("status:"); suggestions.add("status:open"); @@ -189,7 +191,7 @@ return; } - for (final ParamSuggester ps : paramSuggester) { + for (ParamSuggester ps : paramSuggester) { if (ps.applicable(lastWord)) { ps.suggest(lastWord, request, done); return; @@ -208,7 +210,7 @@ done.onSuggestionsReady(request, new Response(r)); } - private String getLastWord(final String query) { + private String getLastWord(String query) { final int lastSpace = query.lastIndexOf(' '); if (lastSpace == query.length() - 1) { return null; @@ -220,7 +222,7 @@ } @Override - protected String getQueryPattern(final String query) { + protected String getQueryPattern(String query) { return super.getQueryPattern(getLastWord(query)); } @@ -255,18 +257,18 @@ private final List<String> operators; private final SuggestOracle parameterSuggestionOracle; - ParamSuggester(final List<String> operators, final SuggestOracle parameterSuggestionOracle) { + ParamSuggester(List<String> operators, SuggestOracle parameterSuggestionOracle) { this.operators = operators; this.parameterSuggestionOracle = parameterSuggestionOracle; } - boolean applicable(final String query) { + boolean applicable(String query) { final String operator = getApplicableOperator(query, operators); return operator != null && query.length() > operator.length(); } - private String getApplicableOperator(final String lastWord, final List<String> operators) { - for (final String operator : operators) { + private String getApplicableOperator(String lastWord, List<String> operators) { + for (String operator : operators) { if (lastWord.startsWith(operator)) { return operator; } @@ -274,17 +276,17 @@ return null; } - void suggest(final String lastWord, final Request request, final Callback done) { + void suggest(String lastWord, Request request, Callback done) { final String operator = getApplicableOperator(lastWord, operators); parameterSuggestionOracle.requestSuggestions( new Request(lastWord.substring(operator.length()), request.getLimit()), new Callback() { @Override - public void onSuggestionsReady(final Request req, final Response response) { + public void onSuggestionsReady(Request req, Response response) { final String query = request.getQuery(); final List<SearchSuggestOracle.Suggestion> r = new ArrayList<>(response.getSuggestions().size()); - for (final SearchSuggestOracle.Suggestion s : response.getSuggestions()) { + for (SearchSuggestOracle.Suggestion s : response.getSuggestions()) { r.add( new SearchSuggestion( s.getDisplayString(), @@ -295,7 +297,7 @@ done.onSuggestionsReady(request, new Response(r)); } - private String quoteIfNeeded(final String s) { + private String quoteIfNeeded(String s) { if (!s.matches("^\\S*$")) { return "\"" + s + "\""; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/StringListPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/StringListPanel.java index 1a1f7bd..f771fee 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/StringListPanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/StringListPanel.java
@@ -189,7 +189,7 @@ return v; } - private void populate(final int row, List<String> values) { + private void populate(int row, List<String> values) { FlexCellFormatter fmt = table.getFlexCellFormatter(); fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().iconCell()); fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().leftMostCell());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java index 40116af..cb529f4 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/UserPopupPanel.java
@@ -52,8 +52,9 @@ userEmail.setText(account.email()); } if (showSettingsLink) { - if (Gerrit.info().auth().switchAccountUrl() != null) { - switchAccount.setHref(Gerrit.info().auth().switchAccountUrl()); + String switchAccountUrl = Gerrit.info().auth().switchAccountUrl(); + if (switchAccountUrl != null) { + switchAccount.setHref(switchAccountUrl.replace("${path}", "/")); } else if (Gerrit.info().auth().isDev() || Gerrit.info().auth().isOpenId()) { switchAccount.setHref(Gerrit.selfRedirect("/login")); } else {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java index 39a52e3..a0060d5 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/access/AccessMap.java
@@ -31,7 +31,7 @@ api.get(NativeMap.copyKeysIntoChildren(callback)); } - public static void get(final Project.NameKey project, final AsyncCallback<ProjectAccessInfo> cb) { + public static void get(Project.NameKey project, AsyncCallback<ProjectAccessInfo> cb) { get( Collections.singleton(project), new AsyncCallback<AccessMap>() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java index 4a3a5f8..314871e 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -67,6 +67,8 @@ String signedOffBy(); + String publishCommentsOnPush(); + String myMenu(); String myMenuInfo();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties index 9d87365..d31abdb 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -38,6 +38,7 @@ showLegacycidInChangeTable = Show Change Number In Changes Table muteCommonPathPrefixes = Mute Common Path Prefixes In File List signedOffBy = Insert Signed-off-by Footer For Inline Edit Changes +publishCommentsOnPush = Publish Draft Comments When a Change Is Updated by Push myMenu = My Menu myMenuInfo = \ Menu items for the 'My' top level menu. \
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java index da0357f..cbd7635 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -91,7 +91,7 @@ registerNewEmail.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { doRegisterNewEmail(); } }); @@ -148,7 +148,7 @@ save.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { doSave(); } }); @@ -156,7 +156,7 @@ emailPick.addChangeHandler( new ChangeHandler() { @Override - public void onChange(final ChangeEvent event) { + public void onChange(ChangeEvent event) { final int idx = emailPick.getSelectedIndex(); final String v = 0 <= idx ? emailPick.getValue(idx) : null; if (Util.C.buttonOpenRegisterNewEmail().equals(v)) { @@ -249,7 +249,7 @@ void display() {} - protected void row(final Grid info, final int row, final String name, final Widget field) { + protected void row(Grid info, int row, String name, Widget field) { info.setText(row, labelIdx, name); info.setWidget(row, fieldIdx, field); info.getCellFormatter().addStyleName(row, 0, Gerrit.RESOURCES.css().header()); @@ -279,7 +279,7 @@ form.addSubmitHandler( new FormPanel.SubmitHandler() { @Override - public void onSubmit(final SubmitEvent event) { + public void onSubmit(SubmitEvent event) { event.cancel(); final String addr = inEmail.getText().trim(); if (!addr.contains("@")) { @@ -310,7 +310,7 @@ } @Override - public void onFailure(final Throwable caught) { + public void onFailure(Throwable caught) { inEmail.setEnabled(true); register.setEnabled(true); if (caught.getMessage().startsWith(EmailException.MESSAGE)) { @@ -331,7 +331,7 @@ register.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { form.submit(); } });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java index dfbd5c7..5c6d40f 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
@@ -49,7 +49,7 @@ deleteIdentity.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { identites.deleteChecked(); } }); @@ -60,7 +60,7 @@ linkIdentity.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { Location.assign(Gerrit.loginRedirect(History.getToken()) + "?link"); } }); @@ -167,7 +167,7 @@ deleteIdentity.setEnabled(on); } - void display(final JsArray<ExternalIdInfo> results) { + void display(JsArray<ExternalIdInfo> results) { List<ExternalIdInfo> idList = Natives.asList(results); Collections.sort(idList); @@ -175,13 +175,13 @@ table.removeRow(table.getRowCount() - 1); } - for (final ExternalIdInfo k : idList) { + for (ExternalIdInfo k : idList) { addOneId(k); } updateDeleteButton(); } - void addOneId(final ExternalIdInfo k) { + void addOneId(ExternalIdInfo k) { if (k.isUsername()) { // Don't display the username as an identity here. return;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyOAuthTokenScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyOAuthTokenScreen.java index 5836763..173dba6 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyOAuthTokenScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyOAuthTokenScreen.java
@@ -111,7 +111,7 @@ }); } - private void display(final GeneralPreferences prefs) { + private void display(GeneralPreferences prefs) { AccountApi.self() .view("oauthtoken") .get(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java index 03e72c7..3852387 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
@@ -104,7 +104,7 @@ } @Override - public void onFailure(final Throwable caught) { + public void onFailure(Throwable caught) { if (RestApi.isNotFound(caught)) { Gerrit.getUserAccount().username(null); display(); @@ -121,7 +121,7 @@ enableUI(true); } - private void row(final Grid info, final int row, final String name, final Widget field) { + private void row(Grid info, int row, String name, Widget field) { final CellFormatter fmt = info.getCellFormatter(); if (LocaleInfo.getCurrentLocale().isRTL()) { info.setText(row, 1, name); @@ -146,7 +146,7 @@ } @Override - public void onFailure(final Throwable caught) { + public void onFailure(Throwable caught) { enableUI(true); } });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java index 2edc137..f349065 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -55,6 +55,7 @@ private CheckBox legacycidInChangeTable; private CheckBox muteCommonPathPrefixes; private CheckBox signedOffBy; + private CheckBox publishCommentsOnPush; private ListBox maximumPageSize; private ListBox dateFormat; private ListBox timeFormat; @@ -73,7 +74,7 @@ showSiteHeader = new CheckBox(Util.C.showSiteHeader()); useFlashClipboard = new CheckBox(Util.C.useFlashClipboard()); maximumPageSize = new ListBox(); - for (final int v : PAGESIZE_CHOICES) { + for (int v : PAGESIZE_CHOICES) { maximumPageSize.addItem(Util.M.rowsPerPage(v), String.valueOf(v)); } @@ -161,9 +162,10 @@ legacycidInChangeTable = new CheckBox(Util.C.showLegacycidInChangeTable()); muteCommonPathPrefixes = new CheckBox(Util.C.muteCommonPathPrefixes()); signedOffBy = new CheckBox(Util.C.signedOffBy()); + publishCommentsOnPush = new CheckBox(Util.C.publishCommentsOnPush()); boolean flashClippy = !UserAgent.hasJavaScriptClipboard() && UserAgent.Flash.isInstalled(); - final Grid formGrid = new Grid(14 + (flashClippy ? 1 : 0), 2); + final Grid formGrid = new Grid(15 + (flashClippy ? 1 : 0), 2); int row = 0; @@ -223,6 +225,10 @@ formGrid.setWidget(row, fieldIdx, signedOffBy); row++; + formGrid.setText(row, labelIdx, ""); + formGrid.setWidget(row, fieldIdx, publishCommentsOnPush); + row++; + if (flashClippy) { formGrid.setText(row, labelIdx, ""); formGrid.setWidget(row, fieldIdx, useFlashClipboard); @@ -235,7 +241,7 @@ save.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { doSave(); } }); @@ -257,6 +263,7 @@ e.listenTo(legacycidInChangeTable); e.listenTo(muteCommonPathPrefixes); e.listenTo(signedOffBy); + e.listenTo(publishCommentsOnPush); e.listenTo(diffView); e.listenTo(reviewCategoryStrategy); e.listenTo(emailStrategy); @@ -283,7 +290,7 @@ }); } - private void enable(final boolean on) { + private void enable(boolean on) { showSiteHeader.setEnabled(on); useFlashClipboard.setEnabled(on); maximumPageSize.setEnabled(on); @@ -295,6 +302,7 @@ legacycidInChangeTable.setEnabled(on); muteCommonPathPrefixes.setEnabled(on); signedOffBy.setEnabled(on); + publishCommentsOnPush.setEnabled(on); reviewCategoryStrategy.setEnabled(on); diffView.setEnabled(on); emailStrategy.setEnabled(on); @@ -320,6 +328,7 @@ legacycidInChangeTable.setValue(p.legacycidInChangeTable()); muteCommonPathPrefixes.setValue(p.muteCommonPathPrefixes()); signedOffBy.setValue(p.signedOffBy()); + publishCommentsOnPush.setValue(p.publishCommentsOnPush()); setListBox( reviewCategoryStrategy, GeneralPreferencesInfo.ReviewCategoryStrategy.NONE, @@ -342,19 +351,18 @@ myMenus.display(values); } - private void setListBox(final ListBox f, final int defaultValue, final int currentValue) { + private void setListBox(ListBox f, int defaultValue, int currentValue) { setListBox(f, String.valueOf(defaultValue), String.valueOf(currentValue)); } - private <T extends Enum<?>> void setListBox( - final ListBox f, final T defaultValue, final T currentValue) { + private <T extends Enum<?>> void setListBox(final ListBox f, T defaultValue, T currentValue) { setListBox( f, defaultValue != null ? defaultValue.name() : "", currentValue != null ? currentValue.name() : ""); } - private void setListBox(final ListBox f, final String defaultValue, final String currentValue) { + private void setListBox(ListBox f, String defaultValue, String currentValue) { final int n = f.getItemCount(); for (int i = 0; i < n; i++) { if (f.getValue(i).equals(currentValue)) { @@ -367,7 +375,7 @@ } } - private int getListBox(final ListBox f, final int defaultValue) { + private int getListBox(ListBox f, int defaultValue) { final int idx = f.getSelectedIndex(); if (0 <= idx) { return Short.parseShort(f.getValue(idx)); @@ -375,7 +383,7 @@ return defaultValue; } - private <T extends Enum<?>> T getListBox(final ListBox f, final T defaultValue, T[] all) { + private <T extends Enum<?>> T getListBox(ListBox f, T defaultValue, T[] all) { final int idx = f.getSelectedIndex(); if (0 <= idx) { String v = f.getValue(idx); @@ -412,6 +420,7 @@ p.legacycidInChangeTable(legacycidInChangeTable.getValue()); p.muteCommonPathPrefixes(muteCommonPathPrefixes.getValue()); p.signedOffBy(signedOffBy.getValue()); + p.publishCommentsOnPush(publishCommentsOnPush.getValue()); p.reviewCategoryStrategy( getListBox( reviewCategoryStrategy, ReviewCategoryStrategy.NONE, ReviewCategoryStrategy.values()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java index 9d67663..177fc09 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
@@ -91,7 +91,7 @@ display(); } - private void infoRow(final int row, final String name) { + private void infoRow(int row, String name) { info.setText(row, labelIdx, name); info.getCellFormatter().addStyleName(row, 0, Gerrit.RESOURCES.css().header()); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java index d3ac463..c99cd1a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
@@ -129,7 +129,7 @@ addNew.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { doAddNew(); } }); @@ -138,7 +138,7 @@ browse.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { int top = grid.getAbsoluteTop() - 50; // under page header // Try to place it to the right of everything else, but not // right justified @@ -158,7 +158,7 @@ delSel.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { watchesTab.deleteChecked(); } });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java index 5e45b68..0a61b2d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
@@ -97,7 +97,7 @@ return infos; } - public void insertWatch(final ProjectWatchInfo k) { + public void insertWatch(ProjectWatchInfo k) { final String newName = k.project(); int row = 1; for (; row < table.getRowCount(); row++) { @@ -112,7 +112,7 @@ populate(row, k); } - public void display(final JsArray<ProjectWatchInfo> result) { + public void display(JsArray<ProjectWatchInfo> result) { while (2 < table.getRowCount()) { table.removeRow(table.getRowCount() - 1); } @@ -125,7 +125,7 @@ } } - protected void populate(final int row, final ProjectWatchInfo info) { + protected void populate(int row, ProjectWatchInfo info) { final FlowPanel fp = new FlowPanel(); fp.add(new ProjectLink(info.project(), new Project.NameKey(info.project()))); if (info.filter() != null) { @@ -156,13 +156,13 @@ } protected void addNotifyButton( - final ProjectWatchInfo.Type type, final ProjectWatchInfo info, final int row, final int col) { + final ProjectWatchInfo.Type type, ProjectWatchInfo info, int row, int col) { final CheckBox cbox = new CheckBox(); cbox.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { final Boolean oldVal = info.notify(type); info.notify(type, cbox.getValue()); cbox.setEnabled(false);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java index afba2e2..7c90884 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
@@ -66,7 +66,7 @@ this(null); } - public NewAgreementScreen(final String token) { + public NewAgreementScreen(String token) { nextToken = token != null ? token : PageLinks.SETTINGS_AGREEMENTS; } @@ -122,7 +122,7 @@ submit.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { doSign(); } }); @@ -156,7 +156,7 @@ } radios.add(hdr); - for (final AgreementInfo cla : available) { + for (AgreementInfo cla : available) { final RadioButton r = new RadioButton("cla_id", cla.name()); r.addStyleName(Gerrit.RESOURCES.css().contributorAgreementButton()); radios.add(r); @@ -170,7 +170,7 @@ r.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { showCLA(cla); } });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java index d3d217c..29de14a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
@@ -31,7 +31,7 @@ public class RegisterScreen extends AccountScreen { private final String nextToken; - public RegisterScreen(final String next) { + public RegisterScreen(String next) { nextToken = next; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshHostKeyPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshHostKeyPanel.java index 70e3911..2dfc2ed 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshHostKeyPanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshHostKeyPanel.java
@@ -24,7 +24,7 @@ import com.google.gwtexpui.clippy.client.CopyableLabel; class SshHostKeyPanel extends Composite { - SshHostKeyPanel(final SshHostKey info) { + SshHostKeyPanel(SshHostKey info) { final FlowPanel body = new FlowPanel(); body.setStyleName(Gerrit.RESOURCES.css().sshHostKeyPanel()); body.add(new SmallHeading(Util.C.sshHostKeyTitle()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java index 0cf30de..6a8b44d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
@@ -68,7 +68,7 @@ showAddKeyBlock.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { showAddKeyBlock(true); } }); @@ -82,7 +82,7 @@ deleteKey.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { keys.deleteChecked(); } }); @@ -114,7 +114,7 @@ clearNew.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { addTxt.setText(""); addTxt.setFocus(true); } @@ -125,7 +125,7 @@ addNew.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { doAddNew(); } }); @@ -135,7 +135,7 @@ closeAddKeyBlock.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { showAddKeyBlock(false); } }); @@ -151,7 +151,7 @@ initWidget(body); } - void setKeyTableVisible(final boolean on) { + void setKeyTableVisible(boolean on) { keys.setVisible(on); deleteKey.setVisible(on); closeAddKeyBlock.setVisible(on); @@ -166,7 +166,7 @@ txt, new GerritCallback<SshKeyInfo>() { @Override - public void onSuccess(final SshKeyInfo k) { + public void onSuccess(SshKeyInfo k) { addNew.setEnabled(true); addTxt.setText(""); keys.addOneKey(k); @@ -178,7 +178,7 @@ } @Override - public void onFailure(final Throwable caught) { + public void onFailure(Throwable caught) { addNew.setEnabled(true); if (isInvalidSshKey(caught)) { @@ -189,7 +189,7 @@ } } - private boolean isInvalidSshKey(final Throwable caught) { + private boolean isInvalidSshKey(Throwable caught) { if (caught instanceof InvalidSshKeyException) { return true; } @@ -207,9 +207,9 @@ Gerrit.SYSTEM_SVC.daemonHostKeys( new GerritCallback<List<SshHostKey>>() { @Override - public void onSuccess(final List<SshHostKey> result) { + public void onSuccess(List<SshHostKey> result) { serverKeys.clear(); - for (final SshHostKey keyInfo : result) { + for (SshHostKey keyInfo : result) { serverKeys.add(new SshHostKeyPanel(keyInfo)); } if (++loadCount == 2) { @@ -238,7 +238,7 @@ void display() {} - private void showAddKeyBlock(final boolean show) { + private void showAddKeyBlock(boolean show) { showAddKeyBlock.setVisible(!show); addKeyBlock.setVisible(show); } @@ -312,7 +312,7 @@ } } - void display(final List<SshKeyInfo> result) { + void display(List<SshKeyInfo> result) { if (result.isEmpty()) { setKeyTableVisible(false); showAddKeyBlock(true); @@ -320,7 +320,7 @@ while (1 < table.getRowCount()) { table.removeRow(table.getRowCount() - 1); } - for (final SshKeyInfo k : result) { + for (SshKeyInfo k : result) { addOneKey(k); } setKeyTableVisible(true); @@ -328,7 +328,7 @@ } } - void addOneKey(final SshKeyInfo k) { + void addOneKey(SshKeyInfo k) { final FlexCellFormatter fmt = table.getFlexCellFormatter(); final int row = table.getRowCount(); table.insertRow(row); @@ -378,7 +378,7 @@ } } - static String elide(final String s, final int len) { + static String elide(String s, int len) { if (s == null || s.length() < len || len <= 10) { return s; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java index 839a3e6..a893c5a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
@@ -75,7 +75,7 @@ setUserName.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { confirmSetUserName(); } }); @@ -143,14 +143,14 @@ }); } - private void enableUI(final boolean on) { + private void enableUI(boolean on) { userNameTxt.setEnabled(on); setUserName.setEnabled(on); } private static final class UserNameValidator implements KeyPressHandler { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { final char code = event.getCharCode(); final int nativeCode = event.getNativeEvent().getKeyCode(); switch (nativeCode) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java index 990798c..b66f108 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
@@ -24,7 +24,7 @@ public class ValidateEmailScreen extends AccountScreen { private final String magicToken; - public ValidateEmailScreen(final String magicToken) { + public ValidateEmailScreen(String magicToken) { this.magicToken = magicToken; } @@ -41,7 +41,7 @@ magicToken, new ScreenLoadCallback<VoidResult>(this) { @Override - protected void preDisplay(final VoidResult result) {} + protected void preDisplay(VoidResult result) {} @Override protected void postDisplay() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java index 37813af..e518d26 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -204,7 +204,7 @@ } } - void setEditing(final boolean editing) { + void setEditing(boolean editing) { this.editing = editing; } @@ -236,7 +236,7 @@ } } - private void addPermission(final String permissionName, final List<String> permissionList) { + private void addPermission(String permissionName, List<String> permissionList) { if (value.getPermission(permissionName) != null) { return; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java index 4d1ad22..34a1ac9 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -48,7 +48,7 @@ private CheckBox visibleToAllCheckBox; private Button saveGroupOptions; - public AccountGroupInfoScreen(final GroupInfo toShow, final String token) { + public AccountGroupInfoScreen(GroupInfo toShow, String token) { super(toShow, token); } @@ -62,7 +62,7 @@ initGroupOptions(); } - private void enableForm(final boolean canModify) { + private void enableForm(boolean canModify) { groupNameTxt.setEnabled(canModify); ownerTxt.setEnabled(canModify); descTxt.setEnabled(canModify); @@ -91,14 +91,14 @@ saveName.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { final String newName = groupNameTxt.getText().trim(); GroupApi.renameGroup( getGroupUUID(), newName, new GerritCallback<com.google.gerrit.client.VoidResult>() { @Override - public void onSuccess(final com.google.gerrit.client.VoidResult result) { + public void onSuccess(com.google.gerrit.client.VoidResult result) { saveName.setEnabled(false); setPageTitle(AdminMessages.I.group(newName)); groupNameTxt.setText(newName); @@ -129,7 +129,7 @@ saveOwner.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { final String newOwner = ownerTxt.getText().trim(); if (newOwner.length() > 0) { AccountGroup.UUID ownerUuid = accountGroupOracle.getUUID(newOwner); @@ -139,7 +139,7 @@ ownerId, new GerritCallback<GroupInfo>() { @Override - public void onSuccess(final GroupInfo result) { + public void onSuccess(GroupInfo result) { updateOwnerGroup(result); saveOwner.setEnabled(false); } @@ -166,14 +166,14 @@ saveDesc.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { final String txt = descTxt.getText().trim(); GroupApi.setGroupDescription( getGroupUUID(), txt, new GerritCallback<VoidResult>() { @Override - public void onSuccess(final VoidResult result) { + public void onSuccess(VoidResult result) { saveDesc.setEnabled(false); } }); @@ -199,13 +199,13 @@ saveGroupOptions.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { GroupApi.setGroupOptions( getGroupUUID(), visibleToAllCheckBox.getValue(), new GerritCallback<VoidResult>() { @Override - public void onSuccess(final VoidResult result) { + public void onSuccess(VoidResult result) { saveGroupOptions.setEnabled(false); } }); @@ -220,7 +220,7 @@ } @Override - protected void display(final GroupInfo group, final boolean canModify) { + protected void display(GroupInfo group, boolean canModify) { groupUUIDLabel.setText(group.getGroupUUID().get()); groupNameTxt.setText(group.name()); ownerTxt.setText(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java index 51b4979..2614224 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -59,7 +59,7 @@ private FlowPanel noMembersInfo; private AccountGroupSuggestOracle accountGroupSuggestOracle; - public AccountGroupMembersScreen(final GroupInfo toShow, final String token) { + public AccountGroupMembersScreen(GroupInfo toShow, String token) { super(toShow, token); } @@ -71,7 +71,7 @@ initNoMembersInfo(); } - private void enableForm(final boolean canModify) { + private void enableForm(boolean canModify) { addMemberBox.setEnabled(canModify); members.setEnabled(canModify); addIncludeBox.setEnabled(canModify); @@ -88,7 +88,7 @@ addMemberBox.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { doAddNewMember(); } }); @@ -100,7 +100,7 @@ delMember.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { members.deleteChecked(); } }); @@ -124,7 +124,7 @@ addIncludeBox.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { doAddNewInclude(); } }); @@ -136,7 +136,7 @@ delInclude.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { includes.deleteChecked(); } }); @@ -157,7 +157,7 @@ } @Override - protected void display(final GroupInfo group, final boolean canModify) { + protected void display(GroupInfo group, boolean canModify) { if (AccountGroup.isInternalGroup(group.getGroupUUID())) { members.display(Natives.asList(group.members())); includes.display(Natives.asList(group.includes())); @@ -184,14 +184,14 @@ nameEmail, new GerritCallback<AccountInfo>() { @Override - public void onSuccess(final AccountInfo memberInfo) { + public void onSuccess(AccountInfo memberInfo) { addMemberBox.setEnabled(true); addMemberBox.setText(""); members.insert(memberInfo); } @Override - public void onFailure(final Throwable caught) { + public void onFailure(Throwable caught) { addMemberBox.setEnabled(true); super.onFailure(caught); } @@ -215,14 +215,14 @@ uuid.get(), new GerritCallback<GroupInfo>() { @Override - public void onSuccess(final GroupInfo result) { + public void onSuccess(GroupInfo result) { addIncludeBox.setEnabled(true); addIncludeBox.setText(""); includes.insert(result); } @Override - public void onFailure(final Throwable caught) { + public void onFailure(Throwable caught) { addIncludeBox.setEnabled(true); super.onFailure(caught); } @@ -242,7 +242,7 @@ fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader()); } - void setEnabled(final boolean enabled) { + void setEnabled(boolean enabled) { this.enabled = enabled; for (int row = 1; row < table.getRowCount(); row++) { final AccountInfo i = getRowItem(row); @@ -266,7 +266,7 @@ ids, new GerritCallback<VoidResult>() { @Override - public void onSuccess(final VoidResult result) { + public void onSuccess(VoidResult result) { for (int row = 1; row < table.getRowCount(); ) { final AccountInfo i = getRowItem(row); if (i != null && ids.contains(i._accountId())) { @@ -280,12 +280,12 @@ } } - void display(final List<AccountInfo> result) { + void display(List<AccountInfo> result) { while (1 < table.getRowCount()) { table.removeRow(table.getRowCount() - 1); } - for (final AccountInfo i : result) { + for (AccountInfo i : result) { final int row = table.getRowCount(); table.insertRow(row); applyDataRowStyle(row); @@ -323,7 +323,7 @@ } } - void populate(final int row, final AccountInfo i) { + void populate(int row, AccountInfo i) { CheckBox checkBox = new CheckBox(); table.setWidget(row, 1, checkBox); checkBox.setEnabled(enabled); @@ -352,7 +352,7 @@ fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader()); } - void setEnabled(final boolean enabled) { + void setEnabled(boolean enabled) { this.enabled = enabled; for (int row = 1; row < table.getRowCount(); row++) { final GroupInfo i = getRowItem(row); @@ -376,7 +376,7 @@ ids, new GerritCallback<VoidResult>() { @Override - public void onSuccess(final VoidResult result) { + public void onSuccess(VoidResult result) { for (int row = 1; row < table.getRowCount(); ) { final GroupInfo i = getRowItem(row); if (i != null && ids.contains(i.getGroupUUID())) { @@ -395,7 +395,7 @@ table.removeRow(table.getRowCount() - 1); } - for (final GroupInfo i : list) { + for (GroupInfo i : list) { final int row = table.getRowCount(); table.insertRow(row); applyDataRowStyle(row); @@ -427,7 +427,7 @@ } } - void populate(final int row, final GroupInfo i) { + void populate(int row, GroupInfo i) { final FlexCellFormatter fmt = table.getFlexCellFormatter(); AccountGroup.UUID uuid = i.getGroupUUID();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java index 29b7677..b67213b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
@@ -32,7 +32,7 @@ private final String membersTabToken; private final String auditLogTabToken; - public AccountGroupScreen(final GroupInfo toShow, final String token) { + public AccountGroupScreen(GroupInfo toShow, String token) { setRequiresSignIn(true); this.group = toShow; @@ -47,7 +47,7 @@ AccountGroup.isInternalGroup(group.getGroupUUID())); } - private String getTabToken(final String token, final String tab) { + private String getTabToken(String token, String tab) { if (token.startsWith("/admin/groups/uuid-")) { return toGroup(group.getGroupUUID(), tab); } @@ -91,7 +91,7 @@ return group.getOwnerUUID(); } - protected void setMembersTabVisible(final boolean visible) { + protected void setMembersTabVisible(boolean visible) { setLinkVisible(membersTabToken, visible); } }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java index cc6a136..8f5f415 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -75,6 +75,10 @@ String rejectImplicitMerges(); + String enableReviewerByEmail(); + + String matchAuthorToCommitterDate(); + String headingMaxObjectSizeLimit(); String headingGroupOptions();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties index 1588ea6..60cadfb 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -37,6 +37,8 @@ headingParentProjectName = Rights Inherit From parentSuggestions = Parent Suggestion columnProjectName = Project Name +enableReviewerByEmail = Enable adding unregistered users as reviewers and CCs on changes +matchAuthorToCommitterDate = Match authored date with committer date upon submit headingGroupUUID = Group UUID headingOwner = Owners @@ -149,7 +151,8 @@ removeReviewer, \ submit, \ submitAs, \ - viewDrafts + viewDrafts, \ + viewPrivateChanges abandon = Abandon addPatchSet = Add Patch Set @@ -175,6 +178,7 @@ submit = Submit submitAs = Submit (On Behalf Of) viewDrafts = View Drafts +viewPrivateChanges = View Private Changes refErrorEmpty = Reference must be supplied refErrorBeginSlash = Reference must not start with '/'
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java index 2e5bbb5..eeacd97 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateChangeAction.java
@@ -26,7 +26,7 @@ import com.google.gwt.user.client.ui.PopupPanel; class CreateChangeAction { - static void call(final Button b, final String project) { + static void call(Button b, String project) { // TODO Replace CreateChangeDialog with a nicer looking display. b.setEnabled(false); new CreateChangeDialog(new Project.NameKey(project)) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java index 457e179..6914ee9 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
@@ -74,6 +74,14 @@ addCreateGroupPanel(); } + @Override + public void onShowView() { + super.onShowView(); + if (addTxt != null) { + addTxt.setFocus(true); + } + } + private void addCreateGroupPanel() { VerticalPanel addPanel = new VerticalPanel(); addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel()); @@ -117,7 +125,7 @@ addNew.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { doCreateGroup(); } }); @@ -138,7 +146,7 @@ newName, new GerritCallback<GroupInfo>() { @Override - public void onSuccess(final GroupInfo result) { + public void onSuccess(GroupInfo result) { History.newItem(Dispatcher.toGroup(result.getGroupId(), AccountGroupScreen.MEMBERS)); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java index 092c6e1..a73d78e 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
@@ -111,6 +111,14 @@ projectsPopup.initPopup(AdminConstants.I.projects(), PageLinks.ADMIN_PROJECTS); } + @Override + public void onShowView() { + super.onShowView(); + if (project != null) { + project.setFocus(true); + } + } + private void addCreateProjectPanel() { final VerticalPanel fp = new VerticalPanel(); fp.setStyleName(Gerrit.RESOURCES.css().createProjectPanel()); @@ -173,7 +181,7 @@ create.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { doCreateProject(); } }); @@ -182,7 +190,7 @@ browse.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { int top = grid.getAbsoluteTop() - 50; // under page header // Try to place it to the right of everything else, but not // right justified @@ -211,7 +219,7 @@ } @Override - protected void populate(final int row, final ProjectInfo k) { + protected void populate(int row, ProjectInfo k) { populateState(row, k); final Anchor projectLink = new Anchor(k.name()); projectLink.addClickHandler( @@ -244,7 +252,7 @@ }); } - private void addGrid(final VerticalPanel fp) { + private void addGrid(VerticalPanel fp) { grid = new Grid(2, 3); grid.setStyleName(Gerrit.RESOURCES.css().infoBlock()); grid.setText(0, 0, AdminConstants.I.columnProjectName() + ":"); @@ -287,7 +295,7 @@ }); } - private void enableForm(final boolean enabled) { + private void enableForm(boolean enabled) { project.setEnabled(enabled); create.setEnabled(enabled); parent.setEnabled(enabled);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java index d28e9bb..47842f5 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/EditConfigAction.java
@@ -24,7 +24,7 @@ import com.google.gwt.user.client.ui.Button; public class EditConfigAction { - static void call(final Button b, final String project) { + static void call(Button b, String project) { b.setEnabled(false); ChangeApi.createChange(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java index 0f5bf22..259847e 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
@@ -43,7 +43,7 @@ this(null); } - public GroupTable(final String pointerId) { + public GroupTable(String pointerId) { super(AdminConstants.I.groupItemHelp()); setSavePointerId(pointerId); @@ -70,12 +70,12 @@ } @Override - protected Object getRowItemKey(final GroupInfo item) { + protected Object getRowItemKey(GroupInfo item) { return item.getGroupId(); } @Override - protected void onOpenRow(final int row) { + protected void onOpenRow(int row) { GroupInfo groupInfo = getRowItem(row); if (isInteralGroup(groupInfo)) { History.newItem(Dispatcher.toGroup(groupInfo.getGroupId())); @@ -121,7 +121,7 @@ } } - void populate(final int row, final GroupInfo k, final String toHighlight) { + void populate(int row, GroupInfo k, String toHighlight) { if (k.url() != null) { if (isInteralGroup(k)) { table.setWidget( @@ -152,7 +152,7 @@ setRowItem(row, k); } - private boolean isInteralGroup(final GroupInfo groupInfo) { + private boolean isInteralGroup(GroupInfo groupInfo) { return groupInfo != null && groupInfo.url().startsWith("#" + PageLinks.ADMIN_GROUPS); } }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java index d254c7d..79a4cef 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -205,7 +205,7 @@ addStage2.getStyle().setDisplay(Display.NONE); } - private void addGroup(final GroupReference ref) { + private void addGroup(GroupReference ref) { if (ref.getUUID() != null) { if (value.getRule(ref) == null) { PermissionRule newRule = value.getRule(ref, true);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java index 8a70f2e..381c644 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
@@ -45,7 +45,7 @@ PluginMap.all( new ScreenLoadCallback<PluginMap>(this) { @Override - protected void preDisplay(final PluginMap result) { + protected void preDisplay(PluginMap result) { pluginTable.display(result); } }); @@ -75,12 +75,12 @@ fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader()); } - void display(final PluginMap plugins) { + void display(PluginMap plugins) { while (1 < table.getRowCount()) { table.removeRow(table.getRowCount() - 1); } - for (final PluginInfo p : Natives.asList(plugins.values())) { + for (PluginInfo p : Natives.asList(plugins.values())) { final int row = table.getRowCount(); table.insertRow(row); applyDataRowStyle(row); @@ -88,7 +88,7 @@ } } - void populate(final int row, final PluginInfo plugin) { + void populate(int row, PluginInfo plugin) { if (plugin.disabled() || plugin.indexUrl() == null) { table.setText(row, 1, plugin.name()); } else {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java index 05142c4..a52ea60 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
@@ -134,7 +134,7 @@ @Override public void setDelegate(EditorDelegate<ProjectAccess> delegate) {} - void setEditing(final boolean editing) { + void setEditing(boolean editing) { this.editing = editing; addSection.setVisible(editing); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java index 0398e9d..8f83e2f 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -91,7 +91,7 @@ private NativeMap<CapabilityInfo> capabilityMap; - public ProjectAccessScreen(final Project.NameKey toShow) { + public ProjectAccessScreen(Project.NameKey toShow) { super(toShow); } @@ -211,7 +211,7 @@ displayReadOnly(newAccess); } else { error.add(new Label(Gerrit.C.projectAccessError())); - for (final String diff : diffs) { + for (String diff : diffs) { error.add(new Label(diff)); } if (access.canUpload()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java index 887e4b8..c6a391b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -82,7 +82,7 @@ private NpTextBox filterTxt; private Query query; - public ProjectBranchesScreen(final Project.NameKey toShow) { + public ProjectBranchesScreen(Project.NameKey toShow) { super(toShow); } @@ -165,7 +165,7 @@ addBranch.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { doAddNewBranch(); } }); @@ -179,7 +179,7 @@ delBranch.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { branchTable.deleteChecked(); } }); @@ -283,7 +283,7 @@ new ConfirmationCallback() { @Override public void onOk() { - //do nothing + // do nothing } }); confirmationDialog.center(); @@ -384,7 +384,7 @@ confirmationDialog.center(); } - private void deleteBranches(final Set<String> branches) { + private void deleteBranches(Set<String> branches) { ProjectApi.deleteBranches( getProjectKey(), branches, @@ -473,7 +473,7 @@ setRowItem(row, k); } - private void setHeadRevision(final int row, final int column, final String rev) { + private void setHeadRevision(int row, int column, String rev) { AccessMap.get( getProjectKey(), new GerritCallback<ProjectAccessInfo>() { @@ -488,7 +488,7 @@ }); } - private Widget getHeadRevisionWidget(final String headRevision) { + private Widget getHeadRevisionWidget(String headRevision) { FlowPanel p = new FlowPanel(); final InlineLabel l = new InlineLabel(headRevision); final Image edit = new Image(Gerrit.RESOURCES.edit());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectDashboardsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectDashboardsScreen.java index 52fe3399..7b5d04d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectDashboardsScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectDashboardsScreen.java
@@ -25,7 +25,7 @@ private DashboardsTable dashes; Project.NameKey project; - public ProjectDashboardsScreen(final Project.NameKey project) { + public ProjectDashboardsScreen(Project.NameKey project) { super(project); this.project = project; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java index 3645fb9..37ecec8 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -86,6 +86,8 @@ private ListBox enableSignedPush; private ListBox requireSignedPush; private ListBox rejectImplicitMerges; + private ListBox enableReviewerByEmail; + private ListBox matchAuthorToCommitterDate; private NpTextBox maxObjectSizeLimit; private Label effectiveMaxObjectSizeLimit; private Map<String, Map<String, HasEnabled>> pluginConfigWidgets; @@ -99,7 +101,7 @@ private OnEditEnabler saveEnabler; - public ProjectInfoScreen(final Project.NameKey toShow) { + public ProjectInfoScreen(Project.NameKey toShow) { super(toShow); } @@ -191,6 +193,8 @@ requireChangeID.setEnabled(isOwner); rejectImplicitMerges.setEnabled(isOwner); maxObjectSizeLimit.setEnabled(isOwner); + enableReviewerByEmail.setEnabled(isOwner); + matchAuthorToCommitterDate.setEnabled(isOwner); if (pluginConfigWidgets != null) { for (Map<String, HasEnabled> widgetMap : pluginConfigWidgets.values()) { @@ -226,7 +230,7 @@ grid.add(AdminConstants.I.headingProjectState(), state); submitType = new ListBox(); - for (final SubmitType type : SubmitType.values()) { + for (SubmitType type : SubmitType.values()) { submitType.addItem(Util.toLongString(type), type.name()); } submitType.addChangeHandler( @@ -264,6 +268,14 @@ saveEnabler.listenTo(rejectImplicitMerges); grid.addHtml(AdminConstants.I.rejectImplicitMerges(), rejectImplicitMerges); + enableReviewerByEmail = newInheritedBooleanBox(); + saveEnabler.listenTo(enableReviewerByEmail); + grid.addHtml(AdminConstants.I.enableReviewerByEmail(), enableReviewerByEmail); + + matchAuthorToCommitterDate = newInheritedBooleanBox(); + saveEnabler.listenTo(matchAuthorToCommitterDate); + grid.addHtml(AdminConstants.I.matchAuthorToCommitterDate(), matchAuthorToCommitterDate); + maxObjectSizeLimit = new NpTextBox(); saveEnabler.listenTo(maxObjectSizeLimit); effectiveMaxObjectSizeLimit = new Label(); @@ -314,7 +326,7 @@ grid.addHtml(AdminConstants.I.useSignedOffBy(), signedOffBy); } - private void setSubmitType(final SubmitType newSubmitType) { + private void setSubmitType(SubmitType newSubmitType) { int index = -1; if (submitType != null) { for (int i = 0; i < submitType.getItemCount(); i++) { @@ -328,7 +340,7 @@ } } - private void setState(final ProjectState newState) { + private void setState(ProjectState newState) { if (state != null) { for (int i = 0; i < state.getItemCount(); i++) { if (newState.name().equals(state.getValue(i))) { @@ -395,6 +407,8 @@ setBool(requireSignedPush, result.requireSignedPush()); } setBool(rejectImplicitMerges, result.rejectImplicitMerges()); + setBool(enableReviewerByEmail, result.enableReviewerByEmail()); + setBool(matchAuthorToCommitterDate, result.matchAuthorToCommitterDate()); setSubmitType(result.submitType()); setState(result.state()); maxObjectSizeLimit.setText(result.maxObjectSizeLimit().configuredValue()); @@ -665,6 +679,8 @@ esp, rsp, getBool(rejectImplicitMerges), + getBool(enableReviewerByEmail), + getBool(matchAuthorToCommitterDate), maxObjectSizeLimit.getText().trim(), SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())), ProjectState.valueOf(state.getValue(state.getSelectedIndex())),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java index 9166c56..2a03136 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -91,11 +91,11 @@ } @Override - protected void onOpenRow(final int row) { + protected void onOpenRow(int row) { History.newItem(link(getRowItem(row))); } - private String link(final ProjectInfo item) { + private String link(ProjectInfo item) { return Dispatcher.toProject(item.name_key()); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java index 3328163..dc964b8 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
@@ -37,7 +37,7 @@ private final Project.NameKey name; - public ProjectScreen(final Project.NameKey toShow) { + public ProjectScreen(Project.NameKey toShow) { name = toShow; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java index f38d36c..57a6c3c 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectTagsScreen.java
@@ -276,7 +276,7 @@ new ConfirmationCallback() { @Override public void onOk() { - //do nothing + // do nothing } }); confirmationDialog.center(); @@ -376,7 +376,7 @@ confirmationDialog.center(); } - private void deleteTags(final Set<String> tags) { + private void deleteTags(Set<String> tags) { ProjectApi.deleteTags( getProjectKey(), tags,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java index f08cdd8..2e4926d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
@@ -30,7 +30,7 @@ AdminResources.I.css().ensureInjected(); } - public static String toLongString(final SubmitType type) { + public static String toLongString(SubmitType type) { if (type == null) { return ""; } @@ -52,7 +52,7 @@ } } - public static String toLongString(final ProjectState type) { + public static String toLongString(ProjectState type) { if (type == null) { return ""; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java index 7e1db46..cf8de54 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ActionContext.java
@@ -176,7 +176,7 @@ * The same as {@link #get(RestApi, JavaScriptObject)} but without converting a {@link * NativeString} result to String. */ - static final void getRaw(RestApi api, final JavaScriptObject cb) { + static final void getRaw(RestApi api, JavaScriptObject cb) { api.get(wrapRaw(cb)); } @@ -268,7 +268,7 @@ api.delete(wrapRaw(cb)); } - private static GerritCallback<JavaScriptObject> wrap(final JavaScriptObject cb) { + private static GerritCallback<JavaScriptObject> wrap(JavaScriptObject cb) { return new GerritCallback<JavaScriptObject>() { @Override public void onSuccess(JavaScriptObject result) { @@ -282,7 +282,7 @@ }; } - private static GerritCallback<JavaScriptObject> wrapRaw(final JavaScriptObject cb) { + private static GerritCallback<JavaScriptObject> wrapRaw(JavaScriptObject cb) { return new GerritCallback<JavaScriptObject>() { @Override public void onSuccess(JavaScriptObject result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java index 1555f56..294fa9b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ApiGlue.java
@@ -100,9 +100,9 @@ var s = new SettingsScreenDefinition(p,m,c); (this.settingsScreens[n] || (this.settingsScreens[n]=[])).push(s); }, - panel: function(i,c){this._panel(this.getPluginName(),i,c)}, - _panel: function(n,i,c){ - var p = new PanelDefinition(n,c); + panel: function(i,c,n){this._panel(this.getPluginName(),i,c,n)}, + _panel: function(n,i,c,x){ + var p = new PanelDefinition(n,c,x); (this.panels[i] || (this.panels[i]=[])).push(p); },
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java index 74668c1..da14412 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
@@ -36,11 +36,16 @@ invoke(action, api, callback(PageLinks.toProject(project))); } - private static AsyncCallback<JavaScriptObject> callback(final String target) { + private static AsyncCallback<JavaScriptObject> callback(String target) { return new GerritCallback<JavaScriptObject>() { @Override public void onSuccess(JavaScriptObject in) { UiResult result = asUiResult(in); + if (result == null) { + Gerrit.display(target); + return; + } + if (result.alert() != null) { Window.alert(result.alert()); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java index 0873363..6d3dd60 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/ExtensionPanel.java
@@ -22,7 +22,10 @@ import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.SimplePanel; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -32,13 +35,17 @@ private final List<Context> contexts; public ExtensionPanel(GerritUiExtensionPoint extensionPoint) { - this.extensionPoint = extensionPoint; - this.contexts = create(); + this(extensionPoint, new ArrayList<String>()); } - private List<Context> create() { + public ExtensionPanel(GerritUiExtensionPoint extensionPoint, List<String> panelNames) { + this.extensionPoint = extensionPoint; + this.contexts = create(panelNames); + } + + private List<Context> create(List<String> panelNames) { List<Context> contexts = new ArrayList<>(); - for (Definition def : Natives.asList(Definition.get(extensionPoint.name()))) { + for (Definition def : getOrderedDefs(panelNames)) { SimplePanel p = new SimplePanel(); add(p); contexts.add(Context.create(def, p)); @@ -46,6 +53,42 @@ return contexts; } + private List<Definition> getOrderedDefs(List<String> panelNames) { + if (panelNames == null) { + panelNames = Collections.emptyList(); + } + Map<String, List<Definition>> defsOrderedByName = new LinkedHashMap<>(); + for (String name : panelNames) { + defsOrderedByName.put(name, new ArrayList<Definition>()); + } + for (Definition def : Natives.asList(Definition.get(extensionPoint.name()))) { + addDef(def, defsOrderedByName); + } + List<Definition> orderedDefs = new ArrayList<>(); + for (List<Definition> defList : defsOrderedByName.values()) { + orderedDefs.addAll(defList); + } + return orderedDefs; + } + + private static void addDef(Definition def, Map<String, List<Definition>> defsOrderedByName) { + String panelName = def.getPanelName(); + if (panelName.equals(def.getPluginName() + ".undefined")) { + /* Handle a partially undefined panel name from the + javascript layer by generating a random panel name. + This maintains support for panels that do not provide a name. */ + panelName = + def.getPluginName() + "." + Long.toHexString(Double.doubleToLongBits(Math.random())); + } + if (defsOrderedByName.containsKey(panelName)) { + defsOrderedByName.get(panelName).add(def); + } else if (defsOrderedByName.containsKey(def.getPluginName())) { + defsOrderedByName.get(def.getPluginName()).add(def); + } else { + defsOrderedByName.put(panelName, Collections.singletonList(def)); + } + } + public void put(GerritUiExtensionPoint.Key key, String value) { for (Context ctx : contexts) { ctx.put(key.name(), value); @@ -103,9 +146,10 @@ static final JavaScriptObject TYPE = init(); private static native JavaScriptObject init() /*-{ - function PanelDefinition(n, c) { + function PanelDefinition(n, c, x) { this.pluginName = n; this.onLoad = c; + this.name = x; }; return PanelDefinition; }-*/; @@ -113,6 +157,10 @@ static native JsArray<Definition> get(String i) /*-{ return $wnd.Gerrit.panels[i] || [] }-*/; protected Definition() {} + + public final native String getPanelName() /*-{ return this.pluginName + "." + this.name; }-*/; + + public final native String getPluginName() /*-{ return this.pluginName; }-*/; } static class Context extends JavaScriptObject {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java index 29787b8..48a812c1 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/Plugin.java
@@ -68,7 +68,7 @@ onAction: function(t,n,c){G._onAction(this.name,t,n,c)}, screen: function(p,c){G._screen(this.name,p,c)}, settingsScreen: function(p,m,c){G._settingsScreen(this.name,p,m,c)}, - panel: function(i,c){G._panel(this.name,i,c)}, + panel: function(i,c,n){G._panel(this.name,i,c,n)}, url: function (u){return G.url(this._url(u))}, get: function(u,b){@com.google.gerrit.client.api.ActionContext::get(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java index 1c59dac..8a479dd 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/PluginLoader.java
@@ -28,6 +28,7 @@ import com.google.gwt.user.client.ui.DialogBox; import com.google.gwtexpui.progress.client.ProgressBar; import java.util.List; +import java.util.stream.Collectors; /** Loads JavaScript plugins with a progress meter visible. */ public class PluginLoader extends DialogBox { @@ -38,10 +39,15 @@ if (plugins == null || plugins.isEmpty()) { callback.onSuccess(VoidResult.create()); } else { - self = new PluginLoader(loadTimeout, callback); - self.load(plugins); - self.startTimers(); - self.center(); + plugins = plugins.stream().filter(p -> p.endsWith(".js")).collect(Collectors.toList()); + if (plugins.isEmpty()) { + callback.onSuccess(VoidResult.create()); + } else { + self = new PluginLoader(loadTimeout, callback); + self.load(plugins); + self.startTimers(); + self.center(); + } } }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java index ada28af..b22b79f 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -48,6 +48,7 @@ "revert", "submit", "topic", + "private", "/", }; @@ -65,6 +66,9 @@ @UiField Button deleteChange; + @UiField Button markPrivate; + @UiField Button unmarkPrivate; + @UiField Button restore; private RestoreAction restoreAction; @@ -122,6 +126,11 @@ a2b(actions, "restore", restore); a2b(actions, "revert", revert); a2b(actions, "followup", followUp); + if (info.isPrivate()) { + a2b(actions, "private", unmarkPrivate); + } else { + a2b(actions, "private", markPrivate); + } for (String id : filterNonCore(actions)) { add(new ActionButton(info, actions.get(id))); } @@ -192,6 +201,16 @@ } } + @UiHandler("markPrivate") + void onMarkPrivate(@SuppressWarnings("unused") ClickEvent e) { + ChangeActions.markPrivate(changeId, markPrivate); + } + + @UiHandler("unmarkPrivate") + void onUnmarkPrivate(@SuppressWarnings("unused") ClickEvent e) { + ChangeActions.unmarkPrivate(changeId, unmarkPrivate); + } + @UiHandler("restore") void onRestore(@SuppressWarnings("unused") ClickEvent e) { if (restoreAction == null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml index d0e5c3e..60efc8c 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
@@ -81,6 +81,12 @@ <g:Button ui:field='followUp' styleName='' visible='false'> <div><ui:msg>Follow-Up</ui:msg></div> </g:Button> + <g:Button ui:field='markPrivate' styleName='' visible='false'> + <div><ui:msg>Mark Private</ui:msg></div> + </g:Button> + <g:Button ui:field='unmarkPrivate' styleName='' visible='false'> + <div><ui:msg>Unmark Private</ui:msg></div> + </g:Button> <g:Button ui:field='submit' styleName='{style.submit}' visible='false'/> </g:FlowPanel>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java index c7f6fae..9050303 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
@@ -140,7 +140,7 @@ onCloseForm(); } - private void editAssignee(final String assignee) { + private void editAssignee(String assignee) { if (assignee.trim().isEmpty()) { ChangeApi.deleteAssignee( changeId.get(),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeActions.java index 1be60cc..d6f67a6 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeActions.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeActions.java
@@ -37,8 +37,15 @@ ChangeApi.deleteChange(id.get(), mine(draftButtons)); } - public static GerritCallback<JavaScriptObject> cs( - final Change.Id id, final Button... draftButtons) { + static void markPrivate(Change.Id id, Button... draftButtons) { + ChangeApi.markPrivate(id.get(), cs(id, draftButtons)); + } + + static void unmarkPrivate(Change.Id id, Button... draftButtons) { + ChangeApi.unmarkPrivate(id.get(), cs(id, draftButtons)); + } + + public static GerritCallback<JavaScriptObject> cs(final Change.Id id, Button... draftButtons) { setEnabled(false, draftButtons); return new GerritCallback<JavaScriptObject>() { @Override @@ -59,7 +66,7 @@ }; } - private static AsyncCallback<JavaScriptObject> mine(final Button... draftButtons) { + private static AsyncCallback<JavaScriptObject> mine(Button... draftButtons) { setEnabled(false, draftButtons); return new GerritCallback<JavaScriptObject>() { @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java index 8b699da..f3a0757 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.java
@@ -198,6 +198,8 @@ @UiField InlineLabel uploaderName; @UiField Element statusText; + @UiField Element privateText; + @UiField Element wipText; @UiField Image projectSettings; @UiField AnchorElement projectSettingsLink; @UiField InlineHyperlink projectDashboard; @@ -306,10 +308,9 @@ group.addFinal( new GerritCallback<ChangeInfo>() { @Override - public void onSuccess(final ChangeInfo info) { + public void onSuccess(ChangeInfo info) { info.init(); - addExtensionPoints(info, initCurrentRevision(info)); - + initCurrentRevision(info); final RevisionInfo rev = info.revision(revision); CallbackGroup group = new CallbackGroup(); loadCommit(rev, group); @@ -378,7 +379,7 @@ return resolveRevisionToDisplay(info); } - private void addExtensionPoints(ChangeInfo change, RevisionInfo rev) { + private void addExtensionPoints(ChangeInfo change, RevisionInfo rev, Entry result) { addExtensionPoint(GerritUiExtensionPoint.CHANGE_SCREEN_HEADER, headerExtension, change, rev); addExtensionPoint( GerritUiExtensionPoint.CHANGE_SCREEN_HEADER_RIGHT_OF_BUTTONS, @@ -391,7 +392,12 @@ change, rev); addExtensionPoint( - GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK, changeExtension, change, rev); + GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK, + changeExtension, + change, + rev, + result.getExtensionPanelNames( + GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK.toString())); addExtensionPoint( GerritUiExtensionPoint.CHANGE_SCREEN_BELOW_RELATED_INFO_BLOCK, relatedExtension, @@ -407,13 +413,22 @@ } private void addExtensionPoint( - GerritUiExtensionPoint extensionPoint, Panel p, ChangeInfo change, RevisionInfo rev) { - ExtensionPanel extensionPanel = new ExtensionPanel(extensionPoint); + GerritUiExtensionPoint extensionPoint, + Panel p, + ChangeInfo change, + RevisionInfo rev, + List<String> panelNames) { + ExtensionPanel extensionPanel = new ExtensionPanel(extensionPoint, panelNames); extensionPanel.putObject(GerritUiExtensionPoint.Key.CHANGE_INFO, change); extensionPanel.putObject(GerritUiExtensionPoint.Key.REVISION_INFO, rev); p.add(extensionPanel); } + private void addExtensionPoint( + GerritUiExtensionPoint extensionPoint, Panel p, ChangeInfo change, RevisionInfo rev) { + addExtensionPoint(extensionPoint, p, change, rev, Collections.emptyList()); + } + private boolean enableSignedPush() { return Gerrit.info().receive().enableSignedPush(); } @@ -571,7 +586,7 @@ downloadAction = new DownloadAction(info, revision, style, headerLine, download); } - private void initProjectLinks(final ChangeInfo info) { + private void initProjectLinks(ChangeInfo info) { projectSettingsLink.setHref("#" + PageLinks.toProject(info.projectNameKey())); projectSettings.addDomHandler( new ClickHandler() { @@ -972,7 +987,7 @@ } } - private void loadConfigInfo(final ChangeInfo info, DiffObject base) { + private void loadConfigInfo(ChangeInfo info, DiffObject base) { final RevisionInfo rev = info.revision(revision); if (base.isAutoMerge() && !initCurrentRevision(info).isMerge()) { Gerrit.display(getToken(), new NotFoundScreen()); @@ -1011,7 +1026,7 @@ group.done(); } - private void loadConfigInfo(final ChangeInfo info, RevisionInfo rev) { + private void loadConfigInfo(ChangeInfo info, RevisionInfo rev) { if (loaded) { return; } @@ -1030,6 +1045,14 @@ loadRevisionInfo(); } }); + ConfigInfoCache.get( + info.projectNameKey(), + new GerritCallback<Entry>() { + @Override + public void onSuccess(Entry entry) { + addExtensionPoints(info, rev, entry); + } + }); } private void updateToken(ChangeInfo info, DiffObject base, RevisionInfo rev) { @@ -1184,7 +1207,7 @@ return r; } - private void loadCommit(final RevisionInfo rev, CallbackGroup group) { + private void loadCommit(RevisionInfo rev, CallbackGroup group) { if (rev.isEdit() || rev.commit() != null) { return; } @@ -1366,6 +1389,14 @@ statusText.setInnerText(Util.toLongString(s)); } + if (info.isPrivate()) { + privateText.setInnerText(Util.C.isPrivate()); + } + + if (info.isWorkInProgress()) { + wipText.setInnerText(Util.C.isWorkInProgress()); + } + if (Gerrit.isSignedIn()) { replyAction = new ReplyAction(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml index 152b157..09bdc24 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeScreen.ui.xml
@@ -99,6 +99,13 @@ .statusText { font-weight: bold; } + .privateText { + font-weight: bold; + } + + .wipText { + font-weight: bold; + } div.popdown { display: inline-block; @@ -376,7 +383,9 @@ <span class='{style.changeId}'> <ui:msg>Change <g:Anchor ui:field='permalink' title='Reload the change (Shortcut: R)'> <ui:attribute name='title'/> - </g:Anchor> - <span ui:field='statusText' class='{style.statusText}'/></ui:msg> + </g:Anchor> - <span ui:field='statusText' class='{style.statusText}'/> + <span ui:field='privateText' class='{style.privateText}'/> + <span ui:field='wipText' class='{style.wipText}'/></ui:msg> </span> <g:SimplePanel ui:field='headerExtension' styleName='{style.headerExtension}'/> </div>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java index 97abddb..907691e 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/EditActions.java
@@ -36,8 +36,7 @@ ChangeApi.rebaseEdit(id.get(), cs(id, editButtons)); } - public static GerritCallback<JavaScriptObject> cs( - final Change.Id id, final Button... editButtons) { + public static GerritCallback<JavaScriptObject> cs(final Change.Id id, Button... editButtons) { setEnabled(false, editButtons); return new GerritCallback<JavaScriptObject>() { @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java index 192be34..c0c8037 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Hashtags.java
@@ -218,7 +218,7 @@ } } - private void addHashtag(final String hashtags) { + private void addHashtag(String hashtags) { ChangeApi.hashtags(changeId.get()) .post( PostInput.create(hashtags, null),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java index 689aa2a..1d6ad3d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/LocalComments.java
@@ -125,7 +125,7 @@ public static void saveInlineComments() { final StorageBackend storage = new StorageBackend(); - for (final String cookie : storage.getKeys()) { + for (String cookie : storage.getKeys()) { if (isInlineComment(cookie)) { InlineComment input = getInlineComment(cookie); if (input.commentInfo.id() == null) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java index 3b96a12..cfed9db 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/PathSuggestOracle.java
@@ -37,7 +37,7 @@ } @Override - protected void onRequestSuggestions(final Request req, final Callback cb) { + protected void onRequestSuggestions(Request req, Callback cb) { RestApi api = ChangeApi.revision(changeId.get(), revision.name()).view("files"); if (req.getQuery() != null) { api.addParameter("q", req.getQuery() == null ? "" : req.getQuery());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java index d5d5f36..32284b0 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RelatedChanges.java
@@ -198,7 +198,7 @@ getTab(Tab.SUBMITTED_TOGETHER).setShowSubmittable(true); } - void set(final ChangeInfo info, final String revision) { + void set(ChangeInfo info, String revision) { if (info.status().isOpen()) { setForOpenChange(info, revision); } @@ -246,7 +246,7 @@ } } - private void setForOpenChange(final ChangeInfo info, final String revision) { + private void setForOpenChange(ChangeInfo info, String revision) { if (info.mergeable()) { StringBuilder conflictsQuery = new StringBuilder(); conflictsQuery.append("status:open");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java index 2a926b6..00a543f 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -229,7 +229,7 @@ } @Override - public void onFailure(final Throwable caught) { + public void onFailure(Throwable caught) { if (RestApi.isNotSignedIn(caught)) { lc.setReplyComment(message.getText()); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java index f216af8..1747352 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/RevertAction.java
@@ -27,8 +27,7 @@ import com.google.gwt.user.client.ui.PopupPanel; class RevertAction { - static void call( - final Button b, final Change.Id id, final String revision, final String commitSubject) { + static void call(final Button b, Change.Id id, String revision, String commitSubject) { // TODO Replace ActionDialog with a nicer looking display. b.setEnabled(false); new TextAreaActionDialog(Util.C.revertChangeTitle(), Util.C.headingRevertMessage()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java index 8609774..1ff751e 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReviewerSuggestOracle.java
@@ -34,7 +34,7 @@ private Change.Id changeId; @Override - protected void onRequestSuggestions(final Request req, final Callback cb) { + protected void onRequestSuggestions(Request req, Callback cb) { ChangeApi.suggestReviewers(changeId.get(), req.getQuery(), req.getLimit(), false) .get( new GerritCallback<JsArray<SuggestReviewerInfo>>() { @@ -56,7 +56,7 @@ } @Override - public void requestDefaultSuggestions(final Request req, final Callback cb) { + public void requestDefaultSuggestions(Request req, Callback cb) { requestSuggestions(req, cb); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java index cd880a3..166029d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Reviewers.java
@@ -151,7 +151,7 @@ suggestBox.setServeSuggestionsOnOracle(false); } - private void addReviewer(final String reviewer, boolean confirmed) { + private void addReviewer(String reviewer, boolean confirmed) { if (reviewer.isEmpty()) { return; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java index f37cbc2..1484809 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -50,7 +50,7 @@ private ChangeTable.Section incoming; private ChangeTable.Section closed; - public AccountDashboardScreen(final Account.Id id) { + public AccountDashboardScreen(Account.Id id) { ownerId = id; mine = Gerrit.isSignedIn() && ownerId.equals(Gerrit.getUserAccount().getId()); } @@ -64,7 +64,7 @@ keysNavigation.add( new KeyCommand(0, 'R', Util.C.keyReloadSearch()) { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { Gerrit.display(getToken()); } }); @@ -101,7 +101,7 @@ + who + " -owner:" + who - + " -star:ignore) OR assignee:" + + " -is:ignored) OR assignee:" + who + ")"; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java index f8a9ba1..915867d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -121,6 +121,14 @@ change(id).view("assignee").put(input, cb); } + public static void markPrivate(int id, AsyncCallback<JavaScriptObject> cb) { + change(id).view("private").post(PrivateInput.create(), cb); + } + + public static void unmarkPrivate(int id, AsyncCallback<JavaScriptObject> cb) { + change(id).view("private.delete").post(PrivateInput.create(), cb); + } + public static RestApi comments(int id) { return call(id, "comments"); } @@ -319,6 +327,16 @@ protected CherryPickInput() {} } + private static class PrivateInput extends JavaScriptObject { + static PrivateInput create() { + return (PrivateInput) createObject(); + } + + final native void setMessage(String m) /*-{ this.message = m; }-*/; + + protected PrivateInput() {} + } + private static class RebaseInput extends JavaScriptObject { final native void setBase(String b) /*-{ this.base = b; }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java index ae64ac0..80049df 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -33,6 +33,10 @@ String notCurrent(); + String isPrivate(); + + String isWorkInProgress(); + String changeEdit(); String myDashboardTitle();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties index 01921de..8a9f323 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -7,6 +7,8 @@ mergeConflict = Merge Conflict notCurrent = Not Current changeEdit = Change Edit +isPrivate = (Private) +isWorkInProgress = (WorkInProgress) myDashboardTitle = My Reviews unknownDashboardTitle = Code Review Dashboard
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java index adf7cff..2d6ae15 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -114,7 +114,7 @@ table.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { final Cell cell = table.getCellForEvent(event); if (cell == null) { return; @@ -133,18 +133,18 @@ } @Override - protected Object getRowItemKey(final ChangeInfo item) { + protected Object getRowItemKey(ChangeInfo item) { return item.legacyId(); } @Override - protected void onOpenRow(final int row) { + protected void onOpenRow(int row) { final ChangeInfo c = getRowItem(row); final Change.Id id = c.legacyId(); Gerrit.display(PageLinks.toChange(id)); } - private void insertNoneRow(final int row) { + private void insertNoneRow(int row) { insertRow(row); table.setText(row, 0, Util.C.changeTableNone()); final FlexCellFormatter fmt = table.getFlexCellFormatter(); @@ -152,13 +152,13 @@ fmt.setStyleName(row, 0, Gerrit.RESOURCES.css().emptySection()); } - private void insertChangeRow(final int row) { + private void insertChangeRow(int row) { insertRow(row); applyDataRowStyle(row); } @Override - protected void applyDataRowStyle(final int row) { + protected void applyDataRowStyle(int row) { super.applyDataRowStyle(row); final CellFormatter fmt = table.getCellFormatter(); fmt.addStyleName(row, C_STAR, Gerrit.RESOURCES.css().iconCell()); @@ -225,7 +225,7 @@ } } - private void populateChangeRow(final int row, final ChangeInfo c, boolean highlightUnreviewed) { + private void populateChangeRow(int row, ChangeInfo c, boolean highlightUnreviewed) { CellFormatter fmt = table.getCellFormatter(); if (Gerrit.isSignedIn()) { table.setWidget(row, C_STAR, StarredChanges.createIcon(c.legacyId(), c.starred())); @@ -237,9 +237,17 @@ Change.Status status = c.status(); if (status != Change.Status.NEW) { - table.setText(row, C_STATUS, Util.toLongString(status)); + table.setText( + row, + C_STATUS, + Util.toLongString(status) + (c.isPrivate() ? (" " + Util.C.isPrivate()) : "")); } else if (!c.mergeable()) { - table.setText(row, C_STATUS, Util.C.changeTableNotMergeable()); + table.setText( + row, + C_STATUS, + Util.C.changeTableNotMergeable() + (c.isPrivate() ? (" " + Util.C.isPrivate()) : "")); + } else if (c.isPrivate()) { + table.setText(row, C_STATUS, Util.C.isPrivate()); } if (c.owner() != null) { @@ -408,7 +416,7 @@ return hex.length() == 1 ? "0" + hex : hex; } - public void addSection(final Section s) { + public void addSection(Section s) { assert s.parent == null; s.parent = this; @@ -426,8 +434,8 @@ sections.add(s); } - private int insertRow(final int beforeRow) { - for (final Section s : sections) { + private int insertRow(int beforeRow) { + for (Section s : sections) { if (beforeRow <= s.titleRow) { s.titleRow++; } @@ -438,8 +446,8 @@ return table.insertRow(beforeRow); } - private void removeRow(final int row) { - for (final Section s : sections) { + private void removeRow(int row) { + for (Section s : sections) { if (row < s.titleRow) { s.titleRow--; } @@ -456,7 +464,7 @@ } @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { int row = getCurrentRow(); ChangeInfo c = getRowItem(row); if (c != null && Gerrit.isSignedIn()) { @@ -466,7 +474,7 @@ } private final class TableChangeLink extends ChangeLink { - private TableChangeLink(final String text, final ChangeInfo c) { + private TableChangeLink(String text, ChangeInfo c) { super(text, c.legacyId()); } @@ -490,7 +498,7 @@ this.highlightUnreviewed = value; } - public void setTitleText(final String text) { + public void setTitleText(String text) { titleText = text; titleWidget = null; if (titleRow >= 0) { @@ -498,7 +506,7 @@ } } - public void setTitleWidget(final Widget title) { + public void setTitleWidget(Widget title) { titleWidget = title; titleText = null; if (titleRow >= 0) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java index 3cfe63d..aba4ee0 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
@@ -34,7 +34,7 @@ private List<String> titles; private List<String> queries; - public DashboardTable(final Screen screen, String params) { + public DashboardTable(Screen screen, String params) { titles = new ArrayList<>(); queries = new ArrayList<>(); String foreach = null;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java index 370d942..1695eb9 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -64,7 +64,7 @@ keysNavigation.add( new KeyCommand(0, 'R', Util.C.keyReloadSearch()) { @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { Gerrit.display(getToken()); } }); @@ -126,7 +126,7 @@ } @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { if (link.isVisible()) { History.newItem(link.getTargetHistoryToken()); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java index 12638d7..f511308 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java
@@ -25,7 +25,7 @@ private DashboardTable table; private String params; - public ProjectDashboardScreen(final Project.NameKey toShow, String params) { + public ProjectDashboardScreen(Project.NameKey toShow, String params) { super(toShow); this.params = params; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java index b4499ac..b1028420 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
@@ -73,7 +73,7 @@ } /** Make a key command that toggles the star for a change. */ - public static KeyCommand newKeyCommand(final Icon icon) { + public static KeyCommand newKeyCommand(Icon icon) { return new KeyCommand(0, 's', Util.C.changeTableStar()) { @Override public void onKeyPress(KeyPressEvent event) { @@ -99,7 +99,7 @@ * Set the starred status of a change. This method broadcasts to all interested UI widgets and * sends an RPC to the server to record the updated status. */ - public static void toggleStar(final Change.Id changeId, final boolean newValue) { + public static void toggleStar(Change.Id changeId, boolean newValue) { pending.put(changeId, newValue); fireChangeStarEvent(changeId, newValue); if (!busy) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java index b2efcdb..b62b547 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
@@ -25,7 +25,7 @@ private static final String SUBJECT_CROP_APPENDIX = "..."; private static final int SUBJECT_CROP_RANGE = 10; - public static String toLongString(final Change.Status status) { + public static String toLongString(Change.Status status) { if (status == null) { return ""; } @@ -62,7 +62,7 @@ * @return the subject, cropped if needed */ @SuppressWarnings("deprecation") - public static String cropSubject(final String subject) { + public static String cropSubject(String subject) { if (subject.length() > SUBJECT_MAX_LENGTH) { final int maxLength = SUBJECT_MAX_LENGTH - SUBJECT_CROP_APPENDIX.length(); for (int cropPosition = maxLength;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java index 6215854..0e4ef4e 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
@@ -34,7 +34,7 @@ public class DashboardsTable extends NavigationTable<DashboardInfo> { Project.NameKey project; - public DashboardsTable(final Project.NameKey project) { + public DashboardsTable(Project.NameKey project) { super(Util.C.dashboardItem()); this.project = project; initColumnHeaders(); @@ -96,7 +96,7 @@ finishDisplay(); } - protected void insertTitleRow(final int row, String section) { + protected void insertTitleRow(int row, String section) { table.insertRow(row); table.setText(row, 0, section); @@ -106,7 +106,7 @@ fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().sectionHeader()); } - protected void insert(final int row, final DashboardInfo k) { + protected void insert(int row, DashboardInfo k) { table.insertRow(row); applyDataRowStyle(row); @@ -121,7 +121,7 @@ populate(row, k); } - protected void populate(final int row, final DashboardInfo k) { + protected void populate(int row, DashboardInfo k) { if (k.isDefault()) { table.setWidget(row, 1, new Image(Gerrit.RESOURCES.greenCheck())); final FlexCellFormatter fmt = table.getFlexCellFormatter(); @@ -147,12 +147,12 @@ } @Override - protected Object getRowItemKey(final DashboardInfo item) { + protected Object getRowItemKey(DashboardInfo item) { return item.id(); } @Override - protected void onOpenRow(final int row) { + protected void onOpenRow(int row) { if (row > 0) { movePointerTo(row); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java index 953bc87..0091f53 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ChunkManager.java
@@ -68,23 +68,15 @@ colorLines(cm, LineClassWhere.WRAP, color, line, line + cnt); } - void colorLines( - final CodeMirror cm, - final LineClassWhere where, - final String className, - final int start, - final int end) { + void colorLines(CodeMirror cm, LineClassWhere where, String className, int start, int end) { if (start < end) { for (int line = start; line < end; line++) { cm.addLineClass(line, where, className); } undo.add( - new Runnable() { - @Override - public void run() { - for (int line = start; line < end; line++) { - cm.removeLineClass(line, where, className); - } + () -> { + for (int line = start; line < end; line++) { + cm.removeLineClass(line, where, className); } }); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java index 587dacc..95b88ac 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/CommentManager.java
@@ -203,32 +203,26 @@ abstract String getTokenSuffixForActiveLine(CodeMirror cm); - Runnable signInCallback(final CodeMirror cm) { - return new Runnable() { - @Override - public void run() { - String token = host.getToken(); - if (cm.extras().hasActiveLine()) { - token += "@" + getTokenSuffixForActiveLine(cm); - } - Gerrit.doSignIn(token); + Runnable signInCallback(CodeMirror cm) { + return () -> { + String token = host.getToken(); + if (cm.extras().hasActiveLine()) { + token += "@" + getTokenSuffixForActiveLine(cm); } + Gerrit.doSignIn(token); }; } abstract void newDraft(CodeMirror cm); - Runnable newDraftCallback(final CodeMirror cm) { + Runnable newDraftCallback(CodeMirror cm) { if (!Gerrit.isSignedIn()) { return signInCallback(cm); } - return new Runnable() { - @Override - public void run() { - if (cm.extras().hasActiveLine()) { - newDraft(cm); - } + return () -> { + if (cm.extras().hasActiveLine()) { + newDraft(cm); } }; } @@ -267,52 +261,49 @@ abstract SortedMap<Integer, CommentGroup> getMapForNav(DisplaySide side); - Runnable commentNav(final CodeMirror src, final Direction dir) { - return new Runnable() { - @Override - public void run() { - // Every comment appears in both side maps as a linked pair. - // It is only necessary to search one side to find a comment - // on either side of the editor pair. - SortedMap<Integer, CommentGroup> map = getMapForNav(src.side()); - int line = - src.extras().hasActiveLine() ? src.getLineNumber(src.extras().activeLine()) + 1 : 0; + Runnable commentNav(CodeMirror src, Direction dir) { + return () -> { + // Every comment appears in both side maps as a linked pair. + // It is only necessary to search one side to find a comment + // on either side of the editor pair. + SortedMap<Integer, CommentGroup> map = getMapForNav(src.side()); + int line = + src.extras().hasActiveLine() ? src.getLineNumber(src.extras().activeLine()) + 1 : 0; - CommentGroup g; - if (dir == Direction.NEXT) { - map = map.tailMap(line + 1); + CommentGroup g; + if (dir == Direction.NEXT) { + map = map.tailMap(line + 1); + if (map.isEmpty()) { + return; + } + g = map.get(map.firstKey()); + while (g.getBoxCount() == 0) { + map = map.tailMap(map.firstKey() + 1); if (map.isEmpty()) { return; } g = map.get(map.firstKey()); - while (g.getBoxCount() == 0) { - map = map.tailMap(map.firstKey() + 1); - if (map.isEmpty()) { - return; - } - g = map.get(map.firstKey()); - } - } else { - map = map.headMap(line); + } + } else { + map = map.headMap(line); + if (map.isEmpty()) { + return; + } + g = map.get(map.lastKey()); + while (g.getBoxCount() == 0) { + map = map.headMap(map.lastKey()); if (map.isEmpty()) { return; } g = map.get(map.lastKey()); - while (g.getBoxCount() == 0) { - map = map.headMap(map.lastKey()); - if (map.isEmpty()) { - return; - } - g = map.get(map.lastKey()); - } } - - CodeMirror cm = g.getCm(); - double y = cm.heightAtLine(g.getLine() - 1, "local"); - cm.setCursor(Pos.create(g.getLine() - 1)); - cm.scrollToY(y - 0.5 * cm.scrollbarV().getClientHeight()); - cm.focus(); } + + CodeMirror cm = g.getCm(); + double y = cm.heightAtLine(g.getLine() - 1, "local"); + cm.setCursor(Pos.create(g.getLine() - 1)); + cm.scrollToY(y - 0.5 * cm.scrollbarV().getClientHeight()); + cm.focus(); }; } @@ -425,26 +416,20 @@ abstract CommentGroup getCommentGroupOnActiveLine(CodeMirror cm); - Runnable toggleOpenBox(final CodeMirror cm) { - return new Runnable() { - @Override - public void run() { - CommentGroup group = getCommentGroupOnActiveLine(cm); - if (group != null) { - group.openCloseLast(); - } + Runnable toggleOpenBox(CodeMirror cm) { + return () -> { + CommentGroup group = getCommentGroupOnActiveLine(cm); + if (group != null) { + group.openCloseLast(); } }; } - Runnable openCloseAll(final CodeMirror cm) { - return new Runnable() { - @Override - public void run() { - CommentGroup group = getCommentGroupOnActiveLine(cm); - if (group != null) { - group.openCloseAll(); - } + Runnable openCloseAll(CodeMirror cm) { + return () -> { + CommentGroup group = getCommentGroupOnActiveLine(cm); + if (group != null) { + group.openCloseAll(); } }; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java index 60a75eb..9ccf6ea 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffScreen.java
@@ -336,7 +336,7 @@ handlers.clear(); } - void registerCmEvents(final CodeMirror cm) { + void registerCmEvents(CodeMirror cm) { cm.on("cursorActivity", updateActiveLine(cm)); cm.on("focus", updateActiveLine(cm)); KeyMap keyMap = @@ -356,170 +356,45 @@ .on("Shift-O", getCommentManager().openCloseAll(cm)) .on( "I", - new Runnable() { - @Override - public void run() { - switch (getIntraLineStatus()) { - case OFF: - case OK: - toggleShowIntraline(); - break; - case FAILURE: - case TIMEOUT: - default: - break; - } + () -> { + switch (getIntraLineStatus()) { + case OFF: + case OK: + toggleShowIntraline(); + break; + case FAILURE: + case TIMEOUT: + default: + break; } }) - .on( - "','", - new Runnable() { - @Override - public void run() { - prefsAction.show(); - } - }) - .on( - "Shift-/", - new Runnable() { - @Override - public void run() { - new ShowHelpCommand().onKeyPress(null); - } - }) - .on( - "Space", - new Runnable() { - @Override - public void run() { - cm.vim().handleKey("<C-d>"); - } - }) - .on( - "Shift-Space", - new Runnable() { - @Override - public void run() { - cm.vim().handleKey("<C-u>"); - } - }) - .on( - "Ctrl-F", - new Runnable() { - @Override - public void run() { - cm.execCommand("find"); - } - }) - .on( - "Ctrl-G", - new Runnable() { - @Override - public void run() { - cm.execCommand("findNext"); - } - }) + .on("','", prefsAction::show) + .on("Shift-/", () -> new ShowHelpCommand().onKeyPress(null)) + .on("Space", () -> cm.vim().handleKey("<C-d>")) + .on("Shift-Space", () -> cm.vim().handleKey("<C-u>")) + .on("Ctrl-F", () -> cm.execCommand("find")) + .on("Ctrl-G", () -> cm.execCommand("findNext")) .on("Enter", maybeNextCmSearch(cm)) - .on( - "Shift-Ctrl-G", - new Runnable() { - @Override - public void run() { - cm.execCommand("findPrev"); - } - }) - .on( - "Shift-Enter", - new Runnable() { - @Override - public void run() { - cm.execCommand("findPrev"); - } - }) + .on("Shift-Ctrl-G", () -> cm.execCommand("findPrev")) + .on("Shift-Enter", () -> cm.execCommand("findPrev")) .on( "Esc", - new Runnable() { - @Override - public void run() { - cm.setCursor(cm.getCursor()); - cm.execCommand("clearSearch"); - cm.vim().handleEx("nohlsearch"); - } + () -> { + cm.setCursor(cm.getCursor()); + cm.execCommand("clearSearch"); + cm.vim().handleEx("nohlsearch"); }) - .on( - "Ctrl-A", - new Runnable() { - @Override - public void run() { - cm.execCommand("selectAll"); - } - }) - .on( - "G O", - new Runnable() { - @Override - public void run() { - Gerrit.display(PageLinks.toChangeQuery("status:open")); - } - }) - .on( - "G M", - new Runnable() { - @Override - public void run() { - Gerrit.display(PageLinks.toChangeQuery("status:merged")); - } - }) - .on( - "G A", - new Runnable() { - @Override - public void run() { - Gerrit.display(PageLinks.toChangeQuery("status:abandoned")); - } - }); + .on("Ctrl-A", () -> cm.execCommand("selectAll")) + .on("G O", () -> Gerrit.display(PageLinks.toChangeQuery("status:open"))) + .on("G M", () -> Gerrit.display(PageLinks.toChangeQuery("status:merged"))) + .on("G A", () -> Gerrit.display(PageLinks.toChangeQuery("status:abandoned"))); if (Gerrit.isSignedIn()) { keyMap - .on( - "G I", - new Runnable() { - @Override - public void run() { - Gerrit.display(PageLinks.MINE); - } - }) - .on( - "G D", - new Runnable() { - @Override - public void run() { - Gerrit.display(PageLinks.toChangeQuery("owner:self is:draft")); - } - }) - .on( - "G C", - new Runnable() { - @Override - public void run() { - Gerrit.display(PageLinks.toChangeQuery("has:draft")); - } - }) - .on( - "G W", - new Runnable() { - @Override - public void run() { - Gerrit.display(PageLinks.toChangeQuery("is:watched status:open")); - } - }) - .on( - "G S", - new Runnable() { - @Override - public void run() { - Gerrit.display(PageLinks.toChangeQuery("is:starred")); - } - }); + .on("G I", () -> Gerrit.display(PageLinks.MINE)) + .on("G D", () -> Gerrit.display(PageLinks.toChangeQuery("owner:self is:draft"))) + .on("G C", () -> Gerrit.display(PageLinks.toChangeQuery("has:draft"))) + .on("G W", () -> Gerrit.display(PageLinks.toChangeQuery("is:watched status:open"))) + .on("G S", () -> Gerrit.display(PageLinks.toChangeQuery("is:starred"))); } if (revision.get() != 0) { @@ -537,7 +412,7 @@ } } - private BeforeSelectionChangeHandler onSelectionChange(final CodeMirror cm) { + private BeforeSelectionChangeHandler onSelectionChange(CodeMirror cm) { return new BeforeSelectionChangeHandler() { private InsertCommentBubble bubble; @@ -698,15 +573,12 @@ abstract void setSyntaxHighlighting(boolean b); - void setContext(final int context) { + void setContext(int context) { operation( - new Runnable() { - @Override - public void run() { - skipManager.removeAll(); - skipManager.render(context, diff); - updateRenderEntireFile(); - } + () -> { + skipManager.removeAll(); + skipManager.render(context, diff); + updateRenderEntireFile(); }); } @@ -753,21 +625,18 @@ return line - offset; } - private Runnable openEditScreen(final CodeMirror cm) { - return new Runnable() { - @Override - public void run() { - LineHandle handle = cm.extras().activeLine(); - int line = cm.getLineNumber(handle) + 1; - if (Patch.COMMIT_MSG.equals(path)) { - line = adjustCommitMessageLine(line); - } - String token = Dispatcher.toEditScreen(revision, path, line); - if (!Gerrit.isSignedIn()) { - Gerrit.doSignIn(token); - } else { - Gerrit.display(token); - } + private Runnable openEditScreen(CodeMirror cm) { + return () -> { + LineHandle handle = cm.extras().activeLine(); + int line = cm.getLineNumber(handle) + 1; + if (Patch.COMMIT_MSG.equals(path)) { + line = adjustCommitMessageLine(line); + } + String token = Dispatcher.toEditScreen(revision, path, line); + if (!Gerrit.isSignedIn()) { + Gerrit.doSignIn(token); + } else { + Gerrit.display(token); } }; } @@ -832,63 +701,51 @@ abstract void operation(Runnable apply); - private Runnable upToChange(final boolean openReplyBox) { - return new Runnable() { - @Override - public void run() { - CallbackGroup group = new CallbackGroup(); - getCommentManager().saveAllDrafts(group); - group.done(); - group.addListener( - new GerritCallback<Void>() { - @Override - public void onSuccess(Void result) { - String rev = String.valueOf(revision.get()); - Gerrit.display( - PageLinks.toChange(changeId, base.asString(), rev), - new ChangeScreen(changeId, base, rev, openReplyBox, FileTable.Mode.REVIEW)); - } - }); + private Runnable upToChange(boolean openReplyBox) { + return () -> { + CallbackGroup group = new CallbackGroup(); + getCommentManager().saveAllDrafts(group); + group.done(); + group.addListener( + new GerritCallback<Void>() { + @Override + public void onSuccess(Void result) { + String rev = String.valueOf(revision.get()); + Gerrit.display( + PageLinks.toChange(changeId, base.asString(), rev), + new ChangeScreen(changeId, base, rev, openReplyBox, FileTable.Mode.REVIEW)); + } + }); + }; + } + + private Runnable maybePrevVimSearch(CodeMirror cm) { + return () -> { + if (cm.vim().hasSearchHighlight()) { + cm.vim().handleKey("N"); + } else { + getCommentManager().commentNav(cm, Direction.NEXT).run(); } }; } - private Runnable maybePrevVimSearch(final CodeMirror cm) { - return new Runnable() { - @Override - public void run() { - if (cm.vim().hasSearchHighlight()) { - cm.vim().handleKey("N"); - } else { - getCommentManager().commentNav(cm, Direction.NEXT).run(); - } + private Runnable maybeNextVimSearch(CodeMirror cm) { + return () -> { + if (cm.vim().hasSearchHighlight()) { + cm.vim().handleKey("n"); + } else { + getChunkManager().diffChunkNav(cm, Direction.NEXT).run(); } }; } - private Runnable maybeNextVimSearch(final CodeMirror cm) { - return new Runnable() { - @Override - public void run() { - if (cm.vim().hasSearchHighlight()) { - cm.vim().handleKey("n"); - } else { - getChunkManager().diffChunkNav(cm, Direction.NEXT).run(); - } - } - }; - } - - Runnable maybeNextCmSearch(final CodeMirror cm) { - return new Runnable() { - @Override - public void run() { - if (cm.hasSearchHighlight()) { - cm.execCommand("findNext"); - } else { - cm.execCommand("clearSearch"); - getCommentManager().toggleOpenBox(cm).run(); - } + Runnable maybeNextCmSearch(CodeMirror cm) { + return () -> { + if (cm.hasSearchHighlight()) { + cm.execCommand("findNext"); + } else { + cm.execCommand("clearSearch"); + getCommentManager().toggleOpenBox(cm).run(); } }; } @@ -973,7 +830,7 @@ } void reloadDiffInfo() { - final int id = ++reloadVersionId; + int id = ++reloadVersionId; DiffApi.diff(revision, path) .base(base.asPatchSetId()) .wholeFile() @@ -986,16 +843,13 @@ if (id == reloadVersionId && isAttached()) { diff = diffInfo; operation( - new Runnable() { - @Override - public void run() { - skipManager.removeAll(); - getChunkManager().reset(); - getDiffTable().scrollbar.removeDiffAnnotations(); - setShowIntraline(prefs.intralineDifference()); - render(diff); - skipManager.render(prefs.context(), diff); - } + () -> { + skipManager.removeAll(); + getChunkManager().reset(); + getDiffTable().scrollbar.removeDiffAnnotations(); + setShowIntraline(prefs.intralineDifference()); + render(diff); + skipManager.render(prefs.context(), diff); }); } } @@ -1017,11 +871,11 @@ abstract Runnable updateActiveLine(CodeMirror cm); - private GutterClickHandler onGutterClick(final CodeMirror cm) { + private GutterClickHandler onGutterClick(CodeMirror cm) { return new GutterClickHandler() { @Override public void handle( - CodeMirror instance, final int line, final String gutterClass, NativeEvent clickEvent) { + CodeMirror instance, int line, String gutterClass, NativeEvent clickEvent) { if (Element.as(clickEvent.getEventTarget()).hasClassName(getLineNumberClassName()) && clickEvent.getButton() == NativeEvent.BUTTON_LEFT && !clickEvent.getMetaKey()
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java index a2ffb03f..bf9f9e3 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Header.java
@@ -318,47 +318,26 @@ } Runnable toggleReviewed() { - return new Runnable() { - @Override - public void run() { - reviewed.setValue(!reviewed.getValue(), true); - } - }; + return () -> reviewed.setValue(!reviewed.getValue(), true); } Runnable navigate(Direction dir) { switch (dir) { case PREV: - return new Runnable() { - @Override - public void run() { - (hasPrev ? prev : up).go(); - } - }; + return () -> (hasPrev ? prev : up).go(); case NEXT: - return new Runnable() { - @Override - public void run() { - (hasNext ? next : up).go(); - } - }; + return () -> (hasNext ? next : up).go(); default: - return new Runnable() { - @Override - public void run() {} - }; + return () -> {}; } } Runnable reviewedAndNext() { - return new Runnable() { - @Override - public void run() { - if (Gerrit.isSignedIn()) { - reviewed.setValue(true, true); - } - navigate(Direction.NEXT).run(); + return () -> { + if (Gerrit.isSignedIn()) { + reviewed.setValue(true, true); } + navigate(Direction.NEXT).run(); }; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java index b04973a..f8eab91 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/InsertCommentBubble.java
@@ -35,7 +35,7 @@ @UiField Image icon; - InsertCommentBubble(final CommentManager commentManager, final CodeMirror cm) { + InsertCommentBubble(CommentManager commentManager, CodeMirror cm) { initWidget(uiBinder.createAndBindUi(this)); addDomHandler( new ClickHandler() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java index 822bc74..e62a283 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PatchSetSelectBox.java
@@ -147,8 +147,7 @@ } } - void setUpBlame( - final CodeMirror cm, final boolean isBase, final PatchSet.Id rev, final String path) { + void setUpBlame(final CodeMirror cm, boolean isBase, PatchSet.Id rev, String path) { if (!Patch.isMagic(path) && Gerrit.isSignedIn() && Gerrit.info().change().allowBlame()) { Anchor blameIcon = createBlameIcon(); blameIcon.addClickHandler(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java index 4d781ea..ed4ac25 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
@@ -322,13 +322,10 @@ prefs.tabSize(Math.max(1, Integer.parseInt(v))); if (view != null) { view.operation( - new Runnable() { - @Override - public void run() { - int v = prefs.tabSize(); - for (CodeMirror cm : view.getCms()) { - cm.setOption("tabSize", v); - } + () -> { + int size = prefs.tabSize(); + for (CodeMirror cm : view.getCms()) { + cm.setOption("tabSize", size); } }); } @@ -341,13 +338,7 @@ if (v != null && v.length() > 0) { prefs.lineLength(Math.max(1, Integer.parseInt(v))); if (view != null) { - view.operation( - new Runnable() { - @Override - public void run() { - view.setLineLength(prefs.lineLength()); - } - }); + view.operation(() -> view.setLineLength(prefs.lineLength())); } } } @@ -448,7 +439,7 @@ @UiHandler("mode") void onMode(@SuppressWarnings("unused") ChangeEvent e) { - final String mode = getSelectedMode(); + String mode = getSelectedMode(); prefs.syntaxHighlighting(true); syntaxHighlighting.setValue(true, false); new ModeInjector() @@ -461,12 +452,9 @@ && Objects.equals(mode, getSelectedMode()) && view.isAttached()) { view.operation( - new Runnable() { - @Override - public void run() { - view.getCmFromSide(DisplaySide.A).setOption("mode", mode); - view.getCmFromSide(DisplaySide.B).setOption("mode", mode); - } + () -> { + view.getCmFromSide(DisplaySide.A).setOption("mode", mode); + view.getCmFromSide(DisplaySide.B).setOption("mode", mode); }); } } @@ -483,13 +471,10 @@ prefs.showWhitespaceErrors(e.getValue()); if (view != null) { view.operation( - new Runnable() { - @Override - public void run() { - boolean s = prefs.showWhitespaceErrors(); - for (CodeMirror cm : view.getCms()) { - cm.setOption("showTrailingSpace", s); - } + () -> { + boolean s = prefs.showWhitespaceErrors(); + for (CodeMirror cm : view.getCms()) { + cm.setOption("showTrailingSpace", s); } }); } @@ -537,7 +522,7 @@ @UiHandler("theme") void onTheme(@SuppressWarnings("unused") ChangeEvent e) { - final Theme newTheme = getSelectedTheme(); + Theme newTheme = getSelectedTheme(); prefs.theme(newTheme); if (view != null) { ThemeLoader.loadTheme( @@ -546,15 +531,12 @@ @Override public void onSuccess(Void result) { view.operation( - new Runnable() { - @Override - public void run() { - if (getSelectedTheme() == newTheme && isAttached()) { - String t = newTheme.name().toLowerCase(); - view.getCmFromSide(DisplaySide.A).setOption("theme", t); - view.getCmFromSide(DisplaySide.B).setOption("theme", t); - view.setThemeStyles(newTheme.isDark()); - } + () -> { + if (getSelectedTheme() == newTheme && isAttached()) { + String t = newTheme.name().toLowerCase(); + view.getCmFromSide(DisplaySide.A).setOption("theme", t); + view.getCmFromSide(DisplaySide.B).setOption("theme", t); + view.setThemeStyles(newTheme.isDark()); } }); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java index 6cb9b6a..ecdac46 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/ScrollbarAnnotation.java
@@ -64,12 +64,9 @@ refresh = cmB.on( "refresh", - new Runnable() { - @Override - public void run() { - if (updateScale()) { - updatePosition(); - } + () -> { + if (updateScale()) { + updatePosition(); } }); updateScale();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java index 1560597..b7f5948 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
@@ -102,14 +102,11 @@ super.onShowView(); operation( - new Runnable() { - @Override - public void run() { - resizeCodeMirror(); - chunkManager.adjustPadding(); - cmA.refresh(); - cmB.refresh(); - } + () -> { + resizeCodeMirror(); + chunkManager.adjustPadding(); + cmA.refresh(); + cmB.refresh(); }); setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength()); diffTable.refresh(); @@ -141,7 +138,7 @@ } @Override - void registerCmEvents(final CodeMirror cm) { + void registerCmEvents(CodeMirror cm) { super.registerCmEvents(cm); KeyMap keyMap = @@ -183,8 +180,8 @@ }; } - private void display(final CommentsCollections comments) { - final DiffInfo diff = getDiff(); + private void display(CommentsCollections comments) { + DiffInfo diff = getDiff(); setThemeStyles(prefs.theme().isDark()); setShowIntraline(prefs.intralineDifference()); if (prefs.showLineNumbers()) { @@ -209,18 +206,15 @@ chunkManager = new SideBySideChunkManager(this, cmA, cmB, diffTable.scrollbar); operation( - new Runnable() { - @Override - public void run() { - // Estimate initial CodeMirror height, fixed up in onShowView. - int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18); - cmA.setHeight(height); - cmB.setHeight(height); + () -> { + // Estimate initial CodeMirror height, fixed up in onShowView. + int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18); + cmA.setHeight(height); + cmB.setHeight(height); - render(diff); - commentManager.render(comments, prefs.expandAllComments()); - skipManager.render(prefs.context(), diff); - } + render(diff); + commentManager.render(comments, prefs.expandAllComments()); + skipManager.render(prefs.context(), diff); }); registerCmEvents(cmA); @@ -319,66 +313,52 @@ } @Override - Runnable updateActiveLine(final CodeMirror cm) { - final CodeMirror other = otherCm(cm); - return new Runnable() { - @Override - public void run() { - // The rendering of active lines has to be deferred. Reflow - // caused by adding and removing styles chokes Firefox when arrow - // key (or j/k) is held down. Performance on Chrome is fine - // without the deferral. - // - Scheduler.get() - .scheduleDeferred( - new ScheduledCommand() { - @Override - public void execute() { - operation( - new Runnable() { - @Override - public void run() { - LineHandle handle = - cm.getLineHandleVisualStart(cm.getCursor("end").line()); - if (!cm.extras().activeLine(handle)) { - return; - } + Runnable updateActiveLine(CodeMirror cm) { + CodeMirror other = otherCm(cm); + return () -> { + // The rendering of active lines has to be deferred. Reflow + // caused by adding and removing styles chokes Firefox when arrow + // key (or j/k) is held down. Performance on Chrome is fine + // without the deferral. + // + Scheduler.get() + .scheduleDeferred( + new ScheduledCommand() { + @Override + public void execute() { + operation( + () -> { + LineHandle handle = cm.getLineHandleVisualStart(cm.getCursor("end").line()); + if (!cm.extras().activeLine(handle)) { + return; + } - LineOnOtherInfo info = lineOnOther(cm.side(), cm.getLineNumber(handle)); - if (info.isAligned()) { - other.extras().activeLine(other.getLineHandle(info.getLine())); - } else { - other.extras().clearActiveLine(); - } - } - }); - } - }); - } + LineOnOtherInfo info = lineOnOther(cm.side(), cm.getLineNumber(handle)); + if (info.isAligned()) { + other.extras().activeLine(other.getLineHandle(info.getLine())); + } else { + other.extras().clearActiveLine(); + } + }); + } + }); }; } - private Runnable moveCursorToSide(final CodeMirror cmSrc, DisplaySide sideDst) { - final CodeMirror cmDst = getCmFromSide(sideDst); + private Runnable moveCursorToSide(CodeMirror cmSrc, DisplaySide sideDst) { + CodeMirror cmDst = getCmFromSide(sideDst); if (cmDst == cmSrc) { - return new Runnable() { - @Override - public void run() {} - }; + return () -> {}; } - final DisplaySide sideSrc = cmSrc.side(); - return new Runnable() { - @Override - public void run() { - if (cmSrc.extras().hasActiveLine()) { - cmDst.setCursor( - Pos.create( - lineOnOther(sideSrc, cmSrc.getLineNumber(cmSrc.extras().activeLine())) - .getLine())); - } - cmDst.focus(); + DisplaySide sideSrc = cmSrc.side(); + return () -> { + if (cmSrc.extras().hasActiveLine()) { + cmDst.setCursor( + Pos.create( + lineOnOther(sideSrc, cmSrc.getLineNumber(cmSrc.extras().activeLine())).getLine())); } + cmDst.focus(); }; } @@ -389,20 +369,8 @@ } @Override - void operation(final Runnable apply) { - cmA.operation( - new Runnable() { - @Override - public void run() { - cmB.operation( - new Runnable() { - @Override - public void run() { - apply.run(); - } - }); - } - }); + void operation(Runnable apply) { + cmA.operation(() -> cmB.operation(apply::run)); } @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java index a78e59e..2877794 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideChunkManager.java
@@ -219,7 +219,7 @@ * @param line line to put the padding below. * @param len number of lines to pad. Padding is inserted only if {@code len >= 1}. */ - private void addPadding(CodeMirror cm, int line, final int len) { + private void addPadding(CodeMirror cm, int line, int len) { if (0 < len) { Element pad = DOM.createDiv(); pad.setClassName(SideBySideTable.style.padding()); @@ -245,16 +245,13 @@ } @Override - Runnable diffChunkNav(final CodeMirror cm, final Direction dir) { - return new Runnable() { - @Override - public void run() { - int line = cm.extras().hasActiveLine() ? cm.getLineNumber(cm.extras().activeLine()) : 0; - int res = - Collections.binarySearch( - chunks, new DiffChunkInfo(cm.side(), line, 0, false), getDiffChunkComparator()); - diffChunkNavHelper(chunks, host, res, dir); - } + Runnable diffChunkNav(CodeMirror cm, Direction dir) { + return () -> { + int line = cm.extras().hasActiveLine() ? cm.getLineNumber(cm.extras().activeLine()) : 0; + int res = + Collections.binarySearch( + chunks, new DiffChunkInfo(cm.side(), line, 0, false), getDiffChunkComparator()); + diffChunkNavHelper(chunks, host, res, dir); }; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java index 6fcd6c8..c728f6f 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideCommentGroup.java
@@ -88,29 +88,26 @@ void handleRedraw() { getLineWidget() .onRedraw( - new Runnable() { - @Override - public void run() { - if (canComputeHeight() && peers.peek().canComputeHeight()) { - if (getResizeTimer() != null) { - getResizeTimer().cancel(); - setResizeTimer(null); - } - adjustPadding(SideBySideCommentGroup.this, peers.peek()); - } else if (getResizeTimer() == null) { - setResizeTimer( - new Timer() { - @Override - public void run() { - if (canComputeHeight() && peers.peek().canComputeHeight()) { - cancel(); - setResizeTimer(null); - adjustPadding(SideBySideCommentGroup.this, peers.peek()); - } - } - }); - getResizeTimer().scheduleRepeating(5); + () -> { + if (canComputeHeight() && peers.peek().canComputeHeight()) { + if (getResizeTimer() != null) { + getResizeTimer().cancel(); + setResizeTimer(null); } + adjustPadding(SideBySideCommentGroup.this, peers.peek()); + } else if (getResizeTimer() == null) { + setResizeTimer( + new Timer() { + @Override + public void run() { + if (canComputeHeight() && peers.peek().canComputeHeight()) { + cancel(); + setResizeTimer(null); + adjustPadding(SideBySideCommentGroup.this, peers.peek()); + } + } + }); + getResizeTimer().scheduleRepeating(5); } }); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java index 7465c81..c65dcf0 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySideTable.java
@@ -75,12 +75,7 @@ } Runnable toggleA() { - return new Runnable() { - @Override - public void run() { - setVisibleA(!isVisibleA()); - } - }; + return () -> setVisibleA(!isVisibleA()); } void setVisibleB(boolean show) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java index 03cfd60..c138f37 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipBar.java
@@ -62,7 +62,7 @@ private TextMarker textMarker; private SkipBar otherBar; - SkipBar(SkipManager manager, final CodeMirror cm) { + SkipBar(SkipManager manager, CodeMirror cm) { this.manager = manager; this.cm = cm; @@ -91,12 +91,9 @@ } if (isNew) { lineWidget.onFirstRedraw( - new Runnable() { - @Override - public void run() { - int w = cm.getGutterElement().getOffsetWidth(); - getElement().getStyle().setPaddingLeft(w, Unit.PX); - } + () -> { + int w = cm.getGutterElement().getOffsetWidth(); + getElement().getStyle().setPaddingLeft(w, Unit.PX); }); } } @@ -110,14 +107,7 @@ .set("inclusiveLeft", true) .set("inclusiveRight", true)); - textMarker.on( - "beforeCursorEnter", - new Runnable() { - @Override - public void run() { - expandAll(); - } - }); + textMarker.on("beforeCursorEnter", this::expandAll); int skipped = end - start + 1; if (skipped <= UP_DOWN_THRESHOLD) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java index 0f0ba41..8647d68 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/Unified.java
@@ -30,7 +30,6 @@ import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArrayString; import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.FocusEvent; import com.google.gwt.event.dom.client.FocusHandler; @@ -102,12 +101,9 @@ super.onShowView(); operation( - new Runnable() { - @Override - public void run() { - resizeCodeMirror(); - cm.refresh(); - } + () -> { + resizeCodeMirror(); + cm.refresh(); }); setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength()); diffTable.refresh(); @@ -137,18 +133,15 @@ } @Override - void registerCmEvents(final CodeMirror cm) { + void registerCmEvents(CodeMirror cm) { super.registerCmEvents(cm); cm.on( "scroll", - new Runnable() { - @Override - public void run() { - ScrollInfo si = cm.getScrollInfo(); - if (autoHideDiffTableHeader) { - updateDiffTableHeader(si); - } + () -> { + ScrollInfo si = cm.getScrollInfo(); + if (autoHideDiffTableHeader) { + updateDiffTableHeader(si); } }); maybeRegisterRenderEntireFileKeyMap(cm); @@ -171,8 +164,8 @@ }; } - private void display(final CommentsCollections comments) { - final DiffInfo diff = getDiff(); + private void display(CommentsCollections comments) { + DiffInfo diff = getDiff(); setThemeStyles(prefs.theme().isDark()); setShowIntraline(prefs.intralineDifference()); if (prefs.showLineNumbers()) { @@ -186,17 +179,14 @@ chunkManager = new UnifiedChunkManager(this, cm, diffTable.scrollbar); operation( - new Runnable() { - @Override - public void run() { - // Estimate initial CodeMirror height, fixed up in onShowView. - int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18); - cm.setHeight(height); + () -> { + // Estimate initial CodeMirror height, fixed up in onShowView. + int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18); + cm.setHeight(height); - render(diff); - commentManager.render(comments, prefs.expandAllComments()); - skipManager.render(prefs.context(), diff); - } + render(diff); + commentManager.render(comments, prefs.expandAllComments()); + skipManager.render(prefs.context(), diff); }); registerCmEvents(cm); @@ -317,25 +307,19 @@ } @Override - Runnable updateActiveLine(final CodeMirror cm) { - return new Runnable() { - @Override - public void run() { - // The rendering of active lines has to be deferred. Reflow - // caused by adding and removing styles chokes Firefox when arrow - // key (or j/k) is held down. Performance on Chrome is fine - // without the deferral. - // - Scheduler.get() - .scheduleDeferred( - new ScheduledCommand() { - @Override - public void execute() { - LineHandle handle = cm.getLineHandleVisualStart(cm.getCursor("end").line()); - cm.extras().activeLine(handle); - } - }); - } + Runnable updateActiveLine(CodeMirror cm) { + return () -> { + // The rendering of active lines has to be deferred. Reflow + // caused by adding and removing styles chokes Firefox when arrow + // key (or j/k) is held down. Performance on Chrome is fine + // without the deferral. + // + Scheduler.get() + .scheduleDeferred( + () -> { + LineHandle handle = cm.getLineHandleVisualStart(cm.getCursor("end").line()); + cm.extras().activeLine(handle); + }); }; } @@ -354,14 +338,8 @@ } @Override - void operation(final Runnable apply) { - cm.operation( - new Runnable() { - @Override - public void run() { - apply.run(); - } - }); + void operation(Runnable apply) { + cm.operation(apply::run); } @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java index 3939f99..1a662e2 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedChunkManager.java
@@ -213,18 +213,15 @@ } @Override - Runnable diffChunkNav(final CodeMirror cm, final Direction dir) { - return new Runnable() { - @Override - public void run() { - int line = cm.extras().hasActiveLine() ? cm.getLineNumber(cm.extras().activeLine()) : 0; - int res = - Collections.binarySearch( - chunks, - new UnifiedDiffChunkInfo(cm.side(), 0, 0, line, false), - getDiffChunkComparatorCmLine()); - diffChunkNavHelper(chunks, host, res, dir); - } + Runnable diffChunkNav(CodeMirror cm, Direction dir) { + return () -> { + int line = cm.extras().hasActiveLine() ? cm.getLineNumber(cm.extras().activeLine()) : 0; + int res = + Collections.binarySearch( + chunks, + new UnifiedDiffChunkInfo(cm.side(), 0, 0, line, false), + getDiffChunkComparatorCmLine()); + diffChunkNavHelper(chunks, host, res, dir); }; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java index a6912df..6d5fba3 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UnifiedCommentGroup.java
@@ -50,29 +50,26 @@ void handleRedraw() { getLineWidget() .onRedraw( - new Runnable() { - @Override - public void run() { - if (canComputeHeight()) { - if (getResizeTimer() != null) { - getResizeTimer().cancel(); - setResizeTimer(null); - } - reportHeightChange(); - } else if (getResizeTimer() == null) { - setResizeTimer( - new Timer() { - @Override - public void run() { - if (canComputeHeight()) { - cancel(); - setResizeTimer(null); - reportHeightChange(); - } - } - }); - getResizeTimer().scheduleRepeating(5); + () -> { + if (canComputeHeight()) { + if (getResizeTimer() != null) { + getResizeTimer().cancel(); + setResizeTimer(null); } + reportHeightChange(); + } else if (getResizeTimer() == null) { + setResizeTimer( + new Timer() { + @Override + public void run() { + if (canComputeHeight()) { + cancel(); + setResizeTimer(null); + reportHeightChange(); + } + } + }); + getResizeTimer().scheduleRepeating(5); } }); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand.java index ea2f2cf..df9bcf9 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/UpToChangeCommand.java
@@ -30,7 +30,7 @@ } @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { Gerrit.display(PageLinks.toChange(revision.getParentKey(), revision.getId())); } }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java index 282176b..8bbc988 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -49,7 +49,6 @@ import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.Scheduler; -import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.dom.client.ClickEvent; @@ -318,12 +317,7 @@ } private Runnable gotoLine() { - return new Runnable() { - @Override - public void run() { - cmEdit.execCommand("jumpToLine"); - } - }; + return () -> cmEdit.execCommand("jumpToLine"); } @Override @@ -472,21 +466,9 @@ cmEdit.setOption(option, value); } - void setTheme(final Theme newTheme) { - cmBase.operation( - new Runnable() { - @Override - public void run() { - cmBase.setOption("theme", newTheme.name().toLowerCase()); - } - }); - cmEdit.operation( - new Runnable() { - @Override - public void run() { - cmEdit.setOption("theme", newTheme.name().toLowerCase()); - } - }); + void setTheme(Theme newTheme) { + cmBase.operation(() -> cmBase.setOption("theme", newTheme.name().toLowerCase())); + cmEdit.operation(() -> cmEdit.setOption("theme", newTheme.name().toLowerCase())); } void setLineLength(int length) { @@ -504,21 +486,9 @@ cmEdit.setOption("lineNumbers", show); } - void setShowWhitespaceErrors(final boolean show) { - cmBase.operation( - new Runnable() { - @Override - public void run() { - cmBase.setOption("showTrailingSpace", show); - } - }); - cmEdit.operation( - new Runnable() { - @Override - public void run() { - cmEdit.setOption("showTrailingSpace", show); - } - }); + void setShowWhitespaceErrors(boolean show) { + cmBase.operation(() -> cmBase.setOption("showTrailingSpace", show)); + cmEdit.operation(() -> cmEdit.setOption("showTrailingSpace", show)); } void setShowTabs(boolean show) { @@ -648,29 +618,13 @@ } private Runnable updateCursorPosition() { - return new Runnable() { - @Override - public void run() { - // The rendering of active lines has to be deferred. Reflow - // caused by adding and removing styles chokes Firefox when arrow - // key (or j/k) is held down. Performance on Chrome is fine - // without the deferral. - // - Scheduler.get() - .scheduleDeferred( - new ScheduledCommand() { - @Override - public void execute() { - cmEdit.operation( - new Runnable() { - @Override - public void run() { - updateActiveLine(); - } - }); - } - }); - } + return () -> { + // The rendering of active lines has to be deferred. Reflow + // caused by adding and removing styles chokes Firefox when arrow + // key (or j/k) is held down. Performance on Chrome is fine + // without the deferral. + // + Scheduler.get().scheduleDeferred(() -> cmEdit.operation(this::updateActiveLine)); }; } @@ -688,37 +642,34 @@ } private Runnable save() { - return new Runnable() { - @Override - public void run() { - if (!cmEdit.isClean(generation)) { - close.setEnabled(false); - String text = cmEdit.getValue(); - if (Patch.COMMIT_MSG.equals(path)) { - String trimmed = text.trim() + "\r"; - if (!trimmed.equals(text)) { - text = trimmed; - cmEdit.setValue(text); - } + return () -> { + if (!cmEdit.isClean(generation)) { + close.setEnabled(false); + String text = cmEdit.getValue(); + if (Patch.COMMIT_MSG.equals(path)) { + String trimmed = text.trim() + "\r"; + if (!trimmed.equals(text)) { + text = trimmed; + cmEdit.setValue(text); } - final int g = cmEdit.changeGeneration(false); - ChangeEditApi.put( - revision.getParentKey().get(), - path, - text, - new GerritCallback<VoidResult>() { - @Override - public void onSuccess(VoidResult result) { - generation = g; - setClean(cmEdit.isClean(g)); - } - - @Override - public void onFailure(final Throwable caught) { - close.setEnabled(true); - } - }); } + final int g = cmEdit.changeGeneration(false); + ChangeEditApi.put( + revision.getParentKey().get(), + path, + text, + new GerritCallback<VoidResult>() { + @Override + public void onSuccess(VoidResult result) { + generation = g; + setClean(cmEdit.isClean(g)); + } + + @Override + public void onFailure(Throwable caught) { + close.setEnabled(true); + } + }); } }; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java index 74cfaf1..01c4d26 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
@@ -44,7 +44,7 @@ } /** Check if the current user is owner of a group */ - public static void isGroupOwner(String groupName, final AsyncCallback<Boolean> cb) { + public static void isGroupOwner(String groupName, AsyncCallback<Boolean> cb) { GroupMap.myOwned( groupName, new AsyncCallback<GroupMap>() { @@ -105,7 +105,7 @@ /** Add members to a group. */ public static void addMembers( - AccountGroup.UUID group, Set<String> members, final AsyncCallback<JsArray<AccountInfo>> cb) { + AccountGroup.UUID group, Set<String> members, AsyncCallback<JsArray<AccountInfo>> cb) { if (members.size() == 1) { addMember( group, @@ -132,7 +132,7 @@ /** Remove members from a group. */ public static void removeMembers( - AccountGroup.UUID group, Set<Integer> ids, final AsyncCallback<VoidResult> cb) { + AccountGroup.UUID group, Set<Integer> ids, AsyncCallback<VoidResult> cb) { if (ids.size() == 1) { members(group).id(ids.iterator().next().toString()).delete(cb); } else { @@ -181,7 +181,7 @@ /** Remove included groups from a group. */ public static void removeIncludedGroups( - AccountGroup.UUID group, Set<AccountGroup.UUID> ids, final AsyncCallback<VoidResult> cb) { + AccountGroup.UUID group, Set<AccountGroup.UUID> ids, AsyncCallback<VoidResult> cb) { if (ids.size() == 1) { AccountGroup.UUID g = ids.iterator().next(); groups(group).id(g.get()).delete(cb);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java index 738319d..c76ec84 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -57,6 +57,12 @@ public final native InheritedBooleanInfo rejectImplicitMerges() /*-{ return this.reject_implicit_merges; }-*/ ; + public final native InheritedBooleanInfo enableReviewerByEmail() + /*-{ return this.enable_reviewer_by_email; }-*/ ; + + public final native InheritedBooleanInfo matchAuthorToCommitterDate() + /*-{ return this.match_author_to_committer_date; }-*/ ; + public final SubmitType submitType() { return SubmitType.valueOf(submitTypeRaw()); } @@ -113,6 +119,9 @@ final native ThemeInfo theme() /*-{ return this.theme; }-*/; + final native NativeMap<JsArrayString> + extensionPanelNames() /*-{ return this.extension_panel_names; }-*/; + protected ConfigInfo() {} static class CommentLinkInfo extends JavaScriptObject {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java index e41cf120..7e2f2be 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfoCache.java
@@ -16,12 +16,14 @@ import com.google.gerrit.client.changes.ChangeApi; import com.google.gerrit.client.info.ChangeInfo; +import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.ui.CommentLinkProcessor; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.rpc.AsyncCallback; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; /** Cache of {@link ConfigInfo} objects by project name. */ @@ -48,6 +50,10 @@ public ThemeInfo getTheme() { return info.theme(); } + + public List<String> getExtensionPanelNames(String extensionPoint) { + return Natives.asList(info.extensionPanelNames().get(extensionPoint)); + } } public static void get(Project.NameKey name, AsyncCallback<Entry> cb) { @@ -87,7 +93,7 @@ }; } - private void getImpl(final String name, final AsyncCallback<Entry> cb) { + private void getImpl(String name, AsyncCallback<Entry> cb) { Entry e = cache.get(name); if (e != null) { cb.onSuccess(e); @@ -110,7 +116,7 @@ }); } - private void getImpl(final Integer id, final AsyncCallback<Entry> cb) { + private void getImpl(Integer id, AsyncCallback<Entry> cb) { String name = changeToProject.get(id); if (name != null) { getImpl(name, cb);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java index 4be877e..181fd73 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -147,6 +147,8 @@ InheritableBoolean enableSignedPush, InheritableBoolean requireSignedPush, InheritableBoolean rejectImplicitMerges, + InheritableBoolean enableReviewerByEmail, + InheritableBoolean matchAuthorToCommitterDate, String maxObjectSizeLimit, SubmitType submitType, ProjectState state, @@ -170,11 +172,13 @@ in.setSubmitType(submitType); in.setState(state); in.setPluginConfigValues(pluginConfigValues); + in.setEnableReviewerByEmail(enableReviewerByEmail); + in.setMatchAuthorToCommitterDate(matchAuthorToCommitterDate); project(name).view("config").put(in, cb); } - public static void getParent(Project.NameKey name, final AsyncCallback<Project.NameKey> cb) { + public static void getParent(Project.NameKey name, AsyncCallback<Project.NameKey> cb) { project(name) .view("parent") .get( @@ -294,6 +298,20 @@ setRequireSignedPushRaw(v.name()); } + final void setEnableReviewerByEmail(InheritableBoolean v) { + setEnableReviewerByEmailRaw(v.name()); + } + + final void setMatchAuthorToCommitterDate(InheritableBoolean v) { + setMatchAuthorToCommitterDateRaw(v.name()); + } + + private native void setMatchAuthorToCommitterDateRaw(String v) + /*-{ if(v)this.match_author_to_committer_date=v; }-*/ ; + + private native void setEnableReviewerByEmailRaw(String v) + /*-{ if(v)this.enable_reviewer_by_email=v; }-*/ ; + private native void setRequireSignedPushRaw(String v) /*-{ if(v)this.require_signed_push=v; }-*/ ;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java index 4327c07..5ff300d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
@@ -44,9 +44,9 @@ .get(NativeMap.copyKeysIntoChildren(callback)); } - public static void suggest(String prefix, int limit, AsyncCallback<ProjectMap> cb) { + public static void suggest(String match, int limit, AsyncCallback<ProjectMap> cb) { new RestApi("/projects/") - .addParameter("p", prefix) + .addParameter("m", match) .addParameter("n", limit) .addParameterRaw("type", "ALL") .addParameterTrue("d") // description
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java index 90a820f..af32d01 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
@@ -65,7 +65,7 @@ return add(cb); } - public <T> Callback<T> add(final AsyncCallback<T> cb) { + public <T> Callback<T> add(AsyncCallback<T> cb) { checkFinalAdded(); return handleAdd(cb); } @@ -75,13 +75,13 @@ return handleAdd(cb); } - public <T> Callback<T> addFinal(final AsyncCallback<T> cb) { + public <T> Callback<T> addFinal(AsyncCallback<T> cb) { checkFinalAdded(); finalAdded = true; return handleAdd(cb); } - public <T> HttpCallback<T> addFinal(final HttpCallback<T> cb) { + public <T> HttpCallback<T> addFinal(HttpCallback<T> cb) { checkFinalAdded(); finalAdded = true; return handleAdd(cb);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java index 5688a31..2d6723a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
@@ -32,7 +32,7 @@ implements com.google.gwtjsonrpc.common.AsyncCallback<T>, com.google.gwt.user.client.rpc.AsyncCallback<T> { @Override - public void onFailure(final Throwable caught) { + public void onFailure(Throwable caught) { showFailure(caught); } @@ -77,7 +77,7 @@ return false; } - protected static boolean isInvalidXSRF(final Throwable caught) { + protected static boolean isInvalidXSRF(Throwable caught) { return caught instanceof InvocationException && caught.getMessage().equals(JsonConstants.ERROR_INVALID_XSRF); } @@ -94,17 +94,17 @@ && caught.getMessage().equals(NoSuchEntityException.MESSAGE)); } - protected static boolean isNoSuchAccount(final Throwable caught) { + protected static boolean isNoSuchAccount(Throwable caught) { return caught instanceof RemoteJsonException && caught.getMessage().startsWith(NoSuchAccountException.MESSAGE); } - protected static boolean isNameAlreadyUsed(final Throwable caught) { + protected static boolean isNameAlreadyUsed(Throwable caught) { return caught instanceof RemoteJsonException && caught.getMessage().startsWith(NameAlreadyUsedException.MESSAGE); } - protected static boolean isNoSuchGroup(final Throwable caught) { + protected static boolean isNoSuchGroup(Throwable caught) { return caught instanceof RemoteJsonException && caught.getMessage().startsWith(NoSuchGroupException.MESSAGE); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java index 250bc6e..8b0fefb 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -116,7 +116,7 @@ } @Override - public void onResponseReceived(Request req, final Response res) { + public void onResponseReceived(Request req, Response res) { int status = res.getStatusCode(); if (status == Response.SC_NO_CONTENT) { cb.onSuccess(new HttpResponse<T>(res, null, null)); @@ -499,7 +499,7 @@ } } - private static <T extends JavaScriptObject> HttpCallback<T> wrap(final AsyncCallback<T> cb) { + private static <T extends JavaScriptObject> HttpCallback<T> wrap(AsyncCallback<T> cb) { return new HttpCallback<T>() { @Override public void onSuccess(HttpResponse<T> r) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java index 74b45df..3aae04a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java
@@ -24,12 +24,12 @@ public abstract class ScreenLoadCallback<T> extends GerritCallback<T> { private final Screen screen; - public ScreenLoadCallback(final Screen s) { + public ScreenLoadCallback(Screen s) { screen = s; } @Override - public final void onSuccess(final T result) { + public final void onSuccess(T result) { if (screen.isAttached()) { preDisplay(result); screen.display(); @@ -42,7 +42,7 @@ protected void postDisplay() {} @Override - public void onFailure(final Throwable caught) { + public void onFailure(Throwable caught) { if (isSigninFailure(caught)) { new NotSignedInDialog().center(); } else if (isNoSuchEntity(caught)) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java index 80b8c66..bdebd68 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
@@ -32,7 +32,7 @@ private Project.NameKey projectName; @Override - public void _onRequestSuggestions(final Request req, final Callback callback) { + public void _onRequestSuggestions(Request req, Callback callback) { GroupMap.suggestAccountGroupForProject( projectName == null ? null : projectName.get(), req.getQuery(), @@ -58,7 +58,7 @@ private static class AccountGroupSuggestion implements SuggestOracle.Suggestion { private final GroupInfo info; - AccountGroupSuggestion(final GroupInfo k) { + AccountGroupSuggestion(GroupInfo k) { info = k; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java index 78ae156..5038ad9 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
@@ -27,7 +27,7 @@ /** Suggestion Oracle for Account entities. */ public class AccountSuggestOracle extends SuggestAfterTypingNCharsOracle { @Override - public void _onRequestSuggestions(final Request req, final Callback cb) { + public void _onRequestSuggestions(Request req, Callback cb) { AccountApi.suggest( req.getQuery(), req.getLimit(),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java index 5d8d56c..a1d2229 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java
@@ -29,8 +29,7 @@ private final Button addMember; private final RemoteSuggestBox suggestBox; - public AddMemberBox( - final String buttonLabel, final String hint, final SuggestOracle suggestOracle) { + public AddMemberBox(final String buttonLabel, String hint, SuggestOracle suggestOracle) { addPanel = new FlowPanel(); addMember = new Button(buttonLabel);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java index 1ae4489..68477bc 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
@@ -20,13 +20,13 @@ import com.google.gwt.core.client.GWT; public class ChangeLink extends InlineHyperlink { - public static String permalink(final Change.Id c) { + public static String permalink(Change.Id c) { return GWT.getHostPageBaseURL() + c.get(); } protected Change.Id cid; - public ChangeLink(final String text, final Change.Id c) { + public ChangeLink(String text, Change.Id c) { super(text, PageLinks.toChange(c)); getElement().setPropertyString("href", permalink(c)); cid = c;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java index 85552c9..0a0c14a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CherryPickDialog.java
@@ -51,7 +51,7 @@ @Override protected void onRequestSuggestions(Request request, Callback done) { List<BranchSuggestion> suggestions = new ArrayList<>(); - for (final BranchInfo b : branches) { + for (BranchInfo b : branches) { if (b.ref().contains(request.getQuery())) { suggestions.add(new BranchSuggestion(b)); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommandMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommandMenuItem.java index 72bf06c..c5ee34f 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommandMenuItem.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommandMenuItem.java
@@ -24,7 +24,7 @@ public class CommandMenuItem extends Anchor implements ClickHandler { private final Command command; - public CommandMenuItem(final String text, final Command cmd) { + public CommandMenuItem(String text, Command cmd) { super(text); setStyleName(Gerrit.RESOURCES.css().menuItem()); Roles.getMenuitemRole().set(getElement()); @@ -33,7 +33,7 @@ } @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { setFocus(false); command.execute(); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java index d497740..b68f329 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
@@ -37,7 +37,7 @@ protected boolean sent; - public CommentedActionDialog(final String title, final String heading) { + public CommentedActionDialog(String title, String heading) { super(/* auto hide */ false, /* modal */ true); setGlassEnabled(true); setText(title); @@ -48,7 +48,7 @@ sendButton.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { enableButtons(false); onSend(); } @@ -59,7 +59,7 @@ cancelButton.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { hide(); } });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java index f65fb1b..c0b662a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ComplexDisclosurePanel.java
@@ -32,7 +32,7 @@ private final DisclosurePanel main; private final Panel header; - public ComplexDisclosurePanel(final String text, final boolean isOpen) { + public ComplexDisclosurePanel(String text, boolean isOpen) { // Ick. GWT's DisclosurePanel won't let us subclass it, or do any // other modification of its header. We're stuck with injecting // into the DOM directly. @@ -81,7 +81,7 @@ return header; } - public void setContent(final Widget w) { + public void setContent(Widget w) { main.setContent(w); } @@ -90,12 +90,12 @@ } @Override - public HandlerRegistration addOpenHandler(final OpenHandler<DisclosurePanel> h) { + public HandlerRegistration addOpenHandler(OpenHandler<DisclosurePanel> h) { return main.addOpenHandler(h); } @Override - public HandlerRegistration addCloseHandler(final CloseHandler<DisclosurePanel> h) { + public HandlerRegistration addCloseHandler(CloseHandler<DisclosurePanel> h) { return main.addCloseHandler(h); } @@ -109,7 +109,7 @@ * * @param isOpen {@code true} to open, {@code false} to close */ - public void setOpen(final boolean isOpen) { + public void setOpen(boolean isOpen) { main.setOpen(isOpen); } }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java index a9a17210..045e0ae 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
@@ -48,11 +48,11 @@ return new MyFlexTable(); } - protected RowItem getRowItem(final int row) { + protected RowItem getRowItem(int row) { return FancyFlexTable.<RowItem>getRowItem(table.getCellFormatter().getElement(row, 0)); } - protected void setRowItem(final int row, final RowItem item) { + protected void setRowItem(int row, RowItem item) { setRowItem(table.getCellFormatter().getElement(row, 0), item); } @@ -117,15 +117,15 @@ return left; } - protected void resetHtml(final SafeHtml body) { - for (final Iterator<Widget> i = table.iterator(); i.hasNext(); ) { + protected void resetHtml(SafeHtml body) { + for (Iterator<Widget> i = table.iterator(); i.hasNext(); ) { i.next(); i.remove(); } impl.resetHtml(table, body); } - protected void scrollIntoView(final int topRow, final int endRow) { + protected void scrollIntoView(int topRow, int endRow) { final CellFormatter fmt = table.getCellFormatter(); final Element top = fmt.getElement(topRow, C_ARROW).getParentElement(); final Element end = fmt.getElement(endRow, C_ARROW).getParentElement(); @@ -164,7 +164,7 @@ Document.get().setScrollTop(nTop); } - protected void applyDataRowStyle(final int newRow) { + protected void applyDataRowStyle(int newRow) { table.getCellFormatter().addStyleName(newRow, C_ARROW, Gerrit.RESOURCES.css().iconCell()); table.getCellFormatter().addStyleName(newRow, C_ARROW, Gerrit.RESOURCES.css().leftMostCell()); } @@ -176,7 +176,7 @@ * @return the td containing element {@code target}; null if {@code target} is not a member of * this table. */ - protected Element getParentCell(final Element target) { + protected Element getParentCell(Element target) { final Element body = FancyFlexTableImpl.getBodyElement(table); for (Element td = target; td != null && td != body; td = DOM.getParent(td)) { // If it's a TD, it might be the one we're looking for. @@ -192,7 +192,7 @@ } /** @return the row of the child element; -1 if the child is not in the table. */ - protected int rowOf(final Element target) { + protected int rowOf(Element target) { final Element td = getParentCell(target); if (td == null) { return -1; @@ -203,7 +203,7 @@ } /** @return the cell of the child element; -1 if the child is not in the table. */ - protected int columnOf(final Element target) { + protected int columnOf(Element target) { final Element td = getParentCell(target); if (td == null) { return -1;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java index ded0140..a3a2a7a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImpl.java
@@ -20,7 +20,7 @@ import com.google.gwtexpui.safehtml.client.SafeHtml; public class FancyFlexTableImpl { - public void resetHtml(final FlexTable myTable, final SafeHtml body) { + public void resetHtml(FlexTable myTable, SafeHtml body) { SafeHtml.setInnerHTML(getBodyElement(myTable), body); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE8.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE8.java index a648412..3eae0f8 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE8.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTableImplIE8.java
@@ -23,7 +23,7 @@ public class FancyFlexTableImplIE8 extends FancyFlexTableImpl { @Override - public void resetHtml(final FlexTable myTable, final SafeHtml bodyHtml) { + public void resetHtml(FlexTable myTable, SafeHtml bodyHtml) { final Element oldBody = getBodyElement(myTable); final Element newBody = parseBody(bodyHtml); assert newBody != null; @@ -34,7 +34,7 @@ DOM.appendChild(tableElem, newBody); } - private static Element parseBody(final SafeHtml body) { + private static Element parseBody(SafeHtml body) { final SafeHtmlBuilder b = new SafeHtmlBuilder(); b.openElement("table"); b.append(body);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingInlineHyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingInlineHyperlink.java index 6e1fb09..f8e382a 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingInlineHyperlink.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingInlineHyperlink.java
@@ -18,8 +18,7 @@ private String toHighlight; - public HighlightingInlineHyperlink( - final String text, final String token, final String toHighlight) { + public HighlightingInlineHyperlink(final String text, String token, String toHighlight) { super(text, token); this.toHighlight = toHighlight; highlight(text, toHighlight); @@ -31,7 +30,7 @@ highlight(text, toHighlight); } - private void highlight(final String text, final String toHighlight) { + private void highlight(String text, String toHighlight) { setHTML(Util.highlight(text, toHighlight)); } }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingProjectsTable.java index 643c766..1e3be3f 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingProjectsTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingProjectsTable.java
@@ -21,13 +21,13 @@ public class HighlightingProjectsTable extends ProjectsTable { private String toHighlight; - public void display(final ProjectMap projects, final String toHighlight) { + public void display(ProjectMap projects, String toHighlight) { this.toHighlight = toHighlight; super.display(projects); } @Override - protected void populate(final int row, final ProjectInfo k) { + protected void populate(int row, ProjectInfo k) { populateState(row, k); table.setWidget( row, ProjectsTable.C_NAME, new InlineHTML(Util.highlight(k.name(), toHighlight)));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java index f8ad835..4ccfe9d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java
@@ -127,7 +127,7 @@ addKeyDownHandler( new KeyDownHandler() { @Override - public void onKeyDown(final KeyDownEvent event) { + public void onKeyDown(KeyDownEvent event) { onKey(event.getNativeKeyCode()); } });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java index 6c28145..c35d097 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java
@@ -35,7 +35,7 @@ * @param token the history token to which it will link, which may not be null (use {@link Anchor} * instead if you don't need history processing) */ - public Hyperlink(final String text, final String token) { + public Hyperlink(String text, String token) { super(text, token); } @@ -52,7 +52,7 @@ } @Override - public void onBrowserEvent(final Event event) { + public void onBrowserEvent(Event event) { if (DOM.eventGetType(event) == Event.ONCLICK && impl.handleAsClick(event)) { event.preventDefault(); go();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java index 24f2887..a4edb5b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/InlineHyperlink.java
@@ -28,7 +28,7 @@ * @param text the hyperlink's text * @param token the history token to which it will link */ - public InlineHyperlink(final String text, final String token) { + public InlineHyperlink(String text, String token) { super(text, token); } @@ -36,7 +36,7 @@ public InlineHyperlink() {} @Override - public void onBrowserEvent(final Event event) { + public void onBrowserEvent(Event event) { if (DOM.eventGetType(event) == Event.ONCLICK && impl.handleAsClick(event)) { event.preventDefault(); go();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java index d08b6f9..d3db098 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
@@ -32,20 +32,20 @@ Gerrit.EVENT_BUS.addHandler(ScreenLoadEvent.TYPE, this); } - public void addItem(final String text, final Command imp) { + public void addItem(String text, Command imp) { add(new CommandMenuItem(text, imp)); } - public void addItem(final CommandMenuItem i) { + public void addItem(CommandMenuItem i) { add(i); } - public void addItem(final LinkMenuItem i) { + public void addItem(LinkMenuItem i) { i.setMenuBar(this); add(i); } - public void insertItem(final LinkMenuItem i, int beforeIndex) { + public void insertItem(LinkMenuItem i, int beforeIndex) { i.setMenuBar(this); insert(i, beforeIndex); } @@ -66,7 +66,7 @@ return null; } - public void add(final Widget i) { + public void add(Widget i) { if (body.getWidgetCount() > 0) { final Widget p = body.getWidget(body.getWidgetCount() - 1); p.addStyleName(Gerrit.RESOURCES.css().linkMenuItemNotLast()); @@ -74,7 +74,7 @@ body.add(i); } - public void insert(final Widget i, int beforeIndex) { + public void insert(Widget i, int beforeIndex) { if (body.getWidgetCount() == 0 || body.getWidgetCount() <= beforeIndex) { add(i); return;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java index 9cc91a0..8a8ab25 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java
@@ -21,7 +21,7 @@ public class LinkMenuItem extends InlineHyperlink implements ScreenLoadHandler { private LinkMenuBar bar; - public LinkMenuItem(final String text, final String targetHistoryToken) { + public LinkMenuItem(String text, String targetHistoryToken) { super(text, targetHistoryToken); setStyleName(Gerrit.RESOURCES.css().menuItem()); Roles.getMenuitemRole().set(getElement());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java index 2c614b5..0f28ddc 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java
@@ -54,22 +54,22 @@ } @Override - protected void add(final Widget w) { + protected void add(Widget w) { body.add(w); } - protected void link(final String text, final String target) { + protected void link(String text, String target) { link(text, target, true); } - protected void link(final String text, final String target, final boolean visible) { + protected void link(String text, String target, boolean visible) { final LinkMenuItem item = new LinkMenuItem(text, target); item.setStyleName(Gerrit.RESOURCES.css().menuItem()); item.setVisible(visible); menu.add(item); } - protected void setLinkVisible(final String token, final boolean visible) { + protected void setLinkVisible(String token, boolean visible) { final LinkMenuItem item = menu.find(token); item.setVisible(visible); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java index 8975dda..7e34730 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
@@ -42,7 +42,7 @@ } @Override - public void onBrowserEvent(final Event event) { + public void onBrowserEvent(Event event) { switch (DOM.eventGetType(event)) { case Event.ONCLICK: { @@ -198,11 +198,11 @@ } } - protected void movePointerTo(final int newRow) { + protected void movePointerTo(int newRow) { movePointerTo(newRow, true); } - protected void movePointerTo(final int newRow, final boolean scroll) { + protected void movePointerTo(int newRow, boolean scroll) { final CellFormatter fmt = table.getCellFormatter(); final boolean clear = 0 <= currentRow && currentRow < table.getRowCount(); if (clear) { @@ -223,7 +223,7 @@ currentRow = newRow; } - protected void scrollIntoView(final Element tr) { + protected void scrollIntoView(Element tr) { if (!computedScrollType) { parentScrollPanel = null; Widget w = getParent(); @@ -280,14 +280,14 @@ } } - protected void movePointerTo(final Object oldId) { + protected void movePointerTo(Object oldId) { final int row = findRow(oldId); if (0 <= row) { movePointerTo(row); } } - protected int findRow(final Object oldId) { + protected int findRow(Object oldId) { if (oldId != null) { final int max = table.getRowCount(); for (int row = 0; row < max; row++) { @@ -318,11 +318,11 @@ } } - public void setSavePointerId(final String id) { + public void setSavePointerId(String id) { saveId = id; } - public void setRegisterKeys(final boolean on) { + public void setRegisterKeys(boolean on) { if (on && isAttached()) { if (regNavigation == null) { regNavigation = GlobalKey.add(this, keysNavigation); @@ -375,7 +375,7 @@ } @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { ensurePointerVisible(); onUp(); } @@ -387,7 +387,7 @@ } @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { ensurePointerVisible(); onDown(); } @@ -399,7 +399,7 @@ } @Override - public void onKeyPress(final KeyPressEvent event) { + public void onKeyPress(KeyPressEvent event) { ensurePointerVisible(); onOpen(); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java index 87de3b7..2c7fcd4 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
@@ -54,33 +54,33 @@ // The first parameter to the contructors must be the FocusWidget to enable, // subsequent parameters are widgets to listenTo. - public OnEditEnabler(final FocusWidget w, final TextBoxBase tb) { + public OnEditEnabler(FocusWidget w, TextBoxBase tb) { this(w); originalValue = tb.getValue().trim(); listenTo(tb); } - public OnEditEnabler(final FocusWidget w, final ListBox lb) { + public OnEditEnabler(FocusWidget w, ListBox lb) { this(w); listenTo(lb); } - public OnEditEnabler(final FocusWidget w, final CheckBox cb) { + public OnEditEnabler(FocusWidget w, CheckBox cb) { this(w); listenTo(cb); } - public OnEditEnabler(final FocusWidget w) { + public OnEditEnabler(FocusWidget w) { widget = w; } - public void updateOriginalValue(final TextBoxBase tb) { + public void updateOriginalValue(TextBoxBase tb) { originalValue = tb.getValue().trim(); } // Register input widgets to be listened to - public void listenTo(final TextBoxBase tb) { + public void listenTo(TextBoxBase tb) { strings.put(tb, tb.getText().trim()); tb.addKeyPressHandler(this); @@ -105,44 +105,44 @@ tb.addKeyDownHandler(this); } - public void listenTo(final ListBox lb) { + public void listenTo(ListBox lb) { lb.addChangeHandler(this); } @SuppressWarnings({"unchecked", "rawtypes"}) - public void listenTo(final CheckBox cb) { + public void listenTo(CheckBox cb) { cb.addValueChangeHandler((ValueChangeHandler) this); } // Handlers @Override - public void onKeyPress(final KeyPressEvent e) { + public void onKeyPress(KeyPressEvent e) { on(e); } @Override - public void onKeyDown(final KeyDownEvent e) { + public void onKeyDown(KeyDownEvent e) { on(e); } @Override - public void onMouseUp(final MouseUpEvent e) { + public void onMouseUp(MouseUpEvent e) { on(e); } @Override - public void onChange(final ChangeEvent e) { + public void onChange(ChangeEvent e) { on(e); } @SuppressWarnings("rawtypes") @Override - public void onValueChange(final ValueChangeEvent e) { + public void onValueChange(ValueChangeEvent e) { on(e); } - private void on(final GwtEvent<?> e) { + private void on(GwtEvent<?> e) { if (widget.isEnabled() || !(e.getSource() instanceof FocusWidget) || !((FocusWidget) e.getSource()).isEnabled()) { @@ -172,7 +172,7 @@ } } - private void onTextBoxBase(final TextBoxBase tb) { + private void onTextBoxBase(TextBoxBase tb) { // The text appears to not get updated until the handlers complete. Scheduler.get() .scheduleDeferred(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ParentProjectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ParentProjectBox.java index fab0cf7..7c45a20 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ParentProjectBox.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ParentProjectBox.java
@@ -38,11 +38,11 @@ suggestBox.setVisibleLength(len); } - public void setProject(final Project.NameKey project) { + public void setProject(Project.NameKey project) { suggestOracle.setProject(project); } - public void setParentProject(final Project.NameKey parent) { + public void setParentProject(Project.NameKey parent) { suggestBox.setText(parent != null ? parent.get() : ""); } @@ -77,7 +77,7 @@ } @Override - public void _onRequestSuggestions(Request req, final Callback callback) { + public void _onRequestSuggestions(Request req, Callback callback) { super._onRequestSuggestions( req, new Callback() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java index cace84b..89bff71 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
@@ -52,7 +52,7 @@ private boolean poppingUp; private boolean firstPopupLoad = true; - public void initPopup(final String popupText, final String currentPageLink) { + public void initPopup(String popupText, String currentPageLink) { createWidgets(popupText, currentPageLink); final FlowPanel pfp = new FlowPanel(); pfp.add(filterPanel); @@ -109,7 +109,7 @@ return poppingUp; } - private void createWidgets(final String popupText, final String currentPageLink) { + private void createWidgets(String popupText, String currentPageLink) { filterPanel = new HorizontalPanel(); filterPanel.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel()); final Label filterLabel = @@ -135,13 +135,13 @@ projectsTab = new HighlightingProjectsTable() { @Override - protected void movePointerTo(final int row, final boolean scroll) { + protected void movePointerTo(int row, boolean scroll) { super.movePointerTo(row, scroll); onMovePointerTo(getRowItem(row).name()); } @Override - protected void onOpenRow(final int row) { + protected void onOpenRow(int row) { super.onOpenRow(row); openRow(getRowItem(row).name()); } @@ -161,7 +161,7 @@ close.addClickHandler( new ClickHandler() { @Override - public void onClick(final ClickEvent event) { + public void onClick(ClickEvent event) { closePopup(); } }); @@ -188,7 +188,7 @@ popup.hide(); } - public void setPreferredCoordinates(final int top, final int left) { + public void setPreferredCoordinates(int top, int left) { this.preferredTop = top; this.preferredLeft = left; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java index 2767a05..f2ebf81 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
@@ -21,7 +21,7 @@ /** Suggestion Oracle for Project.NameKey entities. */ public class ProjectNameSuggestOracle extends SuggestAfterTypingNCharsOracle { @Override - public void _onRequestSuggestions(final Request req, final Callback callback) { + public void _onRequestSuggestions(Request req, Callback callback) { ProjectMap.suggest( req.getQuery(), req.getLimit(),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java index 99d0e8e..ac89180 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
@@ -48,12 +48,12 @@ } @Override - protected Object getRowItemKey(final ProjectInfo item) { + protected Object getRowItemKey(ProjectInfo item) { return item.name(); } @Override - protected void onOpenRow(final int row) { + protected void onOpenRow(int row) { if (row > 0) { movePointerTo(row); } @@ -84,7 +84,7 @@ finishDisplay(); } - protected void insert(final int row, final ProjectInfo k) { + protected void insert(int row, ProjectInfo k) { table.insertRow(row); applyDataRowStyle(row); @@ -98,7 +98,7 @@ populate(row, k); } - protected void populate(final int row, final ProjectInfo k) { + protected void populate(int row, ProjectInfo k) { populateState(row, k); table.setText(row, C_NAME, k.name()); table.setText(row, C_DESCRIPTION, k.description());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java index b0ee915..03ed899 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
@@ -96,12 +96,12 @@ header.getCellFormatter().setWidth(0, Cols.FarEast.ordinal(), "100%"); } - protected void setWindowTitle(final String text) { + protected void setWindowTitle(String text) { windowTitle = text; Gerrit.setWindowTitle(this, text); } - protected void setPageTitle(final String text) { + protected void setPageTitle(String text) { final String old = headerText.getText(); if (text.isEmpty()) { header.setVisible(false); @@ -118,23 +118,23 @@ header.setVisible(value); } - public void setTitle(final Widget w) { + public void setTitle(Widget w) { titleWidget = w; } - protected void setTitleEast(final Widget w) { + protected void setTitleEast(Widget w) { header.setWidget(0, Cols.East.ordinal(), w); } - protected void setTitleFarEast(final Widget w) { + protected void setTitleFarEast(Widget w) { header.setWidget(0, Cols.FarEast.ordinal(), w); } - protected void setTitleWest(final Widget w) { + protected void setTitleWest(Widget w) { header.setWidget(0, Cols.West.ordinal(), w); } - protected void add(final Widget w) { + protected void add(Widget w) { body.add(w); } @@ -142,7 +142,7 @@ return body; } - protected void setTheme(final ThemeInfo t) { + protected void setTheme(ThemeInfo t) { theme = t; } @@ -152,7 +152,7 @@ } /** Set the history token for this screen. */ - public void setToken(final String t) { + public void setToken(String t) { assert t != null && !t.isEmpty(); token = t; @@ -172,7 +172,7 @@ } /** Set whether or not {@link Gerrit#isSignedIn()} must be true. */ - public final void setRequiresSignIn(final boolean b) { + public final void setRequiresSignIn(boolean b) { requiresSignIn = b; }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SmallHeading.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SmallHeading.java index b76c2fe..ea18d62 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SmallHeading.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SmallHeading.java
@@ -22,7 +22,7 @@ setStyleName(Gerrit.RESOURCES.css().smallHeading()); } - public SmallHeading(final String text) { + public SmallHeading(String text) { this(); setText(text); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java index 26026e1..41e3573 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java
@@ -21,7 +21,7 @@ public static final UIConstants C = GWT.create(UIConstants.class); public static final UIMessages M = GWT.create(UIMessages.class); - public static String highlight(final String text, final String toHighlight) { + public static String highlight(String text, String toHighlight) { final SafeHtmlBuilder b = new SafeHtmlBuilder(); if (toHighlight == null || "".equals(toHighlight)) { b.append(text);
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/addon/AddonInjector.java b/gerrit-gwtui/src/main/java/net/codemirror/addon/AddonInjector.java index ce91a46..cb1891e 100644 --- a/gerrit-gwtui/src/main/java/net/codemirror/addon/AddonInjector.java +++ b/gerrit-gwtui/src/main/java/net/codemirror/addon/AddonInjector.java
@@ -68,7 +68,7 @@ } } - private void beginLoading(final String addon) { + private void beginLoading(String addon) { pending++; Loader.injectScript( getAddonScriptUri(addon),
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java index 582a3109..01bc7e2 100644 --- a/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java +++ b/gerrit-gwtui/src/main/java/net/codemirror/lib/Loader.java
@@ -29,7 +29,7 @@ public class Loader { private static native boolean isLibLoaded() /*-{ return $wnd.hasOwnProperty('CodeMirror'); }-*/; - static void initLibrary(final AsyncCallback<Void> cb) { + static void initLibrary(AsyncCallback<Void> cb) { if (isLibLoaded()) { cb.onSuccess(null); return; @@ -53,7 +53,7 @@ group.done(); } - private static void injectCss(ExternalTextResource css, final AsyncCallback<Void> cb) { + private static void injectCss(ExternalTextResource css, AsyncCallback<Void> cb) { try { css.getText( new ResourceCallback<TextResource>() { @@ -74,7 +74,7 @@ } } - public static void injectScript(SafeUri js, final AsyncCallback<Void> callback) { + public static void injectScript(SafeUri js, AsyncCallback<Void> callback) { final ScriptElement[] script = new ScriptElement[1]; script[0] = ScriptInjector.fromUrl(js.asString())
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInjector.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInjector.java index 7440102..5fda608 100644 --- a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInjector.java +++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInjector.java
@@ -70,7 +70,7 @@ } } - private void beginLoading(final String mode) { + private void beginLoading(String mode) { pending++; Loader.injectScript( ModeInfo.getModeScriptUri(mode),
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java b/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java index 1dce708..23039d4 100644 --- a/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java +++ b/gerrit-gwtui/src/main/java/net/codemirror/theme/ThemeLoader.java
@@ -74,7 +74,7 @@ private static final EnumSet<Theme> loaded = EnumSet.of(Theme.DEFAULT); - public static final void loadTheme(final Theme theme, final AsyncCallback<Void> cb) { + public static final void loadTheme(Theme theme, AsyncCallback<Void> cb) { if (loaded.contains(theme)) { cb.onSuccess(null); return;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java index 09b6f25..e6918f70 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java
@@ -97,7 +97,7 @@ } @Override - public void doFilter(ServletRequest req, ServletResponse res, final FilterChain last) + public void doFilter(ServletRequest req, ServletResponse res, FilterChain last) throws IOException, ServletException { final Iterator<AllRequestFilter> itr = filters.iterator(); new FilterChain() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java index 9676cd3..6a19be7 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -19,15 +19,17 @@ import com.google.common.base.Strings; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.HostPageData; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.httpd.WebSessionManager.Key; import com.google.gerrit.httpd.WebSessionManager.Val; +import com.google.gerrit.httpd.restapi.ParameterParser; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.AccessPath; import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AuthResult; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.config.AuthConfig; import com.google.inject.Provider; import com.google.inject.servlet.RequestScoped; @@ -56,12 +58,12 @@ private CurrentUser user; protected CacheBasedWebSession( - final HttpServletRequest request, - final HttpServletResponse response, - final WebSessionManager manager, - final AuthConfig authConfig, - final Provider<AnonymousUser> anonymousProvider, - final IdentifiedUser.RequestFactory identified) { + HttpServletRequest request, + HttpServletResponse response, + WebSessionManager manager, + AuthConfig authConfig, + Provider<AnonymousUser> anonymousProvider, + IdentifiedUser.RequestFactory identified) { this.request = request; this.response = response; this.manager = manager; @@ -70,31 +72,50 @@ this.identified = identified; if (request.getRequestURI() == null || !GitSmartHttpTools.isGitClient(request)) { - String cookie = readCookie(); + String cookie = readCookie(request); if (cookie != null) { - key = new Key(cookie); - val = manager.get(key); - if (val != null && val.needsCookieRefresh()) { - // Cookie is more than half old. Send the cookie again to the - // client with an updated expiration date. - val = manager.createVal(key, val); + authFromCookie(cookie); + } else { + String token; + try { + token = ParameterParser.getQueryParams(request).accessToken(); + } catch (BadRequestException e) { + token = null; } - - String token = request.getHeader(HostPageData.XSRF_HEADER_NAME); - if (val != null && token != null && token.equals(val.getAuth())) { - okPaths.add(AccessPath.REST_API); + if (token != null) { + authFromQueryParameter(token); } } + if (val != null && val.needsCookieRefresh()) { + // Session is more than half old; update cache entry with new expiration date. + val = manager.createVal(key, val); + } } } - private String readCookie() { - final Cookie[] all = request.getCookies(); + private void authFromCookie(String cookie) { + key = new Key(cookie); + val = manager.get(key); + String token = request.getHeader(HostPageData.XSRF_HEADER_NAME); + if (val != null && token != null && token.equals(val.getAuth())) { + okPaths.add(AccessPath.REST_API); + } + } + + private void authFromQueryParameter(String accessToken) { + key = new Key(accessToken); + val = manager.get(key); + if (val != null) { + okPaths.add(AccessPath.REST_API); + } + } + + private static String readCookie(HttpServletRequest request) { + Cookie[] all = request.getCookies(); if (all != null) { - for (final Cookie c : all) { + for (Cookie c : all) { if (ACCOUNT_COOKIE.equals(c.getName())) { - final String v = c.getValue(); - return v != null && !"".equals(v) ? v : null; + return Strings.emptyToNull(c.getValue()); } } } @@ -229,7 +250,7 @@ response.addCookie(outCookie); } - private static boolean isSecure(final HttpServletRequest req) { + private static boolean isSecure(HttpServletRequest req) { return req.isSecure() || "https".equals(req.getScheme()); } }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CookieBase64.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CookieBase64.java index 11342be..52cfde7 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CookieBase64.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CookieBase64.java
@@ -32,14 +32,14 @@ enc[o] = '.'; } - private static int fill(final char[] out, int o, final char f, final int l) { + private static int fill(char[] out, int o, char f, int l) { for (char c = f; c <= l; c++) { out[o++] = c; } return o; } - static String encode(final byte[] in) { + static String encode(byte[] in) { final StringBuilder out = new StringBuilder(in.length * 4 / 3); final int len2 = in.length - 2; int d = 0; @@ -52,8 +52,7 @@ return out.toString(); } - private static void encode3to4( - final StringBuilder out, final byte[] in, final int inOffset, final int numSigBytes) { + private static void encode3to4(StringBuilder out, byte[] in, int inOffset, int numSigBytes) { // 1 2 3 // 01234567890123456789012345678901 Bit position // --------000000001111111122222222 Array position from threeBytes
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java index 825505c..be9df4c 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
@@ -32,8 +32,7 @@ } @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { String query = CharMatcher.is('/').trimTrailingFrom(req.getPathInfo()); List<ChangeInfo> results; try {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java index bbcd977..4282691 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GetUserFilter.java
@@ -41,7 +41,7 @@ private final boolean enabled; @Inject - Module(@GerritServerConfig final Config cfg) { + Module(@GerritServerConfig Config cfg) { enabled = cfg.getBoolean("http", "addUserAsRequestAttribute", true); } @@ -56,7 +56,7 @@ private final Provider<CurrentUser> userProvider; @Inject - GetUserFilter(final Provider<CurrentUser> userProvider) { + GetUserFilter(Provider<CurrentUser> userProvider) { this.userProvider = userProvider; }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java index 7a5956e..a89e2d9 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -16,11 +16,10 @@ import com.google.common.cache.Cache; import com.google.common.collect.Lists; -import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.Capable; import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.AccessPath; import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.CurrentUser; @@ -28,12 +27,12 @@ import com.google.gerrit.server.git.AsyncReceiveCommits; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.ReceiveCommits; -import com.google.gerrit.server.git.SearchingChangeCacheImpl; -import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.TransferConfig; import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.git.validators.UploadValidators; -import com.google.gerrit.server.notedb.ChangeNotes; +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.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; import com.google.inject.AbstractModule; @@ -67,6 +66,7 @@ import org.eclipse.jgit.transport.PreUploadHook; import org.eclipse.jgit.transport.PreUploadHookChain; import org.eclipse.jgit.transport.ReceivePack; +import org.eclipse.jgit.transport.ServiceMayNotContinueException; import org.eclipse.jgit.transport.UploadPack; import org.eclipse.jgit.transport.resolver.ReceivePackFactory; import org.eclipse.jgit.transport.resolver.RepositoryResolver; @@ -142,18 +142,26 @@ static class Resolver implements RepositoryResolver<HttpServletRequest> { private final GitRepositoryManager manager; - private final ProjectControl.Factory projectControlFactory; + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> userProvider; + private final ProjectControl.GenericFactory projectControlFactory; @Inject - Resolver(GitRepositoryManager manager, ProjectControl.Factory projectControlFactory) { + Resolver( + GitRepositoryManager manager, + PermissionBackend permissionBackend, + Provider<CurrentUser> userProvider, + ProjectControl.GenericFactory projectControlFactory) { this.manager = manager; + this.permissionBackend = permissionBackend; + this.userProvider = userProvider; this.projectControlFactory = projectControlFactory; } @Override public Repository open(HttpServletRequest req, String projectName) throws RepositoryNotFoundException, ServiceNotAuthorizedException, - ServiceNotEnabledException { + ServiceNotEnabledException, ServiceMayNotContinueException { while (projectName.endsWith("/")) { projectName = projectName.substring(0, projectName.length() - 1); } @@ -168,28 +176,31 @@ } } - final ProjectControl pc; - try { - pc = projectControlFactory.controlFor(new Project.NameKey(projectName)); - } catch (NoSuchProjectException err) { - throw new RepositoryNotFoundException(projectName); - } - - CurrentUser user = pc.getUser(); + CurrentUser user = userProvider.get(); user.setAccessPath(AccessPath.GIT); - if (!pc.isVisible()) { - if (user instanceof AnonymousUser) { - throw new ServiceNotAuthorizedException(); - } - throw new ServiceNotEnabledException(); - } - req.setAttribute(ATT_CONTROL, pc); - try { - return manager.openRepository(pc.getProject().getNameKey()); - } catch (IOException e) { - throw new RepositoryNotFoundException(pc.getProject().getNameKey().get(), e); + Project.NameKey nameKey = new Project.NameKey(projectName); + ProjectControl pc; + try { + pc = projectControlFactory.controlFor(nameKey, user); + } catch (NoSuchProjectException err) { + throw new RepositoryNotFoundException(projectName); + } + req.setAttribute(ATT_CONTROL, pc); + + try { + permissionBackend.user(user).project(nameKey).check(ProjectPermission.ACCESS); + } catch (AuthException e) { + if (user instanceof AnonymousUser) { + throw new ServiceNotAuthorizedException(); + } + throw new ServiceNotEnabledException(e.getMessage()); + } + + return manager.openRepository(nameKey); + } catch (IOException | PermissionBackendException err) { + throw new ServiceMayNotContinueException(projectName + " unavailable", err); } } } @@ -221,23 +232,14 @@ } static class UploadFilter implements Filter { - private final Provider<ReviewDb> db; - private final TagCache tagCache; - private final ChangeNotes.Factory changeNotesFactory; - @Nullable private final SearchingChangeCacheImpl changeCache; + private final VisibleRefFilter.Factory refFilterFactory; private final UploadValidators.Factory uploadValidatorsFactory; @Inject UploadFilter( - Provider<ReviewDb> db, - TagCache tagCache, - ChangeNotes.Factory changeNotesFactory, - @Nullable SearchingChangeCacheImpl changeCache, + VisibleRefFilter.Factory refFilterFactory, UploadValidators.Factory uploadValidatorsFactory) { - this.db = db; - this.tagCache = tagCache; - this.changeNotesFactory = changeNotesFactory; - this.changeCache = changeCache; + this.refFilterFactory = refFilterFactory; this.uploadValidatorsFactory = uploadValidatorsFactory; } @@ -263,9 +265,7 @@ uploadValidatorsFactory.create(pc.getProject(), repo, request.getRemoteHost()); up.setPreUploadHook( PreUploadHookChain.newChain(Lists.newArrayList(up.getPreUploadHook(), uploadValidators))); - up.setAdvertiseRefsHook( - new VisibleRefFilter( - tagCache, changeNotesFactory, changeCache, repo, pc, db.get(), true)); + up.setAdvertiseRefsHook(refFilterFactory.create(pc.getProjectState(), repo)); next.doFilter(request, response); }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCanonicalWebUrlProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCanonicalWebUrlProvider.java index 6411ee5..3dd31d9 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCanonicalWebUrlProvider.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCanonicalWebUrlProvider.java
@@ -28,12 +28,12 @@ private Provider<HttpServletRequest> requestProvider; @Inject - HttpCanonicalWebUrlProvider(@GerritServerConfig final Config config) { + HttpCanonicalWebUrlProvider(@GerritServerConfig Config config) { super(config); } @Inject(optional = true) - public void setHttpServletRequest(final Provider<HttpServletRequest> hsr) { + public void setHttpServletRequest(Provider<HttpServletRequest> hsr) { requestProvider = hsr; }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java index 00c18af..eb77a30 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -42,18 +42,17 @@ @Inject protected HttpLogoutServlet( - final AuthConfig authConfig, - final DynamicItem<WebSession> webSession, - @CanonicalWebUrl @Nullable final Provider<String> urlProvider, - final AuditService audit) { + AuthConfig authConfig, + DynamicItem<WebSession> webSession, + @CanonicalWebUrl @Nullable Provider<String> urlProvider, + AuditService audit) { this.webSession = webSession; this.urlProvider = urlProvider; this.logoutUrl = authConfig.getLogoutURL(); this.audit = audit; } - protected void doLogout(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doLogout(HttpServletRequest req, HttpServletResponse rsp) throws IOException { webSession.get().logout(); if (logoutUrl != null) { rsp.sendRedirect(logoutUrl); @@ -73,8 +72,7 @@ } @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { final String sid = webSession.get().getSessionId(); final CurrentUser currentUser = webSession.get().getUser();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRemotePeerProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRemotePeerProvider.java index 2dedd86..e023644 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRemotePeerProvider.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRemotePeerProvider.java
@@ -29,7 +29,7 @@ private final HttpServletRequest req; @Inject - HttpRemotePeerProvider(final HttpServletRequest r) { + HttpRemotePeerProvider(HttpServletRequest r) { req = r; }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java index 87de003..7f78385 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/LoginUrlToken.java
@@ -22,7 +22,7 @@ public class LoginUrlToken { private static final String DEFAULT_TOKEN = '#' + PageLinks.MINE; - public static String getToken(final HttpServletRequest req) { + public static String getToken(HttpServletRequest req) { String token = req.getPathInfo(); if (Strings.isNullOrEmpty(token)) { return DEFAULT_TOKEN;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java index 57ec9c5..8ceb50a 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -174,7 +174,7 @@ } } - private boolean succeedAuthentication(final AccountState who) { + private boolean succeedAuthentication(AccountState who) { setUserIdentified(who.getAccount().getId()); return true; }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java index 548db48..6e02796 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java
@@ -48,9 +48,7 @@ @Inject RequestContextFilter( - final Provider<RequestCleanup> r, - final Provider<HttpRequestContext> c, - final ThreadLocalRequestContext l) { + Provider<RequestCleanup> r, Provider<HttpRequestContext> c, ThreadLocalRequestContext l) { cleanup = r; requestContext = c; local = l; @@ -63,8 +61,7 @@ public void destroy() {} @Override - public void doFilter( - final ServletRequest request, final ServletResponse response, final FilterChain chain) + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { RequestContext old = local.setContext(requestContext.get()); try {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireSslFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireSslFilter.java index 4bdd1f0..d8e6f84 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireSslFilter.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireSslFilter.java
@@ -52,7 +52,7 @@ private final Provider<String> urlProvider; @Inject - RequireSslFilter(@CanonicalWebUrl @Nullable final Provider<String> urlProvider) { + RequireSslFilter(@CanonicalWebUrl @Nullable Provider<String> urlProvider) { this.urlProvider = urlProvider; } @@ -63,8 +63,7 @@ public void destroy() {} @Override - public void doFilter( - final ServletRequest request, final ServletResponse response, final FilterChain chain) + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { final HttpServletRequest req = (HttpServletRequest) request; final HttpServletResponse rsp = (HttpServletResponse) response; @@ -91,11 +90,11 @@ } } - private static boolean isSecure(final HttpServletRequest req) { + private static boolean isSecure(HttpServletRequest req) { return "https".equals(req.getScheme()) || req.isSecure(); } - private static boolean isLocalHost(final HttpServletRequest req) { + private static boolean isLocalHost(HttpServletRequest req) { return "localhost".equals(req.getServerName()) || "127.0.0.1".equals(req.getServerName()); } }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java index 4862a70..0ee720a 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -19,11 +19,15 @@ import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import com.google.gerrit.extensions.registration.DynamicItem; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.config.AuthConfig; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -38,6 +42,7 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,6 +62,7 @@ private final Provider<ReviewDb> db; private final boolean enabled; private final DynamicItem<WebSession> session; + private final PermissionBackend permissionBackend; private final AccountResolver accountResolver; @Inject @@ -64,10 +70,12 @@ Provider<ReviewDb> db, AuthConfig config, DynamicItem<WebSession> session, + PermissionBackend permissionBackend, AccountResolver accountResolver) { this.db = db; this.enabled = config.isRunAsEnabled(); this.session = session; + this.permissionBackend = permissionBackend; this.accountResolver = accountResolver; } @@ -85,18 +93,26 @@ } CurrentUser self = session.get().getUser(); - if (!self.getCapabilities().canRunAs() + try { + if (!self.isIdentifiedUser()) { // Always disallow for anonymous users, even if permitted by the ACL, // because that would be crazy. - || !self.isIdentifiedUser()) { + throw new AuthException("denied"); + } + permissionBackend.user(self).check(GlobalPermission.RUN_AS); + } catch (AuthException e) { replyError(req, res, SC_FORBIDDEN, "not permitted to use " + RUN_AS, null); return; + } catch (PermissionBackendException e) { + log.warn("cannot check runAs", e); + replyError(req, res, SC_INTERNAL_SERVER_ERROR, RUN_AS + " unavailable", null); + return; } Account target; try { target = accountResolver.find(db.get(), runas); - } catch (OrmException e) { + } catch (OrmException | IOException | ConfigInvalidException e) { log.warn("cannot resolve account for " + RUN_AS, e); replyError(req, res, SC_INTERNAL_SERVER_ERROR, "cannot resolve " + RUN_AS, e); return;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java index b6719e6..6d9cd3c 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -121,8 +121,7 @@ private static final long serialVersionUID = 1L; @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { rsp.sendError(HttpServletResponse.SC_NOT_FOUND); } }); @@ -134,21 +133,19 @@ private static final long serialVersionUID = 1L; @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { toGerrit(req.getRequestURI(), req, rsp); } }); } - private Key<HttpServlet> screen(final String target) { + private Key<HttpServlet> screen(String target) { return key( new HttpServlet() { private static final long serialVersionUID = 1L; @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { toGerrit(target, req, rsp); } }); @@ -160,8 +157,7 @@ private static final long serialVersionUID = 1L; @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { final String token = req.getPathInfo().substring(1); toGerrit(token, req, rsp); } @@ -174,8 +170,7 @@ private static final long serialVersionUID = 1L; @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { try { String idString = req.getPathInfo(); if (idString.endsWith("/")) { @@ -224,20 +219,19 @@ }); } - private Key<HttpServlet> query(final String query) { + private Key<HttpServlet> query(String query) { return key( new HttpServlet() { private static final long serialVersionUID = 1L; @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { toGerrit(PageLinks.toChangeQuery(query), req, rsp); } }); } - private Key<HttpServlet> key(final HttpServlet servlet) { + private Key<HttpServlet> key(HttpServlet servlet) { final Key<HttpServlet> srv = Key.get(HttpServlet.class, UniqueAnnotations.create()); bind(srv) .toProvider( @@ -257,15 +251,13 @@ private static final long serialVersionUID = 1L; @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { toGerrit("/register" + req.getPathInfo(), req, rsp); } }); } - static void toGerrit( - final String target, final HttpServletRequest req, final HttpServletResponse rsp) + static void toGerrit(String target, HttpServletRequest req, HttpServletResponse rsp) throws IOException { final StringBuilder url = new StringBuilder(); url.append(req.getContextPath());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java index f1600bc..e476f15 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
@@ -19,7 +19,7 @@ import com.google.gerrit.server.AccessPath; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.AuthResult; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; public interface WebSession { boolean isSignedIn();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java index 59591cc..8b6694c 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -30,7 +30,7 @@ import com.google.common.cache.Cache; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.GerritServerConfig; import com.google.inject.Inject; @@ -55,7 +55,7 @@ private final Cache<String, Val> self; @Inject - WebSessionManager(@GerritServerConfig Config cfg, @Assisted final Cache<String, Val> cache) { + WebSessionManager(@GerritServerConfig Config cfg, @Assisted Cache<String, Val> cache) { prng = new SecureRandom(); self = cache; @@ -76,11 +76,11 @@ } } - Key createKey(final Account.Id who) { + Key createKey(Account.Id who) { return new Key(newUniqueToken(who)); } - private String newUniqueToken(final Account.Id who) { + private String newUniqueToken(Account.Id who) { try { final int nonceLen = 20; final ByteArrayOutputStream buf; @@ -135,7 +135,7 @@ return val; } - int getCookieAge(final Val val) { + int getCookieAge(Val val) { if (val.isPersistentCookie()) { // Client may store the cookie until we would remove it from our // own cache, after which it will certainly be invalid. @@ -150,7 +150,7 @@ return -1; } - Val get(final Key key) { + Val get(Key key) { Val val = self.getIfPresent(key.token); if (val != null && val.expiresAt <= nowMs()) { self.invalidate(key.token); @@ -159,14 +159,14 @@ return val; } - void destroy(final Key key) { + void destroy(Key key) { self.invalidate(key.token); } static final class Key { private transient String token; - Key(final String t) { + Key(String t) { token = t; } @@ -241,7 +241,7 @@ return persistentCookie; } - private void writeObject(final ObjectOutputStream out) throws IOException { + private void writeObject(ObjectOutputStream out) throws IOException { writeVarInt32(out, 1); writeVarInt32(out, accountId.get()); @@ -272,7 +272,7 @@ writeVarInt32(out, 0); } - private void readObject(final ObjectInputStream in) throws IOException { + private void readObject(ObjectInputStream in) throws IOException { PARSE: for (; ; ) { final int tag = readVarInt32(in);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java index b7c6be3..e721b7a 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -14,8 +14,8 @@ package com.google.gerrit.httpd.auth.become; -import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME; -import static com.google.gerrit.server.account.ExternalId.SCHEME_UUID; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID; import com.google.gerrit.common.PageLinks; import com.google.gerrit.extensions.registration.DynamicItem; @@ -25,16 +25,17 @@ import com.google.gerrit.httpd.template.SiteHeaderFooter; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.account.Accounts; import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthResult; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gwtexpui.server.CacheHeaders; import com.google.gwtorm.server.OrmException; -import com.google.gwtorm.server.ResultSet; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -48,14 +49,17 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.w3c.dom.Document; import org.w3c.dom.Element; @SuppressWarnings("serial") @Singleton class BecomeAnyAccountLoginServlet extends HttpServlet { - private final SchemaFactory<ReviewDb> schema; private final DynamicItem<WebSession> webSession; + private final SchemaFactory<ReviewDb> schema; + private final Accounts accounts; + private final AccountCache accountCache; private final AccountManager accountManager; private final SiteHeaderFooter headers; private final InternalAccountQuery accountQuery; @@ -64,24 +68,28 @@ BecomeAnyAccountLoginServlet( DynamicItem<WebSession> ws, SchemaFactory<ReviewDb> sf, + Accounts a, + AccountCache ac, AccountManager am, SiteHeaderFooter shf, InternalAccountQuery aq) { webSession = ws; schema = sf; + accounts = a; + accountCache = ac; accountManager = am; headers = shf; accountQuery = aq; } @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException, ServletException { doPost(req, rsp); } @Override - protected void doPost(final HttpServletRequest req, final HttpServletResponse rsp) + protected void doPost(HttpServletRequest req, HttpServletResponse rsp) throws IOException, ServletException { CacheHeaders.setNotCacheable(rsp); @@ -149,8 +157,8 @@ Element userlistElement = HtmlDomUtil.find(doc, "userlist"); try (ReviewDb db = schema.open()) { - ResultSet<Account> accounts = db.accounts().firstNById(100); - for (Account a : accounts) { + for (Account.Id accountId : accounts.firstNIds(100)) { + Account a = accountCache.get(accountId).getAccount(); String displayName; if (a.getUserName() != null) { displayName = a.getUserName(); @@ -159,7 +167,7 @@ } else if (a.getPreferredEmail() != null) { displayName = a.getPreferredEmail(); } else { - displayName = a.getId().toString(); + displayName = accountId.toString(); } Element linkElement = doc.createElement("a"); @@ -173,7 +181,7 @@ return HtmlDomUtil.toUTF8(doc); } - private AuthResult auth(final Account account) { + private AuthResult auth(Account account) { if (account != null) { return new AuthResult(account.getId(), null, false); } @@ -187,7 +195,7 @@ return null; } - private AuthResult byUserName(final String userName) { + private AuthResult byUserName(String userName) { try { List<AccountState> accountStates = accountQuery.byExternalId(SCHEME_USERNAME, userName); if (accountStates.isEmpty()) { @@ -205,7 +213,7 @@ } } - private AuthResult byPreferredEmail(final String email) { + private AuthResult byPreferredEmail(String email) { try (ReviewDb db = schema.open()) { List<Account> matches = db.accounts().byPreferredEmail(email).toList(); return matches.size() == 1 ? auth(matches.get(0)) : null; @@ -215,7 +223,7 @@ } } - private AuthResult byAccountId(final String idStr) { + private AuthResult byAccountId(String idStr) { final Account.Id id; try { id = Account.Id.parse(idStr); @@ -223,8 +231,8 @@ return null; } try (ReviewDb db = schema.open()) { - return auth(db.accounts().get(id)); - } catch (OrmException e) { + return auth(accounts.get(db, id)); + } catch (OrmException | IOException | ConfigInvalidException e) { getServletContext().log("cannot query database", e); return null; }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java index 5a0ed71..c7229bc 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -17,7 +17,7 @@ import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Strings.emptyToNull; import static com.google.common.net.HttpHeaders.AUTHORIZATION; -import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT; import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.UTF_8; @@ -26,7 +26,7 @@ import com.google.gerrit.httpd.RemoteUserUtil; import com.google.gerrit.httpd.WebSession; import com.google.gerrit.httpd.raw.HostPageServlet; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.config.AuthConfig; import com.google.gwtexpui.server.CacheHeaders; import com.google.gwtjsonrpc.server.RPCServletUtils; @@ -66,8 +66,7 @@ private final boolean userNameToLowerCase; @Inject - HttpAuthFilter(final DynamicItem<WebSession> webSession, final AuthConfig authConfig) - throws IOException { + HttpAuthFilter(DynamicItem<WebSession> webSession, AuthConfig authConfig) throws IOException { this.sessionProvider = webSession; final String pageName = "LoginRedirect.html"; @@ -86,8 +85,7 @@ } @Override - public void doFilter( - final ServletRequest request, final ServletResponse response, final FilterChain chain) + public void doFilter(final ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (isSessionValid((HttpServletRequest) request)) { chain.doFilter(request, response); @@ -165,7 +163,7 @@ } @Override - public void init(final FilterConfig filterConfig) {} + public void init(FilterConfig filterConfig) {} @Override public void destroy() {}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java index 638d527..f8c86ee 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java
@@ -21,7 +21,7 @@ public class HttpAuthModule extends ServletModule { private final AuthConfig authConfig; - public HttpAuthModule(final AuthConfig authConfig) { + public HttpAuthModule(AuthConfig authConfig) { this.authConfig = authConfig; }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java index 40b543b..d86c85a 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -14,7 +14,7 @@ package com.google.gerrit.httpd.auth.container; -import static com.google.gerrit.server.account.ExternalId.SCHEME_EXTERNAL; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_EXTERNAL; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.gerrit.common.PageLinks; @@ -27,7 +27,7 @@ import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthResult; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.config.AuthConfig; import com.google.gwtexpui.server.CacheHeaders; import com.google.gwtorm.server.OrmException; @@ -79,7 +79,7 @@ } @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws ServletException, IOException { final String token = LoginUrlToken.getToken(req);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java index bb3dc6a..534e50ec 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
@@ -46,7 +46,7 @@ @Inject HttpsClientSslCertAuthFilter( - final DynamicItem<WebSession> webSession, final AccountManager accountManager) { + final DynamicItem<WebSession> webSession, AccountManager accountManager) { this.webSession = webSession; this.accountManager = accountManager; }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java index 8b14af7..e93b0b6 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
@@ -47,8 +47,7 @@ } @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { final StringBuilder rdr = new StringBuilder(); rdr.append(urlProvider.get()); rdr.append(LoginUrlToken.getToken(req));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java index 67d36e4..af853cc 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java
@@ -57,13 +57,12 @@ } @Override - protected long getLastModified(final HttpServletRequest req) { + protected long getLastModified(HttpServletRequest req) { return modified; } @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { if (raw != null) { rsp.setContentType("image/png"); rsp.setContentLength(raw.length);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java index c5a1f18..5e22081 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebCssServlet.java
@@ -54,7 +54,7 @@ private final byte[] raw_css; private final byte[] gz_css; - GitwebCssServlet(final Path src) throws IOException { + GitwebCssServlet(Path src) throws IOException { if (src != null) { final Path dir = src.getParent(); final String name = src.getFileName().toString(); @@ -76,13 +76,12 @@ } @Override - protected long getLastModified(final HttpServletRequest req) { + protected long getLastModified(HttpServletRequest req) { return modified; } @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { if (raw_css != null) { rsp.setContentType("text/css"); rsp.setCharacterEncoding(UTF_8.name());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebJavaScriptServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebJavaScriptServlet.java index 70f6e4c..651b582 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebJavaScriptServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebJavaScriptServlet.java
@@ -57,13 +57,12 @@ } @Override - protected long getLastModified(final HttpServletRequest req) { + protected long getLastModified(HttpServletRequest req) { return modified; } @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { if (raw != null) { rsp.setContentType("text/javascript"); rsp.setContentLength(raw.length);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java index f3abf2d..ce8a500 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitwebServlet.java
@@ -33,6 +33,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.gerrit.common.PageLinks; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.AnonymousUser; @@ -44,8 +45,10 @@ import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.LocalDiskRepositoryManager; -import com.google.gerrit.server.project.NoSuchProjectException; -import com.google.gerrit.server.project.ProjectControl; +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.project.ProjectCache; import com.google.gerrit.server.ssh.SshInfo; import com.google.gwtexpui.server.CacheHeaders; import com.google.inject.Inject; @@ -92,7 +95,8 @@ private final Path gitwebCgi; private final URI gitwebUrl; private final LocalDiskRepositoryManager repoManager; - private final ProjectControl.Factory projectControl; + private final ProjectCache projectCache; + private final PermissionBackend permissionBackend; private final Provider<AnonymousUser> anonymousUserProvider; private final Provider<CurrentUser> userProvider; private final EnvList _env; @@ -100,7 +104,8 @@ @Inject GitwebServlet( GitRepositoryManager repoManager, - ProjectControl.Factory projectControl, + ProjectCache projectCache, + PermissionBackend permissionBackend, Provider<AnonymousUser> anonymousUserProvider, Provider<CurrentUser> userProvider, SitePaths site, @@ -113,7 +118,8 @@ throw new ProvisionException("Gitweb can only be used with LocalDiskRepositoryManager"); } this.repoManager = (LocalDiskRepositoryManager) repoManager; - this.projectControl = projectControl; + this.projectCache = projectCache; + this.permissionBackend = permissionBackend; this.anonymousUserProvider = anonymousUserProvider; this.userProvider = userProvider; this.gitwebCgi = gitwebCgiConfig.getGitwebCgi(); @@ -364,8 +370,7 @@ } @Override - protected void service(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void service(HttpServletRequest req, HttpServletResponse rsp) throws IOException { if (req.getQueryString() == null || req.getQueryString().isEmpty()) { // No query string? They want the project list, which we don't // currently support. Return to Gerrit's own web UI. @@ -402,35 +407,39 @@ name = name.substring(0, name.length() - 4); } - final Project.NameKey nameKey = new Project.NameKey(name); - final ProjectControl project; + Project.NameKey nameKey = new Project.NameKey(name); try { - project = projectControl.validateFor(nameKey); - if (!project.allRefsAreVisible() && !project.isOwner()) { - // Pretend the project doesn't exist - throw new NoSuchProjectException(nameKey); + if (projectCache.checkedGet(nameKey) == null) { + notFound(req, rsp); + return; } - } catch (NoSuchProjectException e) { - if (userProvider.get().isIdentifiedUser()) { - rsp.sendError(HttpServletResponse.SC_NOT_FOUND); - } else { - // Allow anonymous users a chance to login. - // Avoid leaking information by not distinguishing between - // project not existing and no access rights. - rsp.sendRedirect(getLoginRedirectUrl(req)); - } + permissionBackend.user(userProvider).project(nameKey).check(ProjectPermission.READ); + } catch (AuthException e) { + notFound(req, rsp); + return; + } catch (IOException | PermissionBackendException err) { + log.error("cannot load " + name, err); + rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; } try (Repository repo = repoManager.openRepository(nameKey)) { CacheHeaders.setNotCacheable(rsp); - exec(req, rsp, project); + exec(req, rsp, nameKey); } catch (RepositoryNotFoundException e) { getServletContext().log("Cannot open repository", e); rsp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } + private void notFound(HttpServletRequest req, HttpServletResponse rsp) throws IOException { + if (userProvider.get().isIdentifiedUser()) { + rsp.sendError(HttpServletResponse.SC_NOT_FOUND); + } else { + rsp.sendRedirect(getLoginRedirectUrl(req)); + } + } + private static String getLoginRedirectUrl(HttpServletRequest req) { String contextPath = req.getContextPath(); String loginUrl = contextPath + "/login/"; @@ -448,7 +457,7 @@ private static Map<String, String> getParameters(HttpServletRequest req) { final Map<String, String> params = new HashMap<>(); - for (final String pair : req.getQueryString().split("[&;]")) { + for (String pair : req.getQueryString().split("[&;]")) { final int eq = pair.indexOf('='); if (0 < eq) { String name = pair.substring(0, eq); @@ -462,8 +471,7 @@ return params; } - private void exec( - final HttpServletRequest req, final HttpServletResponse rsp, final ProjectControl project) + private void exec(HttpServletRequest req, HttpServletResponse rsp, Project.NameKey project) throws IOException { final Process proc = Runtime.getRuntime() @@ -512,7 +520,7 @@ } } - private String[] makeEnv(final HttpServletRequest req, final ProjectControl project) { + private String[] makeEnv(HttpServletRequest req, Project.NameKey nameKey) { final EnvList env = new EnvList(_env); final int contentLength = Math.max(0, req.getContentLength()); @@ -551,20 +559,21 @@ } env.set("GERRIT_CONTEXT_PATH", req.getContextPath() + "/"); - env.set("GERRIT_PROJECT_NAME", project.getProject().getName()); + env.set("GERRIT_PROJECT_NAME", nameKey.get()); - env.set( - "GITWEB_PROJECTROOT", - repoManager.getBasePath(project.getProject().getNameKey()).toAbsolutePath().toString()); + env.set("GITWEB_PROJECTROOT", repoManager.getBasePath(nameKey).toAbsolutePath().toString()); - if (project.forUser(anonymousUserProvider.get()).isVisible()) { + if (permissionBackend + .user(anonymousUserProvider) + .project(nameKey) + .testOrFalse(ProjectPermission.READ)) { env.set("GERRIT_ANONYMOUS_READ", "1"); } String remoteUser = null; - if (project.getUser().isIdentifiedUser()) { - final IdentifiedUser u = project.getUser().asIdentifiedUser(); - final String user = u.getUserName(); + if (userProvider.get().isIdentifiedUser()) { + IdentifiedUser u = userProvider.get().asIdentifiedUser(); + String user = u.getUserName(); env.set("GERRIT_USER_NAME", user); if (user != null && !user.isEmpty()) { remoteUser = user; @@ -612,8 +621,7 @@ return env.getEnvArray(); } - private void copyContentToCGI(final HttpServletRequest req, final OutputStream dst) - throws IOException { + private void copyContentToCGI(HttpServletRequest req, OutputStream dst) throws IOException { final int contentLength = req.getContentLength(); final InputStream src = req.getInputStream(); new Thread( @@ -642,7 +650,7 @@ .start(); } - private void copyStderrToLog(final InputStream in) { + private void copyStderrToLog(InputStream in) { new Thread( () -> { try (BufferedReader br = @@ -663,7 +671,7 @@ return req.getHeaderNames(); } - private void readCgiHeaders(HttpServletResponse res, final InputStream in) throws IOException { + private void readCgiHeaders(HttpServletResponse res, InputStream in) throws IOException { String line; while (!(line = readLine(in)).isEmpty()) { if (line.startsWith("HTTP")) { @@ -694,7 +702,7 @@ } } - private String readLine(final InputStream in) throws IOException { + private String readLine(InputStream in) throws IOException { final StringBuilder buf = new StringBuilder(); int b; while ((b = in.read()) != -1 && b != '\n') { @@ -711,12 +719,12 @@ envMap = new HashMap<>(); } - EnvList(final EnvList l) { + EnvList(EnvList l) { envMap = new HashMap<>(l.envMap); } /** Set a name/value pair, null values will be treated as an empty String */ - public void set(final String name, String value) { + public void set(String name, String value) { if (value == null) { value = ""; }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java index 279903c..937b24a 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
@@ -15,9 +15,11 @@ package com.google.gerrit.httpd.plugins; import com.google.gerrit.extensions.api.lfs.LfsDefinitions; +import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.httpd.resources.Resource; import com.google.gerrit.httpd.resources.ResourceKey; import com.google.gerrit.httpd.resources.ResourceWeigher; +import com.google.gerrit.server.DynamicOptions; import com.google.gerrit.server.cache.CacheModule; import com.google.gerrit.server.plugins.ModuleGenerator; import com.google.gerrit.server.plugins.ReloadPluginListener; @@ -63,5 +65,7 @@ .weigher(ResourceWeigher.class); } }); + + DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class); } }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java index 9730032..039fcb3 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -64,6 +64,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentMap; @@ -637,7 +638,11 @@ Path path = plugin.getSrcFile(); if (req.getRequestURI().endsWith(getJsPluginPath(plugin)) && Files.exists(path)) { res.setHeader("Content-Length", Long.toString(Files.size(path))); - res.setContentType("application/javascript"); + if (path.toString().toLowerCase(Locale.US).endsWith(".html")) { + res.setContentType("text/html"); + } else { + res.setContentType("application/javascript"); + } writeToResponse(res, Files.newInputStream(path)); } else { resourceCache.put(key, Resource.NOT_FOUND);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java index 7e298aa..a4eea96 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -35,6 +35,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jgit.lib.ObjectId; /** * Exports a single version of a patch as a normal file download. @@ -68,8 +69,7 @@ } @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { String keyStr = req.getPathInfo(); // We shouldn't have to do this extra decode pass, but somehow we @@ -126,7 +126,7 @@ try { Optional<ChangeEdit> edit = changeEditUtil.byChange(control.getChange()); if (edit.isPresent()) { - revision = edit.get().getRevision().get(); + revision = ObjectId.toString(edit.get().getEditCommit()); } else { rsp.sendError(HttpServletResponse.SC_NOT_FOUND); return;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java index 84348d0..51340ae 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -37,6 +37,7 @@ import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.notedb.NotesMigration; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtexpui.server.CacheHeaders; import com.google.gwtjsonrpc.server.JsonServlet; import com.google.gwtjsonrpc.server.RPCServletUtils; @@ -132,7 +133,7 @@ String src = "gerrit_ui/gerrit_ui.nocache.js"; try (InputStream in = servletContext.getResourceAsStream("/" + src)) { if (in != null) { - Hasher md = Hashing.md5().newHasher(); + Hasher md = Hashing.murmur3_128().newHasher(); byte[] buf = new byte[1024]; int n; while ((n = in.read(buf)) > 0) { @@ -220,7 +221,7 @@ private DiffPreferencesInfo getDiffPreferences(IdentifiedUser user) { try { return getDiff.apply(new AccountResource(user)); - } catch (AuthException | ConfigInvalidException | IOException e) { + } catch (AuthException | ConfigInvalidException | IOException | PermissionBackendException e) { log.warn("Cannot query account diff preferences", e); } return DiffPreferencesInfo.defaults();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java index b55cf6c..107f584 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java
@@ -34,7 +34,7 @@ public class IndexServlet extends HttpServlet { private static final long serialVersionUID = 1L; - private final byte[] indexSource; + protected final byte[] indexSource; IndexServlet(String canonicalURL, @Nullable String cdnPath) throws URISyntaxException { String resourcePath = "com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy";
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java index 8ccf221..10735a5 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
@@ -53,8 +53,7 @@ } @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { final byte[] tosend; if (RPCServletUtils.acceptsGzipEncoding(req)) { rsp.setHeader("Content-Encoding", "gzip");
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java index c79fa74..150acc6 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ResourceServlet.java
@@ -289,7 +289,7 @@ || name.contains("//"); // windows UNC path can be "//..." } - private Callable<Resource> newLoader(final Path p) { + private Callable<Resource> newLoader(Path p) { return () -> { try { return new Resource( @@ -312,7 +312,7 @@ this.lastModified = checkNotNull(lastModified, "lastModified"); this.contentType = checkNotNull(contentType, "contentType"); this.raw = checkNotNull(raw, "raw"); - this.etag = Hashing.md5().hashBytes(raw).toString(); + this.etag = Hashing.murmur3_128().hashBytes(raw).toString(); } boolean isStale(Path p, ResourceServlet rs) throws IOException {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java index b20f990..55bc2a6 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
@@ -52,13 +52,12 @@ private final SshInfo sshd; @Inject - SshInfoServlet(final SshInfo daemon) { + SshInfoServlet(SshInfo daemon) { sshd = daemon; } @Override - protected void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { final List<HostKey> hostKeys = sshd.getHostKeys(); final String out; if (!hostKeys.isEmpty()) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java index ced3121..bfaf0c7 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -14,19 +14,29 @@ package com.google.gerrit.httpd.restapi; +import static com.google.gerrit.httpd.restapi.RestApiServlet.ALLOWED_CORS_METHODS; +import static com.google.gerrit.httpd.restapi.RestApiServlet.XD_AUTHORIZATION; +import static com.google.gerrit.httpd.restapi.RestApiServlet.XD_CONTENT_TYPE; +import static com.google.gerrit.httpd.restapi.RestApiServlet.XD_METHOD; import static com.google.gerrit.httpd.restapi.RestApiServlet.replyBinaryResult; import static com.google.gerrit.httpd.restapi.RestApiServlet.replyError; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; +import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Splitter; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.Url; +import com.google.gerrit.server.DynamicOptions; import com.google.gerrit.util.cli.CmdLineParser; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -34,6 +44,7 @@ import com.google.gson.JsonPrimitive; import com.google.gwtexpui.server.CacheHeaders; import com.google.inject.Inject; +import com.google.inject.Injector; import java.io.IOException; import java.io.StringWriter; import java.util.HashSet; @@ -44,21 +55,119 @@ import javax.servlet.http.HttpServletResponse; import org.kohsuke.args4j.CmdLineException; -class ParameterParser { +public class ParameterParser { private static final ImmutableSet<String> RESERVED_KEYS = ImmutableSet.of("pp", "prettyPrint", "strict", "callback", "alt", "fields"); + @AutoValue + public abstract static class QueryParams { + static final String I = QueryParams.class.getName(); + + static QueryParams create( + @Nullable String accessToken, + @Nullable String xdMethod, + @Nullable String xdContentType, + ImmutableListMultimap<String, String> config, + ImmutableListMultimap<String, String> params) { + return new AutoValue_ParameterParser_QueryParams( + accessToken, xdMethod, xdContentType, config, params); + } + + @Nullable + public abstract String accessToken(); + + @Nullable + abstract String xdMethod(); + + @Nullable + abstract String xdContentType(); + + abstract ImmutableListMultimap<String, String> config(); + + abstract ImmutableListMultimap<String, String> params(); + + boolean hasXdOverride() { + return xdMethod() != null || xdContentType() != null; + } + } + + public static QueryParams getQueryParams(HttpServletRequest req) throws BadRequestException { + QueryParams qp = (QueryParams) req.getAttribute(QueryParams.I); + if (qp != null) { + return qp; + } + + String accessToken = null; + String xdMethod = null; + String xdContentType = null; + ListMultimap<String, String> config = MultimapBuilder.hashKeys(4).arrayListValues().build(); + ListMultimap<String, String> params = MultimapBuilder.hashKeys().arrayListValues().build(); + + String queryString = req.getQueryString(); + if (!Strings.isNullOrEmpty(queryString)) { + for (String kvPair : Splitter.on('&').split(queryString)) { + Iterator<String> i = Splitter.on('=').limit(2).split(kvPair).iterator(); + String key = Url.decode(i.next()); + String val = i.hasNext() ? Url.decode(i.next()) : ""; + + if (XD_AUTHORIZATION.equals(key)) { + if (accessToken != null) { + throw new BadRequestException("duplicate " + XD_AUTHORIZATION); + } + accessToken = val; + } else if (XD_METHOD.equals(key)) { + if (xdMethod != null) { + throw new BadRequestException("duplicate " + XD_METHOD); + } else if (!ALLOWED_CORS_METHODS.contains(val)) { + throw new BadRequestException("invalid " + XD_METHOD); + } + xdMethod = val; + } else if (XD_CONTENT_TYPE.equals(key)) { + if (xdContentType != null) { + throw new BadRequestException("duplicate " + XD_CONTENT_TYPE); + } + xdContentType = val; + } else if (RESERVED_KEYS.contains(key)) { + config.put(key, val); + } else { + params.put(key, val); + } + } + } + + qp = + QueryParams.create( + accessToken, + xdMethod, + xdContentType, + ImmutableListMultimap.copyOf(config), + ImmutableListMultimap.copyOf(params)); + req.setAttribute(QueryParams.I, qp); + return qp; + } + private final CmdLineParser.Factory parserFactory; + private final Injector injector; + private final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans; @Inject - ParameterParser(CmdLineParser.Factory pf) { + ParameterParser( + CmdLineParser.Factory pf, + Injector injector, + DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) { this.parserFactory = pf; + this.injector = injector; + this.dynamicBeans = dynamicBeans; } <T> boolean parse( T param, ListMultimap<String, String> in, HttpServletRequest req, HttpServletResponse res) throws IOException { CmdLineParser clp = parserFactory.create(param); + DynamicOptions pluginOptions = new DynamicOptions(param, injector, dynamicBeans); + pluginOptions.parseDynamicBeans(clp); + pluginOptions.setDynamicBeans(); + pluginOptions.onBeanParseStart(); try { clp.parseOptionMap(in); } catch (CmdLineException | NumberFormatException e) { @@ -79,28 +188,11 @@ replyBinaryResult(req, res, BinaryResult.create(msg.toString()).setContentType("text/plain")); return false; } + pluginOptions.onBeanParseEnd(); return true; } - static void splitQueryString( - String queryString, - ListMultimap<String, String> config, - ListMultimap<String, String> params) { - if (!Strings.isNullOrEmpty(queryString)) { - for (String kvPair : Splitter.on('&').split(queryString)) { - Iterator<String> i = Splitter.on('=').limit(2).split(kvPair).iterator(); - String key = Url.decode(i.next()); - String val = i.hasNext() ? Url.decode(i.next()) : ""; - if (RESERVED_KEYS.contains(key)) { - config.put(key, val); - } else { - params.put(key, val); - } - } - } - } - private static Set<String> query(HttpServletRequest req) { Set<String> params = new HashSet<>(); if (!Strings.isNullOrEmpty(req.getQueryString())) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java index d9dd5d4..8adf5f5 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -15,12 +15,16 @@ package com.google.gerrit.httpd.restapi; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; +import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_MAX_AGE; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS; import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD; +import static com.google.common.net.HttpHeaders.AUTHORIZATION; +import static com.google.common.net.HttpHeaders.CONTENT_TYPE; import static com.google.common.net.HttpHeaders.ORIGIN; import static com.google.common.net.HttpHeaders.VARY; import static java.math.RoundingMode.CEILING; @@ -51,7 +55,6 @@ import com.google.common.collect.Iterables; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; -import com.google.common.collect.MultimapBuilder; import com.google.common.io.BaseEncoding; import com.google.common.io.CountingOutputStream; import com.google.common.math.IntMath; @@ -90,13 +93,16 @@ import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.httpd.WebSession; +import com.google.gerrit.httpd.restapi.ParameterParser.QueryParams; import com.google.gerrit.server.AccessPath; import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.OptionUtil; import com.google.gerrit.server.OutputFormat; -import com.google.gerrit.server.account.CapabilityUtils; import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.util.http.RequestUtil; import com.google.gson.ExclusionStrategy; import com.google.gson.FieldAttributes; @@ -113,12 +119,14 @@ import com.google.gwtexpui.server.CacheHeaders; import com.google.inject.Inject; import com.google.inject.Provider; +import com.google.inject.TypeLiteral; import com.google.inject.util.Providers; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.EOFException; import java.io.FilterOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; @@ -133,17 +141,20 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; -import java.util.stream.StreamSupport; +import java.util.stream.Stream; import java.util.zip.GZIPOutputStream; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jgit.http.server.ServletUtils; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.TemporaryBuffer.Heap; @@ -163,8 +174,17 @@ // TODO: Remove when HttpServletResponse.SC_UNPROCESSABLE_ENTITY is available private static final int SC_UNPROCESSABLE_ENTITY = 422; private static final String X_REQUESTED_WITH = "X-Requested-With"; + private static final String X_GERRIT_AUTH = "X-Gerrit-Auth"; + static final ImmutableSet<String> ALLOWED_CORS_METHODS = + ImmutableSet.of("GET", "HEAD", "POST", "PUT", "DELETE"); private static final ImmutableSet<String> ALLOWED_CORS_REQUEST_HEADERS = - ImmutableSet.of(X_REQUESTED_WITH); + Stream.of(AUTHORIZATION, CONTENT_TYPE, X_GERRIT_AUTH, X_REQUESTED_WITH) + .map(s -> s.toLowerCase(Locale.US)) + .collect(ImmutableSet.toImmutableSet()); + + public static final String XD_AUTHORIZATION = "access_token"; + public static final String XD_CONTENT_TYPE = "$ct"; + public static final String XD_METHOD = "$m"; private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks. @@ -186,6 +206,7 @@ final Provider<CurrentUser> currentUser; final DynamicItem<WebSession> webSession; final Provider<ParameterParser> paramParser; + final PermissionBackend permissionBackend; final AuditService auditService; final RestApiMetrics metrics; final Pattern allowOrigin; @@ -195,12 +216,14 @@ Provider<CurrentUser> currentUser, DynamicItem<WebSession> webSession, Provider<ParameterParser> paramParser, + PermissionBackend permissionBackend, AuditService auditService, RestApiMetrics metrics, @GerritServerConfig Config cfg) { this.currentUser = currentUser; this.webSession = webSession; this.paramParser = paramParser; + this.permissionBackend = permissionBackend; this.auditService = auditService; this.metrics = metrics; allowOrigin = makeAllowOrigin(cfg); @@ -243,8 +266,7 @@ int status = SC_OK; long responseBytes = -1; Object result = null; - ListMultimap<String, String> params = MultimapBuilder.hashKeys().arrayListValues().build(); - ListMultimap<String, String> config = MultimapBuilder.hashKeys().arrayListValues().build(); + QueryParams qp = null; Object inputRequestBody = null; RestResource rsrc = TopLevelResource.INSTANCE; ViewData viewData = null; @@ -254,20 +276,26 @@ doCorsPreflight(req, res); return; } - checkCors(req, res); - checkUserSession(req); - ParameterParser.splitQueryString(req.getQueryString(), config, params); + qp = ParameterParser.getQueryParams(req); + checkCors(req, res, qp.hasXdOverride()); + if (qp.hasXdOverride()) { + req = applyXdOverrides(req, qp); + } + checkUserSession(req); List<IdString> path = splitPath(req); RestCollection<RestResource, RestResource> rc = members.get(); - CapabilityUtils.checkRequiresCapability(globals.currentUser, null, rc.getClass()); + globals + .permissionBackend + .user(globals.currentUser) + .checkAny(GlobalPermission.fromAnnotation(rc.getClass())); viewData = new ViewData(null, null); if (path.isEmpty()) { if (rc instanceof NeedsParams) { - ((NeedsParams) rc).setParams(params); + ((NeedsParams) rc).setParams(qp.params()); } if (isRead(req)) { @@ -360,7 +388,7 @@ return; } - if (!globals.paramParser.get().parse(viewData.view, params, req, res)) { + if (!globals.paramParser.get().parse(viewData.view, qp.params(), req, res)) { return; } @@ -371,8 +399,10 @@ RestModifyView<RestResource, Object> m = (RestModifyView<RestResource, Object>) viewData.view; - inputRequestBody = parseRequest(req, inputType(m)); + Type type = inputType(m); + inputRequestBody = parseRequest(req, type); result = m.apply(rsrc, inputRequestBody); + consumeRawInputRequestBody(req, type); } else { throw new ResourceNotFoundException(); } @@ -401,7 +431,7 @@ if (result instanceof BinaryResult) { responseBytes = replyBinaryResult(req, res, (BinaryResult) result); } else { - responseBytes = replyJson(req, res, config, result); + responseBytes = replyJson(req, res, qp.config(), result); } } } catch (MalformedJsonException e) { @@ -476,7 +506,7 @@ globals.currentUser.get(), req, auditStartTs, - params, + qp != null ? qp.params() : ImmutableListMultimap.of(), inputRequestBody, status, result, @@ -485,11 +515,53 @@ } } - private void checkCors(HttpServletRequest req, HttpServletResponse res) { + private static HttpServletRequest applyXdOverrides(HttpServletRequest req, QueryParams qp) + throws BadRequestException { + if (!"POST".equals(req.getMethod())) { + throw new BadRequestException("POST required"); + } + + String method = qp.xdMethod(); + String contentType = qp.xdContentType(); + if (method.equals("POST") || method.equals("PUT")) { + if (!"text/plain".equals(req.getContentType())) { + throw new BadRequestException("invalid " + CONTENT_TYPE); + } else if (Strings.isNullOrEmpty(contentType)) { + throw new BadRequestException(XD_CONTENT_TYPE + " required"); + } + } + + return new HttpServletRequestWrapper(req) { + @Override + public String getMethod() { + return method; + } + + @Override + public String getContentType() { + return contentType; + } + }; + } + + private void checkCors(HttpServletRequest req, HttpServletResponse res, boolean isXd) + throws BadRequestException { String origin = req.getHeader(ORIGIN); - if (isRead(req) && !Strings.isNullOrEmpty(origin) && isOriginAllowed(origin)) { + if (isXd) { + // Cross-domain, non-preflighted requests must come from an approved origin. + if (Strings.isNullOrEmpty(origin) || !isOriginAllowed(origin)) { + throw new BadRequestException("origin not allowed"); + } res.addHeader(VARY, ORIGIN); - setCorsHeaders(res, origin); + res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + } else if (!Strings.isNullOrEmpty(origin)) { + // All other requests must be processed, but conditionally set CORS headers. + if (globals.allowOrigin != null) { + res.addHeader(VARY, ORIGIN); + } + if (isOriginAllowed(origin)) { + setCorsHeaders(res, origin); + } } } @@ -502,8 +574,10 @@ private void doCorsPreflight(HttpServletRequest req, HttpServletResponse res) throws BadRequestException { CacheHeaders.setNotCacheable(res); - res.setHeader( - VARY, Joiner.on(", ").join(ImmutableList.of(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD))); + setHeaderList( + res, + VARY, + ImmutableList.of(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS)); String origin = req.getHeader(ORIGIN); if (Strings.isNullOrEmpty(origin) || !isOriginAllowed(origin)) { @@ -511,20 +585,17 @@ } String method = req.getHeader(ACCESS_CONTROL_REQUEST_METHOD); - if (!"GET".equals(method) && !"HEAD".equals(method)) { + if (!ALLOWED_CORS_METHODS.contains(method)) { throw new BadRequestException(method + " not allowed in CORS"); } String headers = req.getHeader(ACCESS_CONTROL_REQUEST_HEADERS); if (headers != null) { res.addHeader(VARY, ACCESS_CONTROL_REQUEST_HEADERS); - String badHeader = - StreamSupport.stream(Splitter.on(',').trimResults().split(headers).spliterator(), false) - .filter(h -> !ALLOWED_CORS_REQUEST_HEADERS.contains(h)) - .findFirst() - .orElse(null); - if (badHeader != null) { - throw new BadRequestException(badHeader + " not allowed in CORS"); + for (String reqHdr : Splitter.on(',').trimResults().split(headers)) { + if (!ALLOWED_CORS_REQUEST_HEADERS.contains(reqHdr.toLowerCase(Locale.US))) { + throw new BadRequestException(reqHdr + " not allowed in CORS"); + } } } @@ -534,11 +605,19 @@ res.setContentLength(0); } - private void setCorsHeaders(HttpServletResponse res, String origin) { + private static void setCorsHeaders(HttpServletResponse res, String origin) { res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin); res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - res.setHeader(ACCESS_CONTROL_ALLOW_METHODS, "GET, OPTIONS"); - res.setHeader(ACCESS_CONTROL_ALLOW_HEADERS, Joiner.on(", ").join(ALLOWED_CORS_REQUEST_HEADERS)); + res.setHeader(ACCESS_CONTROL_MAX_AGE, "600"); + setHeaderList( + res, + ACCESS_CONTROL_ALLOW_METHODS, + Iterables.concat(ALLOWED_CORS_METHODS, ImmutableList.of("OPTIONS"))); + setHeaderList(res, ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_REQUEST_HEADERS); + } + + private static void setHeaderList(HttpServletResponse res, String name, Iterable<String> values) { + res.setHeader(name, Joiner.on(", ").join(values)); } private boolean isOriginAllowed(String origin) { @@ -626,64 +705,51 @@ } private static Type inputType(RestModifyView<RestResource, Object> m) { - Type inputType = extractInputType(m.getClass()); - if (inputType == null) { - throw new IllegalStateException( - String.format( - "View %s does not correctly implement %s", - m.getClass(), RestModifyView.class.getSimpleName())); - } - return inputType; - } + // MyModifyView implements RestModifyView<SomeResource, MyInput> + TypeLiteral<?> typeLiteral = TypeLiteral.get(m.getClass()); - @SuppressWarnings("rawtypes") - private static Type extractInputType(Class clazz) { - for (Type t : clazz.getGenericInterfaces()) { - if (t instanceof ParameterizedType - && ((ParameterizedType) t).getRawType() == RestModifyView.class) { - return ((ParameterizedType) t).getActualTypeArguments()[1]; - } - } + // RestModifyView<SomeResource, MyInput> + // This is smart enough to resolve even when there are intervening subclasses, even if they have + // reordered type arguments. + TypeLiteral<?> supertypeLiteral = typeLiteral.getSupertype(RestModifyView.class); - if (clazz.getSuperclass() != null) { - Type i = extractInputType(clazz.getSuperclass()); - if (i != null) { - return i; - } - } - - for (Class t : clazz.getInterfaces()) { - Type i = extractInputType(t); - if (i != null) { - return i; - } - } - - return null; + Type supertype = supertypeLiteral.getType(); + checkState( + supertype instanceof ParameterizedType, + "supertype of %s is not parameterized: %s", + typeLiteral, + supertypeLiteral); + return ((ParameterizedType) supertype).getActualTypeArguments()[1]; } private Object parseRequest(HttpServletRequest req, Type type) throws IOException, BadRequestException, SecurityException, IllegalArgumentException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, MethodNotAllowedException { + // HTTP/1.1 requires consuming the request body before writing non-error response (less than + // 400). Consume the request body for all but raw input request types here. if (isType(JSON_TYPE, req.getContentType())) { try (BufferedReader br = req.getReader(); JsonReader json = new JsonReader(br)) { - json.setLenient(true); - - JsonToken first; try { - first = json.peek(); - } catch (EOFException e) { - throw new BadRequestException("Expected JSON object"); + json.setLenient(true); + + JsonToken first; + try { + first = json.peek(); + } catch (EOFException e) { + throw new BadRequestException("Expected JSON object"); + } + if (first == JsonToken.STRING) { + return parseString(json.nextString(), type); + } + return OutputFormat.JSON.newGson().fromJson(json, type); + } finally { + // Reader.close won't consume the rest of the input. Explicitly consume the request body. + br.skip(Long.MAX_VALUE); } - if (first == JsonToken.STRING) { - return parseString(json.nextString(), type); - } - return OutputFormat.JSON.newGson().fromJson(json, type); } - } else if (("PUT".equals(req.getMethod()) || "POST".equals(req.getMethod())) - && acceptsRawInput(type)) { + } else if (rawInputRequest(req, type)) { return parseRawInput(req, type); } else if ("DELETE".equals(req.getMethod()) && hasNoBody(req)) { return null; @@ -706,6 +772,19 @@ } } + private void consumeRawInputRequestBody(HttpServletRequest req, Type type) throws IOException { + if (rawInputRequest(req, type)) { + try (InputStream is = req.getInputStream()) { + ServletUtils.consumeRequestBody(is); + } + } + } + + private static boolean rawInputRequest(HttpServletRequest req, Type type) { + String method = req.getMethod(); + return ("PUT".equals(method) || "POST".equals(method)) && acceptsRawInput(type); + } + private static boolean hasNoBody(HttpServletRequest req) { int len = req.getContentLength(); String type = req.getContentType(); @@ -724,7 +803,7 @@ return false; } - private Object parseRawInput(final HttpServletRequest req, Type type) + private Object parseRawInput(HttpServletRequest req, Type type) throws SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException, MethodNotAllowedException { @@ -899,7 +978,7 @@ } } - private static BinaryResult stackJsonString(HttpServletResponse res, final BinaryResult src) + private static BinaryResult stackJsonString(HttpServletResponse res, BinaryResult src) throws IOException { TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, Integer.MAX_VALUE); buf.write(JSON_MAGIC); @@ -915,7 +994,7 @@ return asBinaryResult(buf).setContentType(JSON_TYPE).setCharacterEncoding(UTF_8); } - private static BinaryResult stackBase64(HttpServletResponse res, final BinaryResult src) + private static BinaryResult stackBase64(HttpServletResponse res, BinaryResult src) throws IOException { BinaryResult b64; long len = src.getContentLength(); @@ -946,7 +1025,7 @@ return b64.setContentType("text/plain").setCharacterEncoding(ISO_8859_1); } - private static BinaryResult stackGzip(HttpServletResponse res, final BinaryResult src) + private static BinaryResult stackGzip(HttpServletResponse res, BinaryResult src) throws IOException { BinaryResult gz; long len = src.getContentLength(); @@ -1083,9 +1162,12 @@ return "GET".equals(req.getMethod()) || "HEAD".equals(req.getMethod()); } - private void checkRequiresCapability(ViewData viewData) throws AuthException { - CapabilityUtils.checkRequiresCapability( - globals.currentUser, viewData.pluginName, viewData.view.getClass()); + private void checkRequiresCapability(ViewData d) + throws AuthException, PermissionBackendException { + globals + .permissionBackend + .user(globals.currentUser) + .checkAny(GlobalPermission.fromAnnotation(d.pluginName, d.view.getClass())); } private static long handleException( @@ -1196,7 +1278,7 @@ } @SuppressWarnings("resource") - private static BinaryResult asBinaryResult(final TemporaryBuffer.Heap buf) { + private static BinaryResult asBinaryResult(TemporaryBuffer.Heap buf) { return new BinaryResult() { @Override public void writeTo(OutputStream os) throws IOException {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java index e561c9b..9e0e8f6 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
@@ -34,7 +34,7 @@ private final Provider<? extends CurrentUser> currentUser; protected BaseServiceImplementation( - final Provider<ReviewDb> schema, final Provider<? extends CurrentUser> currentUser) { + final Provider<ReviewDb> schema, Provider<? extends CurrentUser> currentUser) { this.schema = schema; this.currentUser = currentUser; } @@ -63,7 +63,7 @@ * @param callback the callback that will receive the result. * @param action the action logic to perform. */ - protected <T> void run(final AsyncCallback<T> callback, final Action<T> action) { + protected <T> void run(AsyncCallback<T> callback, Action<T> action) { try { final T r = action.run(schema.get()); if (r != null) { @@ -100,7 +100,7 @@ } } - private static <T> void handleOrmException(final AsyncCallback<T> callback, Exception e) { + private static <T> void handleOrmException(AsyncCallback<T> callback, Exception e) { if (e.getCause() instanceof Failure) { callback.onFailure(e.getCause().getCause()); } else if (e.getCause() instanceof NoSuchEntityException) { @@ -114,7 +114,7 @@ public static class Failure extends Exception { private static final long serialVersionUID = 1L; - public Failure(final Throwable why) { + public Failure(Throwable why) { super(why); } }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java index cce87a8..178cda9 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -52,16 +52,14 @@ private final AuditService audit; @Inject - GerritJsonServlet( - final DynamicItem<WebSession> w, final RemoteJsonService s, final AuditService a) { + GerritJsonServlet(final DynamicItem<WebSession> w, RemoteJsonService s, AuditService a) { session = w; service = s; audit = a; } @Override - protected GerritCall createActiveCall( - final HttpServletRequest req, final HttpServletResponse rsp) { + protected GerritCall createActiveCall(final HttpServletRequest req, HttpServletResponse rsp) { final GerritCall call = new GerritCall(session.get(), req, new AuditedHttpServletResponse(rsp)); currentCall.set(call); return call; @@ -82,7 +80,7 @@ } @Override - protected void preInvoke(final GerritCall call) { + protected void preInvoke(GerritCall call) { super.preInvoke(call); if (call.isComplete()) { @@ -106,8 +104,7 @@ } @Override - protected void service(final HttpServletRequest req, final HttpServletResponse resp) - throws IOException { + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { try { super.service(req, resp); } finally { @@ -163,7 +160,7 @@ return args; } - private String extractWhat(final Audit note, final GerritCall call) { + private String extractWhat(Audit note, GerritCall call) { Class<?> methodClass = call.getMethodClass(); String methodClassName = methodClass != null ? methodClass.getName() : "<UNKNOWN_CLASS>"; methodClassName = methodClassName.substring(methodClassName.lastIndexOf(".") + 1); @@ -233,7 +230,7 @@ return null; } - GerritCall(final WebSession session, final HttpServletRequest i, final HttpServletResponse o) { + GerritCall(WebSession session, HttpServletRequest i, HttpServletResponse o) { super(i, o); this.session = session; this.when = TimeUtil.nowMs(); @@ -248,7 +245,7 @@ } @Override - public void onFailure(final Throwable error) { + public void onFailure(Throwable error) { if (error instanceof IllegalArgumentException || error instanceof IllegalStateException) { super.onFailure(error); } else if (error instanceof OrmException || error instanceof RuntimeException) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServletProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServletProvider.java index 9fd9269..b167167 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServletProvider.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServletProvider.java
@@ -27,7 +27,7 @@ private final Class<? extends RemoteJsonService> serviceClass; @Inject - GerritJsonServletProvider(final Class<? extends RemoteJsonService> c) { + GerritJsonServletProvider(Class<? extends RemoteJsonService> c) { serviceClass = c; }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java index a9d654c..b932169 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
@@ -44,7 +44,7 @@ * successfully. */ public abstract class Handler<T> implements Callable<T> { - public static <T> Handler<T> wrap(final Callable<T> r) { + public static <T> Handler<T> wrap(Callable<T> r) { return new Handler<T>() { @Override public T call() throws Exception { @@ -58,7 +58,7 @@ * * @param callback callback to receive the result of {@link #call()}. */ - public final void to(final AsyncCallback<T> callback) { + public final void to(AsyncCallback<T> callback) { try { final T r = call(); if (r != null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/RpcServletModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/RpcServletModule.java index 5315182..b03609e 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/RpcServletModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/RpcServletModule.java
@@ -26,7 +26,7 @@ private final String prefix; - protected RpcServletModule(final String pathPrefix) { + protected RpcServletModule(String pathPrefix) { prefix = pathPrefix; } @@ -38,7 +38,7 @@ rpc(name, clazz); } - protected void rpc(final String name, Class<? extends RemoteJsonService> clazz) { + protected void rpc(String name, Class<? extends RemoteJsonService> clazz) { final Key<GerritJsonServlet> srv = Key.get(GerritJsonServlet.class, UniqueAnnotations.create()); final GerritJsonServletProvider provider = new GerritJsonServletProvider(clazz); bind(clazz);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java index ec67661..7a7713d 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
@@ -44,9 +44,9 @@ } @Override - public void daemonHostKeys(final AsyncCallback<List<SshHostKey>> callback) { + public void daemonHostKeys(AsyncCallback<List<SshHostKey>> callback) { final ArrayList<SshHostKey> r = new ArrayList<>(hostKeys.size()); - for (final HostKey hk : hostKeys) { + for (HostKey hk : hostKeys) { String host = hk.getHost(); if (host.startsWith("*:")) { final String port = host.substring(2);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java index 75026d3..0e6ae84 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -24,6 +24,7 @@ import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; +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.ProjectControl; @@ -89,7 +90,8 @@ ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate) - throws IOException, NoSuchProjectException, ConfigInvalidException { + throws IOException, NoSuchProjectException, ConfigInvalidException, + PermissionBackendException { RevCommit commit = config.commit(md); gitRefUpdated.fire(
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java index ca23ec2..afec3b6 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -14,6 +14,10 @@ package com.google.gerrit.httpd.rpc.project; +import static com.google.gerrit.server.permissions.GlobalPermission.ADMINISTRATE_SERVER; +import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE; +import static com.google.gerrit.server.permissions.RefPermission.READ; + import com.google.common.collect.Maps; import com.google.gerrit.common.data.AccessSection; import com.google.gerrit.common.data.GroupDescription; @@ -24,21 +28,27 @@ import com.google.gerrit.common.data.RefConfigSection; import com.google.gerrit.common.data.WebLinkInfoCommon; import com.google.gerrit.common.errors.NoSuchGroupException; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.httpd.rpc.Handler; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.WebLinks; import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.GroupControl; import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; +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.NoSuchProjectException; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectControl; -import com.google.gerrit.server.project.RefControl; import com.google.inject.Inject; +import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import java.io.IOException; import java.util.ArrayList; @@ -56,27 +66,32 @@ private final GroupBackend groupBackend; private final ProjectCache projectCache; - private final ProjectControl.Factory projectControlFactory; + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; + private final ProjectControl.GenericFactory projectControlFactory; private final GroupControl.Factory groupControlFactory; private final MetaDataUpdate.Server metaDataUpdateFactory; private final AllProjectsName allProjectsName; private final Project.NameKey projectName; - private ProjectControl pc; private WebLinks webLinks; @Inject ProjectAccessFactory( - final GroupBackend groupBackend, - final ProjectCache projectCache, - final ProjectControl.Factory projectControlFactory, - final GroupControl.Factory groupControlFactory, - final MetaDataUpdate.Server metaDataUpdateFactory, - final AllProjectsName allProjectsName, - final WebLinks webLinks, + GroupBackend groupBackend, + ProjectCache projectCache, + PermissionBackend permissionBackend, + Provider<CurrentUser> user, + ProjectControl.GenericFactory projectControlFactory, + GroupControl.Factory groupControlFactory, + MetaDataUpdate.Server metaDataUpdateFactory, + AllProjectsName allProjectsName, + WebLinks webLinks, @Assisted final Project.NameKey name) { this.groupBackend = groupBackend; this.projectCache = projectCache; + this.permissionBackend = permissionBackend; + this.user = user; this.projectControlFactory = projectControlFactory; this.groupControlFactory = groupControlFactory; this.metaDataUpdateFactory = metaDataUpdateFactory; @@ -87,8 +102,10 @@ } @Override - public ProjectAccess call() throws NoSuchProjectException, IOException, ConfigInvalidException { - pc = open(); + public ProjectAccess call() + throws NoSuchProjectException, IOException, ConfigInvalidException, + PermissionBackendException { + ProjectControl pc = checkProjectControl(); // Load the current configuration from the repository, ensuring its the most // recent version available. If it differs from what was in the project @@ -97,23 +114,23 @@ ProjectConfig config; try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) { config = ProjectConfig.read(md); - if (config.updateGroupNames(groupBackend)) { md.setMessage("Update group names\n"); config.commit(md); projectCache.evict(config.getProject()); - pc = open(); + pc = checkProjectControl(); } else if (config.getRevision() != null && !config.getRevision().equals(pc.getProjectState().getConfig().getRevision())) { projectCache.evict(config.getProject()); - pc = open(); + pc = checkProjectControl(); } } - final RefControl metaConfigControl = pc.controlForRef(RefNames.REFS_CONFIG); List<AccessSection> local = new ArrayList<>(); Set<String> ownerOf = new HashSet<>(); Map<AccountGroup.UUID, Boolean> visibleGroups = new HashMap<>(); + PermissionBackend.ForProject perm = permissionBackend.user(user).project(projectName); + boolean checkReadConfig = check(perm, RefNames.REFS_CONFIG, READ); for (AccessSection section : config.getAccessSections()) { String name = section.getName(); @@ -122,20 +139,19 @@ local.add(section); ownerOf.add(name); - } else if (metaConfigControl.isVisible()) { + } else if (checkReadConfig) { local.add(section); } } else if (RefConfigSection.isValid(name)) { - RefControl rc = pc.controlForRef(name); - if (rc.isOwner()) { + if (pc.controlForRef(name).isOwner()) { local.add(section); ownerOf.add(name); - } else if (metaConfigControl.isVisible()) { + } else if (checkReadConfig) { local.add(section); - } else if (rc.isVisible()) { + } else if (check(perm, name, READ)) { // Filter the section to only add rules describing groups that // are visible to the current-user. This includes any group the // user is a member of, as well as groups they own or that @@ -193,17 +209,17 @@ detail.setInheritsFrom(config.getProject().getParent(allProjectsName)); - if (projectName.equals(allProjectsName)) { - if (pc.isOwner()) { - ownerOf.add(AccessSection.GLOBAL_CAPABILITIES); - } + if (projectName.equals(allProjectsName) + && permissionBackend.user(user).testOrFalse(ADMINISTRATE_SERVER)) { + ownerOf.add(AccessSection.GLOBAL_CAPABILITIES); } detail.setLocal(local); detail.setOwnerOf(ownerOf); detail.setCanUpload( - metaConfigControl.isVisible() && (pc.isOwner() || metaConfigControl.canUpload())); - detail.setConfigVisible(pc.isOwner() || metaConfigControl.isVisible()); + pc.isOwner() + || (checkReadConfig && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE))); + detail.setConfigVisible(pc.isOwner() || checkReadConfig); detail.setGroupInfo(buildGroupInfo(local)); detail.setLabelTypes(pc.getLabelTypes()); detail.setFileHistoryLinks(getConfigFileLogLinks(projectName.get())); @@ -235,9 +251,24 @@ return Maps.filterEntries(infos, in -> in.getValue() != null); } - private ProjectControl open() throws NoSuchProjectException { - return projectControlFactory.validateFor( // - projectName, // - ProjectControl.OWNER | ProjectControl.VISIBLE); + private ProjectControl checkProjectControl() + throws NoSuchProjectException, IOException, PermissionBackendException { + ProjectControl pc = projectControlFactory.controlFor(projectName, user.get()); + try { + permissionBackend.user(user).project(projectName).check(ProjectPermission.ACCESS); + } catch (AuthException e) { + throw new NoSuchProjectException(projectName); + } + return pc; + } + + private static boolean check(PermissionBackend.ForProject ctx, String ref, RefPermission perm) + throws PermissionBackendException { + try { + ctx.ref(ref).check(perm); + return true; + } catch (AuthException denied) { + return false; + } } }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java index 0d90190..252d023 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -36,6 +36,7 @@ import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.RefPattern; @@ -95,7 +96,7 @@ public final T call() throws NoSuchProjectException, IOException, ConfigInvalidException, InvalidNameException, NoSuchGroupException, OrmException, UpdateParentFailedException, - PermissionDeniedException { + PermissionDeniedException, PermissionBackendException { final ProjectControl projectControl = projectControlFactory.controlFor(projectName); Capable r = projectControl.canPushToAtLeastOneRef(); @@ -182,7 +183,7 @@ MetaDataUpdate md, boolean parentProjectUpdate) throws IOException, NoSuchProjectException, ConfigInvalidException, OrmException, - PermissionDeniedException; + PermissionDeniedException, PermissionBackendException; private void replace(ProjectConfig config, Set<String> toDelete, AccessSection section) throws NoSuchGroupException {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java index bdb274d..da471c3 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
@@ -41,11 +41,11 @@ @Override public void projectAccess( - final Project.NameKey projectName, final AsyncCallback<ProjectAccess> callback) { + final Project.NameKey projectName, AsyncCallback<ProjectAccess> callback) { projectAccessFactory.create(projectName).to(callback); } - private static ObjectId getBase(final String baseRevision) { + private static ObjectId getBase(String baseRevision) { if (baseRevision != null && !baseRevision.isEmpty()) { return ObjectId.fromString(baseRevision); }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java index 9ad1250..4e2a4d3 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -14,6 +14,7 @@ package com.google.gerrit.httpd.rpc.project; +import com.google.common.base.Throwables; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.AccessSection; @@ -21,6 +22,7 @@ import com.google.gerrit.common.data.PermissionRule; import com.google.gerrit.common.errors.PermissionDeniedException; import com.google.gerrit.extensions.api.changes.AddReviewerInput; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.reviewdb.client.Change; @@ -37,11 +39,12 @@ import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; -import com.google.gerrit.server.git.validators.CommitValidators; import com.google.gerrit.server.group.SystemGroupBackend; +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.ProjectCache; import com.google.gerrit.server.project.ProjectControl; -import com.google.gerrit.server.project.RefControl; import com.google.gerrit.server.project.SetParent; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.UpdateException; @@ -53,6 +56,7 @@ import java.util.List; 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; @@ -67,6 +71,7 @@ } private final ReviewDb db; + private final PermissionBackend permissionBackend; private final Sequences seq; private final Provider<PostReviewers> reviewersProvider; private final ProjectCache projectCache; @@ -77,6 +82,7 @@ @Inject ReviewProjectAccess( final ProjectControl.Factory projectControlFactory, + PermissionBackend permissionBackend, GroupBackend groupBackend, MetaDataUpdate.User metaDataUpdateFactory, ReviewDb db, @@ -106,6 +112,7 @@ message, false); this.db = db; + this.permissionBackend = permissionBackend; this.seq = seq; this.reviewersProvider = reviewersProvider; this.projectCache = projectCache; @@ -114,19 +121,32 @@ this.updateFactory = updateFactory; } + // TODO(dborowitz): Hack MetaDataUpdate so it can be created within a BatchUpdate and we can avoid + // calling setUpdateRef(false). + @SuppressWarnings("deprecation") @Override protected Change.Id updateProjectConfig( ProjectControl projectControl, ProjectConfig config, MetaDataUpdate md, boolean parentProjectUpdate) - throws IOException, OrmException, PermissionDeniedException { - RefControl refsMetaConfigControl = projectControl.controlForRef(RefNames.REFS_CONFIG); - if (!refsMetaConfigControl.isVisible()) { + throws IOException, OrmException, PermissionDeniedException, PermissionBackendException { + PermissionBackend.ForRef metaRef = + permissionBackend + .user(projectControl.getUser()) + .project(projectControl.getProject().getNameKey()) + .ref(RefNames.REFS_CONFIG); + try { + metaRef.check(RefPermission.READ); + } catch (AuthException denied) { throw new PermissionDeniedException(RefNames.REFS_CONFIG + " not visible"); } - if (!projectControl.isOwner() && !refsMetaConfigControl.canUpload()) { - throw new PermissionDeniedException("cannot upload to " + RefNames.REFS_CONFIG); + if (!projectControl.isOwner()) { + try { + metaRef.check(RefPermission.CREATE_CHANGE); + } catch (AuthException denied) { + throw new PermissionDeniedException("cannot create change for " + RefNames.REFS_CONFIG); + } } md.setInsertChangeId(true); @@ -138,8 +158,9 @@ return null; } - try (RevWalk rw = new RevWalk(md.getRepository()); - ObjectInserter objInserter = md.getRepository().newObjectInserter(); + try (ObjectInserter objInserter = md.getRepository().newObjectInserter(); + ObjectReader objReader = objInserter.newReader(); + RevWalk rw = new RevWalk(objReader); BatchUpdate bu = updateFactory.create( db, config.getProject().getNameKey(), projectControl.getUser(), TimeUtil.nowTs())) { @@ -147,7 +168,7 @@ bu.insertChange( changeInserterFactory .create(changeId, commit, RefNames.REFS_CONFIG) - .setValidatePolicy(CommitValidators.Policy.NONE) + .setValidate(false) .setUpdateRef(false)); // Created by commitToNewRef. bu.execute(); } catch (UpdateException | RestApiException e) { @@ -173,9 +194,10 @@ AddReviewerInput input = new AddReviewerInput(); input.reviewer = projectOwners; reviewersProvider.get().apply(rsrc, input); - } catch (IOException | OrmException | RestApiException | UpdateException e) { + } catch (Exception e) { // one of the owner groups is not visible to the user and this it why it // can't be added as reviewer + Throwables.throwIfUnchecked(e); } } @@ -192,8 +214,9 @@ AddReviewerInput input = new AddReviewerInput(); input.reviewer = r.getGroup().getUUID().get(); reviewersProvider.get().apply(rsrc, input); - } catch (IOException | OrmException | RestApiException | UpdateException e) { + } catch (Exception e) { // ignore + Throwables.throwIfUnchecked(e); } } }
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy index eed6ccc..558cc78 100644 --- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy +++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
@@ -19,6 +19,7 @@ /** * @param canonicalPath * @param staticResourcePath + * @param? versionInfo */ {template .Index autoescape="strict" kind="html"} <!DOCTYPE html>{\n} @@ -27,19 +28,28 @@ <meta name="description" content="Gerrit Code Review">{\n} <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">{\n} - {if $canonicalPath != ''} - <script>window.CANONICAL_PATH = '{$canonicalPath}';</script>{\n} + {if $canonicalPath != '' or $versionInfo} + <script> + {if $canonicalPath != ''}window.CANONICAL_PATH = '{$canonicalPath}';{/if} + {if $versionInfo}window.VERSION_INFO = '{$versionInfo}';{/if} + </script>{\n} {/if} <link rel="icon" type="image/x-icon" href="{$canonicalPath}/favicon.ico">{\n} - // SourceCodePro fonts are used in styles/fonts.css + // RobotoMono fonts are used in styles/fonts.css // @see https://github.com/w3c/preload/issues/32 regarding crossorigin - <link rel="preload" href="{$staticResourcePath}/fonts/SourceCodePro-Regular.woff2" as="font" type="font/woff2" crossorigin>{\n} - <link rel="preload" href="{$staticResourcePath}/fonts/SourceCodePro-Regular.woff" as="font" type="font/woff" crossorigin>{\n} + <link rel="preload" href="{$staticResourcePath}/fonts/RobotoMono-Regular.woff2" as="font" type="font/woff2" crossorigin>{\n} + <link rel="preload" href="{$staticResourcePath}/fonts/RobotoMono-Regular.woff" as="font" type="font/woff" crossorigin>{\n} <link rel="stylesheet" href="{$staticResourcePath}/styles/fonts.css">{\n} <link rel="stylesheet" href="{$staticResourcePath}/styles/main.css">{\n} <script src="{$staticResourcePath}/bower_components/webcomponentsjs/webcomponents-lite.js"></script>{\n} + // Content between webcomponents-lite and the load of the main app element + // run before polymer-resin is installed so may have security consequences. + // Contact your local security engineer if you have any questions, and + // CC them on any changes that load content before gr-app.html. + // + // github.com/Polymer/polymer-resin/blob/master/getting-started.md#integrating <link rel="preload" href="{$staticResourcePath}/elements/gr-app.js" as="script" crossorigin="anonymous">{\n} <link rel="import" href="{$staticResourcePath}/elements/gr-app.html">{\n}
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java index 86989dd..086dcc2 100644 --- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java +++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/AllRequestFilterFilterProxyTest.java
@@ -74,7 +74,7 @@ * <p>This method adds the given filter to all {@link AllRequestFilter.FilterProxy} instances * created by {@link #getFilterProxy()}. */ - private ReloadableRegistrationHandle<AllRequestFilter> addFilter(final AllRequestFilter filter) { + private ReloadableRegistrationHandle<AllRequestFilter> addFilter(AllRequestFilter filter) { Key<AllRequestFilter> key = Key.get(AllRequestFilter.class); return filters.add(key, Providers.of(filter)); }
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/IndexServletTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/IndexServletTest.java index 7133cf6..d106eec 100644 --- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/IndexServletTest.java +++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/IndexServletTest.java
@@ -21,6 +21,18 @@ import org.junit.Test; public class IndexServletTest { + class TestIndexServlet extends IndexServlet { + private static final long serialVersionUID = 1L; + + TestIndexServlet(String canonicalURL, String cdnPath) throws URISyntaxException { + super(canonicalURL, cdnPath); + } + + String getIndexSource() { + return new String(indexSource); + } + } + @Test public void noPathAndNoCDN() throws URISyntaxException { SoyMapData data = IndexServlet.getTemplateData("http://example.com/", null); @@ -52,4 +64,15 @@ assertThat(data.getSingle("staticResourcePath").stringValue()) .isEqualTo("http://my-cdn.com/foo/bar/"); } + + @Test + public void renderTemplate() throws URISyntaxException { + String testCanonicalUrl = "foo-url"; + String testCdnPath = "bar-cdn"; + TestIndexServlet servlet = new TestIndexServlet(testCanonicalUrl, testCdnPath); + String output = servlet.getIndexSource(); + assertThat(output).contains("<!DOCTYPE html>"); + assertThat(output).contains("window.CANONICAL_PATH = '" + testCanonicalUrl); + assertThat(output).contains("<link rel=\"preload\" href=\"" + testCdnPath); + } }
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java index 2b724e2..13732b0 100644 --- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java +++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java
@@ -14,11 +14,15 @@ package com.google.gerrit.httpd.restapi; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.httpd.restapi.ParameterParser.QueryParams; +import com.google.gerrit.util.http.testutil.FakeHttpServletRequest; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; @@ -49,4 +53,91 @@ assertEquals(exp, obj); } + + @Test + public void parseQuery() throws BadRequestException { + FakeHttpServletRequest req = new FakeHttpServletRequest(); + req.setQueryString("query=status%3aopen"); + QueryParams qp = ParameterParser.getQueryParams(req); + assertThat(qp.accessToken()).isNull(); + assertThat(qp.xdMethod()).isNull(); + assertThat(qp.xdContentType()).isNull(); + assertThat(qp.hasXdOverride()).isFalse(); + assertThat(qp.config()).isEmpty(); + assertThat(qp.params()).containsKey("query"); + assertThat(qp.params().get("query")).containsExactly("status:open"); + } + + @Test + public void parseAccessToken() throws BadRequestException { + FakeHttpServletRequest req = new FakeHttpServletRequest(); + req.setQueryString("query=status%3aopen&access_token=secr%65t"); + QueryParams qp = ParameterParser.getQueryParams(req); + assertThat(qp.accessToken()).isEqualTo("secret"); + assertThat(qp.xdMethod()).isNull(); + assertThat(qp.xdContentType()).isNull(); + assertThat(qp.hasXdOverride()).isFalse(); + assertThat(qp.config()).isEmpty(); + assertThat(qp.params()).containsKey("query"); + assertThat(qp.params().get("query")).containsExactly("status:open"); + + req = new FakeHttpServletRequest(); + req.setQueryString("access_token=secret"); + qp = ParameterParser.getQueryParams(req); + assertThat(qp.accessToken()).isEqualTo("secret"); + assertThat(qp.xdMethod()).isNull(); + assertThat(qp.xdContentType()).isNull(); + assertThat(qp.hasXdOverride()).isFalse(); + assertThat(qp.config()).isEmpty(); + assertThat(qp.params()).isEmpty(); + } + + @Test + public void parseXdOverride() throws BadRequestException { + FakeHttpServletRequest req = new FakeHttpServletRequest(); + req.setQueryString("$m=PUT&$ct=json&access_token=secret"); + QueryParams qp = ParameterParser.getQueryParams(req); + assertThat(qp.accessToken()).isEqualTo("secret"); + assertThat(qp.xdMethod()).isEqualTo("PUT"); + assertThat(qp.xdContentType()).isEqualTo("json"); + assertThat(qp.hasXdOverride()).isTrue(); + assertThat(qp.config()).isEmpty(); + assertThat(qp.params()).isEmpty(); + } + + @Test + public void rejectDuplicateMethod() { + FakeHttpServletRequest req = new FakeHttpServletRequest(); + req.setQueryString("$m=PUT&$m=DELETE"); + try { + ParameterParser.getQueryParams(req); + fail("expected BadRequestException"); + } catch (BadRequestException bad) { + assertThat(bad).hasMessageThat().isEqualTo("duplicate $m"); + } + } + + @Test + public void rejectDuplicateContentType() { + FakeHttpServletRequest req = new FakeHttpServletRequest(); + req.setQueryString("$ct=json&$ct=string"); + try { + ParameterParser.getQueryParams(req); + fail("expected BadRequestException"); + } catch (BadRequestException bad) { + assertThat(bad).hasMessageThat().isEqualTo("duplicate $ct"); + } + } + + @Test + public void rejectInvalidMethod() { + FakeHttpServletRequest req = new FakeHttpServletRequest(); + req.setQueryString("$m=CONNECT"); + try { + ParameterParser.getQueryParams(req); + fail("expected BadRequestException"); + } catch (BadRequestException bad) { + assertThat(bad).hasMessageThat().isEqualTo("invalid $m"); + } + } }
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java index b22ba49..4efbecc 100644 --- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java +++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -59,11 +59,11 @@ private static ClassLoader daemonClassLoader; - public static void main(final String[] argv) throws Exception { + public static void main(String[] argv) throws Exception { System.exit(mainImpl(argv)); } - public static int mainImpl(final String[] argv) throws Exception { + public static int mainImpl(String[] argv) throws Exception { if (argv.length == 0) { File me; try { @@ -108,7 +108,7 @@ return invokeProgram(cl, argv); } - public static void daemonStart(final String[] argv) throws Exception { + public static void daemonStart(String[] argv) throws Exception { if (daemonClassLoader != null) { throw new IllegalStateException("daemonStart can be called only once per JVM instance"); } @@ -128,7 +128,7 @@ } } - public static void daemonStop(final String[] argv) throws Exception { + public static void daemonStop(String[] argv) throws Exception { if (daemonClassLoader == null) { throw new IllegalStateException("daemonStop can be called only after call to daemonStop"); } @@ -148,7 +148,7 @@ return "PrologShell".equals(cn) || "Rulec".equals(cn); } - private static String getVersion(final File me) { + private static String getVersion(File me) { if (me == null) { return ""; } @@ -163,8 +163,7 @@ } } - private static int invokeProgram(final ClassLoader loader, final String[] origArgv) - throws Exception { + private static int invokeProgram(ClassLoader loader, String[] origArgv) throws Exception { String name = origArgv[0]; final String[] argv = new String[origArgv.length - 1]; System.arraycopy(origArgv, 1, argv, 0, argv.length); @@ -316,7 +315,7 @@ } } - private static String safeName(final ZipEntry ze) { + private static String safeName(ZipEntry ze) { // Try to derive the name of the temporary file so it // doesn't completely suck. Best if we can make it // match the name it was in the archive. @@ -535,7 +534,7 @@ if (tmpEntries != null) { final long now = System.currentTimeMillis(); final long expired = now - MILLISECONDS.convert(7, DAYS); - for (final File tmpEntry : tmpEntries) { + for (File tmpEntry : tmpEntries) { if (tmpEntry.isDirectory() && tmpEntry.lastModified() < expired) { final String[] all = tmpEntry.list(); if (all == null || all.length == 0) {
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java index 5c3183a..9e375e7 100644 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java +++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/AbstractLuceneIndex.java
@@ -20,7 +20,6 @@ import com.google.common.base.Joiner; import com.google.common.collect.Sets; import com.google.common.util.concurrent.AbstractFuture; -import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -98,7 +97,7 @@ this.sitePaths = sitePaths; this.dir = dir; this.name = name; - final String index = Joiner.on('_').skipNulls().join(name, subIndex); + String index = Joiner.on('_').skipNulls().join(name, subIndex); IndexWriter delegateWriter; long commitPeriod = writerConfig.getCommitWithinMs(); @@ -121,28 +120,25 @@ @SuppressWarnings("unused") // Error handling within Runnable. Future<?> possiblyIgnoredError = autoCommitExecutor.scheduleAtFixedRate( - new Runnable() { - @Override - public void run() { + () -> { + try { + if (autoCommitWriter.hasUncommittedChanges()) { + autoCommitWriter.manualFlush(); + autoCommitWriter.commit(); + } + } catch (IOException e) { + log.error("Error committing " + index + " Lucene index", e); + } catch (OutOfMemoryError e) { + log.error("Error committing " + index + " Lucene index", e); try { - if (autoCommitWriter.hasUncommittedChanges()) { - autoCommitWriter.manualFlush(); - autoCommitWriter.commit(); - } - } catch (IOException e) { - log.error("Error committing " + index + " Lucene index", e); - } catch (OutOfMemoryError e) { - log.error("Error committing " + index + " Lucene index", e); - try { - autoCommitWriter.close(); - } catch (IOException e2) { - log.error( - "SEVERE: Error closing " - + index - + " Lucene index after OOM;" - + " index may be corrupted.", - e); - } + autoCommitWriter.close(); + } catch (IOException e2) { + log.error( + "SEVERE: Error closing " + + index + + " Lucene index after OOM;" + + " index may be corrupted.", + e); } } }, @@ -247,48 +243,27 @@ } } - ListenableFuture<?> insert(final Document doc) { - return submit( - new Callable<Long>() { - @Override - public Long call() throws IOException, InterruptedException { - return writer.addDocument(doc); - } - }); + ListenableFuture<?> insert(Document doc) { + return submit(() -> writer.addDocument(doc)); } - ListenableFuture<?> replace(final Term term, final Document doc) { - return submit( - new Callable<Long>() { - @Override - public Long call() throws IOException, InterruptedException { - return writer.updateDocument(term, doc); - } - }); + ListenableFuture<?> replace(Term term, Document doc) { + return submit(() -> writer.updateDocument(term, doc)); } - ListenableFuture<?> delete(final Term term) { - return submit( - new Callable<Long>() { - @Override - public Long call() throws IOException, InterruptedException { - return writer.deleteDocuments(term); - } - }); + ListenableFuture<?> delete(Term term) { + return submit(() -> writer.deleteDocuments(term)); } private ListenableFuture<?> submit(Callable<Long> task) { ListenableFuture<Long> future = Futures.nonCancellationPropagating(writerThread.submit(task)); return Futures.transformAsync( future, - new AsyncFunction<Long, Void>() { - @Override - public ListenableFuture<Void> apply(Long gen) throws InterruptedException { - // Tell the reopen thread a future is waiting on this - // generation so it uses the min stale time when refreshing. - reopenThread.waitForGeneration(gen, 0); - return new NrtFuture(gen); - } + gen -> { + // Tell the reopen thread a future is waiting on this + // generation so it uses the min stale time when refreshing. + reopenThread.waitForGeneration(gen, 0); + return new NrtFuture(gen); }, directExecutor()); }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneAccountIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneAccountIndex.java index fbde7be..be4f917 100644 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneAccountIndex.java +++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneAccountIndex.java
@@ -81,7 +81,7 @@ if (LuceneIndexModule.isInMemoryTest(cfg)) { return new RAMDirectory(); } - Path indexDir = LuceneVersionManager.getDir(sitePaths, ACCOUNTS + "_", schema); + Path indexDir = LuceneVersionManager.getDir(sitePaths, ACCOUNTS, schema); return FSDirectory.open(indexDir); }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java index 96986a9..c97ffc5 100644 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java +++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -61,9 +61,9 @@ import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmRuntimeException; import com.google.gwtorm.server.ResultSet; +import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; @@ -108,7 +108,7 @@ static final String UPDATED_SORT_FIELD = sortFieldName(ChangeField.UPDATED); static final String ID_SORT_FIELD = sortFieldName(ChangeField.LEGACY_ID); - private static final String CHANGES_PREFIX = "changes_"; + private static final String CHANGES = "changes"; private static final String CHANGES_OPEN = "open"; private static final String CHANGES_CLOSED = "closed"; private static final String ADDED_FIELD = ChangeField.ADDED.getName(); @@ -117,10 +117,14 @@ private static final String DELETED_FIELD = ChangeField.DELETED.getName(); private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName(); private static final String PATCH_SET_FIELD = ChangeField.PATCH_SET.getName(); + private static final String PENDING_REVIEWER_FIELD = ChangeField.PENDING_REVIEWER.getName(); + private static final String PENDING_REVIEWER_BY_EMAIL_FIELD = + ChangeField.PENDING_REVIEWER_BY_EMAIL.getName(); private static final String REF_STATE_FIELD = ChangeField.REF_STATE.getName(); private static final String REF_STATE_PATTERN_FIELD = ChangeField.REF_STATE_PATTERN.getName(); private static final String REVIEWEDBY_FIELD = ChangeField.REVIEWEDBY.getName(); private static final String REVIEWER_FIELD = ChangeField.REVIEWER.getName(); + private static final String REVIEWER_BY_EMAIL_FIELD = ChangeField.REVIEWER_BY_EMAIL.getName(); private static final String HASHTAG_FIELD = ChangeField.HASHTAG_CASE_AWARE.getName(); private static final String STAR_FIELD = ChangeField.STAR.getName(); private static final String SUBMIT_RECORD_LENIENT_FIELD = @@ -147,7 +151,7 @@ private final ChangeSubIndex openIndex; private final ChangeSubIndex closedIndex; - @AssistedInject + @Inject LuceneChangeIndex( @GerritServerConfig Config cfg, SitePaths sitePaths, @@ -177,7 +181,7 @@ new ChangeSubIndex( schema, sitePaths, new RAMDirectory(), "ramClosed", closedConfig, searcherFactory); } else { - Path dir = LuceneVersionManager.getDir(sitePaths, CHANGES_PREFIX, schema); + Path dir = LuceneVersionManager.getDir(sitePaths, CHANGES, schema); openIndex = new ChangeSubIndex( schema, sitePaths, dir.resolve(CHANGES_OPEN), openConfig, searcherFactory); @@ -459,6 +463,15 @@ if (fields.contains(REVIEWER_FIELD)) { decodeReviewers(doc, cd); } + if (fields.contains(REVIEWER_BY_EMAIL_FIELD)) { + decodeReviewersByEmail(doc, cd); + } + if (fields.contains(PENDING_REVIEWER_FIELD)) { + decodePendingReviewers(doc, cd); + } + if (fields.contains(PENDING_REVIEWER_BY_EMAIL_FIELD)) { + decodePendingReviewersByEmail(doc, cd); + } decodeSubmitRecords( doc, SUBMIT_RECORD_STRICT_FIELD, ChangeField.SUBMIT_RULE_OPTIONS_STRICT, cd); decodeSubmitRecords( @@ -555,6 +568,28 @@ FluentIterable.from(doc.get(REVIEWER_FIELD)).transform(IndexableField::stringValue))); } + private void decodeReviewersByEmail(ListMultimap<String, IndexableField> doc, ChangeData cd) { + cd.setReviewersByEmail( + ChangeField.parseReviewerByEmailFieldValues( + FluentIterable.from(doc.get(REVIEWER_BY_EMAIL_FIELD)) + .transform(IndexableField::stringValue))); + } + + private void decodePendingReviewers(ListMultimap<String, IndexableField> doc, ChangeData cd) { + cd.setPendingReviewers( + ChangeField.parseReviewerFieldValues( + FluentIterable.from(doc.get(PENDING_REVIEWER_FIELD)) + .transform(IndexableField::stringValue))); + } + + private void decodePendingReviewersByEmail( + ListMultimap<String, IndexableField> doc, ChangeData cd) { + cd.setPendingReviewersByEmail( + ChangeField.parseReviewerByEmailFieldValues( + FluentIterable.from(doc.get(PENDING_REVIEWER_BY_EMAIL_FIELD)) + .transform(IndexableField::stringValue))); + } + private void decodeSubmitRecords( ListMultimap<String, IndexableField> doc, String field,
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java index c4f10ff..daece8c 100644 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java +++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneGroupIndex.java
@@ -80,7 +80,7 @@ if (LuceneIndexModule.isInMemoryTest(cfg)) { return new RAMDirectory(); } - Path indexDir = LuceneVersionManager.getDir(sitePaths, GROUPS + "_", schema); + Path indexDir = LuceneVersionManager.getDir(sitePaths, GROUPS, schema); return FSDirectory.open(indexDir); }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java index 699fd51..b5531d5 100644 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java +++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneIndexModule.java
@@ -14,15 +14,20 @@ package com.google.gerrit.lucene; +import static com.google.common.base.Preconditions.checkArgument; + import com.google.common.collect.ImmutableMap; import com.google.gerrit.lifecycle.LifecycleModule; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.index.IndexConfig; import com.google.gerrit.server.index.IndexModule; +import com.google.gerrit.server.index.OnlineUpgrader; import com.google.gerrit.server.index.SingleVersionModule; +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 com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.inject.Singleton; import com.google.inject.assistedinject.FactoryModuleBuilder; @@ -30,30 +35,40 @@ import org.apache.lucene.search.BooleanQuery; import org.eclipse.jgit.lib.Config; -public class LuceneIndexModule extends LifecycleModule { +public class LuceneIndexModule extends AbstractModule { public static LuceneIndexModule singleVersionAllLatest(int threads) { - return new LuceneIndexModule(ImmutableMap.<String, Integer>of(), threads); + return new LuceneIndexModule(ImmutableMap.<String, Integer>of(), threads, false); } public static LuceneIndexModule singleVersionWithExplicitVersions( Map<String, Integer> versions, int threads) { - return new LuceneIndexModule(versions, threads); + return new LuceneIndexModule(versions, threads, false); } public static LuceneIndexModule latestVersionWithOnlineUpgrade() { - return new LuceneIndexModule(null, 0); + return new LuceneIndexModule(null, 0, true); + } + + public static LuceneIndexModule latestVersionWithoutOnlineUpgrade() { + return new LuceneIndexModule(null, 0, false); } static boolean isInMemoryTest(Config cfg) { return cfg.getBoolean("index", "lucene", "testInmemory", false); } - private final int threads; private final Map<String, Integer> singleVersions; + private final int threads; + private final boolean onlineUpgrade; - private LuceneIndexModule(Map<String, Integer> singleVersions, int threads) { + private LuceneIndexModule( + Map<String, Integer> singleVersions, int threads, boolean onlineUpgrade) { + if (singleVersions != null) { + checkArgument(!onlineUpgrade, "online upgrade is incompatible with single version map"); + } this.singleVersions = singleVersions; this.threads = threads; + this.onlineUpgrade = onlineUpgrade; } @Override @@ -84,13 +99,17 @@ IndexConfig getIndexConfig(@GerritServerConfig Config cfg) { BooleanQuery.setMaxClauseCount( cfg.getInt("index", "maxTerms", BooleanQuery.getMaxClauseCount())); - return IndexConfig.fromConfig(cfg); + return IndexConfig.fromConfig(cfg).separateChangeSubIndexes(true).build(); } - private static class MultiVersionModule extends LifecycleModule { + private class MultiVersionModule extends LifecycleModule { @Override public void configure() { + bind(VersionManager.class).to(LuceneVersionManager.class); listener().to(LuceneVersionManager.class); + if (onlineUpgrade) { + listener().to(OnlineUpgrader.class); + } } } }
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java index cda562b..9e8007c 100644 --- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java +++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneVersionManager.java
@@ -15,14 +15,15 @@ package com.google.gerrit.lucene; import com.google.common.primitives.Ints; -import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; -import com.google.gerrit.server.index.AbstractVersionManager; import com.google.gerrit.server.index.GerritIndexStatus; import com.google.gerrit.server.index.Index; import com.google.gerrit.server.index.IndexDefinition; +import com.google.gerrit.server.index.OnlineUpgradeListener; import com.google.gerrit.server.index.Schema; +import com.google.gerrit.server.index.VersionManager; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; @@ -36,10 +37,10 @@ import org.slf4j.LoggerFactory; @Singleton -public class LuceneVersionManager extends AbstractVersionManager implements LifecycleListener { +public class LuceneVersionManager extends VersionManager { private static final Logger log = LoggerFactory.getLogger(LuceneVersionManager.class); - private static class Version<V> extends AbstractVersionManager.Version<V> { + private static class Version<V> extends VersionManager.Version<V> { private final boolean exists; private Version(Schema<V> schema, int version, boolean exists, boolean ready) { @@ -48,33 +49,33 @@ } } - static Path getDir(SitePaths sitePaths, String prefix, Schema<?> schema) { - return sitePaths.index_dir.resolve(String.format("%s%04d", prefix, schema.getVersion())); + static Path getDir(SitePaths sitePaths, String name, Schema<?> schema) { + return sitePaths.index_dir.resolve(String.format("%s_%04d", name, schema.getVersion())); } @Inject LuceneVersionManager( @GerritServerConfig Config cfg, SitePaths sitePaths, + DynamicSet<OnlineUpgradeListener> listeners, Collection<IndexDefinition<?, ?, ?>> defs) { - super(cfg, sitePaths, defs); + super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg)); } @Override protected <V> boolean isDirty( - Collection<com.google.gerrit.server.index.AbstractVersionManager.Version<V>> inUse, - com.google.gerrit.server.index.AbstractVersionManager.Version<V> v) { + Collection<com.google.gerrit.server.index.VersionManager.Version<V>> inUse, + com.google.gerrit.server.index.VersionManager.Version<V> v) { return !inUse.contains(v) && ((Version<V>) v).exists; } @Override - protected <K, V, I extends Index<K, V>> - TreeMap<Integer, AbstractVersionManager.Version<V>> scanVersions( - IndexDefinition<K, V, I> def, GerritIndexStatus cfg) { - TreeMap<Integer, AbstractVersionManager.Version<V>> versions = new TreeMap<>(); + protected <K, V, I extends Index<K, V>> TreeMap<Integer, VersionManager.Version<V>> scanVersions( + IndexDefinition<K, V, I> def, GerritIndexStatus cfg) { + TreeMap<Integer, VersionManager.Version<V>> versions = new TreeMap<>(); for (Schema<V> schema : def.getSchemas().values()) { // This part is Lucene-specific. - Path p = getDir(sitePaths, def.getName() + "_", schema); + Path p = getDir(sitePaths, def.getName(), schema); boolean isDir = Files.isDirectory(p); if (Files.exists(p) && !isDir) { log.warn("Not a directory: %s", p.toAbsolutePath());
diff --git a/gerrit-main/src/main/java/Main.java b/gerrit-main/src/main/java/Main.java index 8c9deb1..0eca665 100644 --- a/gerrit-main/src/main/java/Main.java +++ b/gerrit-main/src/main/java/Main.java
@@ -19,7 +19,7 @@ // to jump into the real main code. // - public static void main(final String[] argv) throws Exception { + public static void main(String[] argv) throws Exception { if (onSupportedJavaVersion()) { com.google.gerrit.launcher.GerritLauncher.main(argv);
diff --git a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java index c3210ae..68b28a9d 100644 --- a/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java +++ b/gerrit-oauth/src/main/java/com/google/gerrit/httpd/auth/oauth/OAuthSession.java
@@ -32,7 +32,7 @@ import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthResult; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.auth.oauth.OAuthTokenCache; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject;
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java index e862bac..878f9ee 100644 --- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java +++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OAuthSessionOverOpenID.java
@@ -31,7 +31,7 @@ import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AuthResult; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider;
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdLoginServlet.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdLoginServlet.java index 1406267..a97e8ae 100644 --- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdLoginServlet.java +++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdLoginServlet.java
@@ -29,19 +29,17 @@ private final OpenIdServiceImpl impl; @Inject - OpenIdLoginServlet(final OpenIdServiceImpl i) { + OpenIdLoginServlet(OpenIdServiceImpl i) { impl = i; } @Override - public void doGet(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + public void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { doPost(req, rsp); } @Override - public void doPost(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + public void doPost(HttpServletRequest req, HttpServletResponse rsp) throws IOException { try { CacheHeaders.setNotCacheable(rsp); impl.doAuth(req, rsp);
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java index efe8c5f..86ed398 100644 --- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java +++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -26,7 +26,7 @@ import com.google.gerrit.server.UrlEncoded; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.auth.openid.OpenIdProviderPattern; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.ConfigUtil; @@ -99,12 +99,12 @@ @Inject OpenIdServiceImpl( - final DynamicItem<WebSession> cf, - final Provider<IdentifiedUser> iu, + DynamicItem<WebSession> cf, + Provider<IdentifiedUser> iu, CanonicalWebUrl up, - @GerritServerConfig final Config config, - final AuthConfig ac, - final AccountManager am, + @GerritServerConfig Config config, + AuthConfig ac, + AccountManager am, ProxyProperties proxyProperties) { if (proxyProperties.getProxyUrl() != null) { @@ -139,9 +139,9 @@ DiscoveryResult discover( HttpServletRequest req, String openidIdentifier, - final SignInMode mode, - final boolean remember, - final String returnToken) { + SignInMode mode, + boolean remember, + String returnToken) { final State state; state = init(req, openidIdentifier, mode, remember, returnToken); if (state == null) { @@ -183,7 +183,7 @@ return new DiscoveryResult(aReq.getDestinationUrl(false), aReq.getParameterMap()); } - private boolean requestRegistration(final AuthRequest aReq) { + private boolean requestRegistration(AuthRequest aReq) { if (AuthRequest.SELECT_ID.equals(aReq.getIdentity())) { // We don't know anything about the identity, as the provider // will offer the user a way to indicate their identity. Skip @@ -204,7 +204,7 @@ } /** Called by {@link OpenIdLoginServlet} doGet, doPost */ - void doAuth(final HttpServletRequest req, final HttpServletResponse rsp) throws Exception { + void doAuth(HttpServletRequest req, HttpServletResponse rsp) throws Exception { if (OMODE_CANCEL.equals(req.getParameter(OPENID_MODE))) { cancel(req, rsp); return; @@ -459,7 +459,7 @@ } } - private boolean isSignIn(final SignInMode mode) { + private boolean isSignIn(SignInMode mode) { switch (mode) { case SIGN_IN: case REGISTER: @@ -470,7 +470,7 @@ } } - private static SignInMode signInMode(final HttpServletRequest req) { + private static SignInMode signInMode(HttpServletRequest req) { try { return SignInMode.valueOf(req.getParameter(P_MODE)); } catch (RuntimeException e) { @@ -478,8 +478,7 @@ } } - private void callback( - final boolean isNew, final HttpServletRequest req, final HttpServletResponse rsp) + private void callback(final boolean isNew, HttpServletRequest req, HttpServletResponse rsp) throws IOException { String token = req.getParameter(P_TOKEN); if (token == null || token.isEmpty() || token.startsWith("/SignInFailure,")) { @@ -495,8 +494,7 @@ rsp.sendRedirect(rdr.toString()); } - private void cancel(final HttpServletRequest req, final HttpServletResponse rsp) - throws IOException { + private void cancel(HttpServletRequest req, HttpServletResponse rsp) throws IOException { if (isSignIn(signInMode(req))) { webSession.get().logout(); } @@ -504,7 +502,7 @@ } private void cancelWithError( - final HttpServletRequest req, final HttpServletResponse rsp, final String errorDetail) + final HttpServletRequest req, HttpServletResponse rsp, String errorDetail) throws IOException { final SignInMode mode = signInMode(req); if (isSignIn(mode)) { @@ -554,8 +552,8 @@ return new State(discovered, retTo, contextUrl); } - boolean isAllowedOpenID(final String id) { - for (final OpenIdProviderPattern pattern : allowedOpenIDs) { + boolean isAllowedOpenID(String id) { + for (OpenIdProviderPattern pattern : allowedOpenIDs) { if (pattern.matches(id)) { return true; } @@ -568,7 +566,7 @@ final UrlEncoded retTo; final String contextUrl; - State(final DiscoveryInformation d, final UrlEncoded r, final String c) { + State(DiscoveryInformation d, UrlEncoded r, String c) { discovered = d; retTo = r; contextUrl = c;
diff --git a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java index 48890dd..33dd609 100644 --- a/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java +++ b/gerrit-patch-commonsnet/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
@@ -38,15 +38,15 @@ public class AuthSMTPClient extends SMTPClient { private String authTypes; - public AuthSMTPClient(final String charset) { + public AuthSMTPClient(String charset) { super(charset); } - public void enableSSL(final boolean verify) { + public void enableSSL(boolean verify) { _socketFactory_ = sslFactory(verify); } - public boolean startTLS(final String hostname, final int port, final boolean verify) + public boolean startTLS(String hostname, int port, boolean verify) throws SocketException, IOException { if (sendCommand("STARTTLS") != 220) { return false; @@ -74,7 +74,7 @@ return true; } - private static SSLSocketFactory sslFactory(final boolean verify) { + private static SSLSocketFactory sslFactory(boolean verify) { if (verify) { return (SSLSocketFactory) SSLSocketFactory.getDefault(); } @@ -168,7 +168,7 @@ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; - private String toHex(final byte[] b) { + private String toHex(byte[] b) { final StringBuilder sec = new StringBuilder(); for (byte c : b) { final int u = (c >> 4) & 0xf; @@ -186,7 +186,7 @@ return SMTPReply.isPositiveCompletion(sendCommand("AUTH", cmd)); } - private static String encodeBase64(final byte[] data) { + private static String encodeBase64(byte[] data) { return new String(Base64.encodeBase64(data), UTF_8); } }
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java index 8090f60..9435979 100644 --- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java +++ b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
@@ -29,8 +29,7 @@ public class EditDeserializer implements JsonDeserializer<Edit>, JsonSerializer<Edit> { @Override - public Edit deserialize( - final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) + public Edit deserialize(final JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { if (json.isJsonNull()) { return null; @@ -60,7 +59,7 @@ return new ReplaceEdit(get(o, 0), get(o, 1), get(o, 2), get(o, 3), l); } - private static int get(final JsonArray a, final int idx) throws JsonParseException { + private static int get(JsonArray a, int idx) throws JsonParseException { final JsonElement v = a.get(idx); if (!v.isJsonPrimitive()) { throw new JsonParseException("Expected array of 4 for Edit type"); @@ -73,8 +72,7 @@ } @Override - public JsonElement serialize( - final Edit src, final Type typeOfSrc, final JsonSerializationContext context) { + public JsonElement serialize(final Edit src, Type typeOfSrc, JsonSerializationContext context) { if (src == null) { return JsonNull.INSTANCE; } @@ -88,7 +86,7 @@ return a; } - private void add(final JsonArray a, final Edit src) { + private void add(JsonArray a, Edit src) { a.add(new JsonPrimitive(src.getBeginA())); a.add(new JsonPrimitive(src.getEndA())); a.add(new JsonPrimitive(src.getBeginB()));
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/Edit_JsonSerializer.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/Edit_JsonSerializer.java index ce8a9f3..184cb36 100644 --- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/Edit_JsonSerializer.java +++ b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/Edit_JsonSerializer.java
@@ -46,7 +46,7 @@ } @Override - public void printJson(final StringBuilder sb, final Edit o) { + public void printJson(StringBuilder sb, Edit o) { sb.append('['); append(sb, o); if (o instanceof ReplaceEdit) { @@ -58,7 +58,7 @@ sb.append(']'); } - private void append(final StringBuilder sb, final Edit o) { + private void append(StringBuilder sb, Edit o) { sb.append(o.getBeginA()); sb.append(','); sb.append(o.getEndA());
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/lib/ObjectIdSerialization.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/lib/ObjectIdSerialization.java index 6617793..c98da64 100644 --- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/lib/ObjectIdSerialization.java +++ b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/lib/ObjectIdSerialization.java
@@ -20,8 +20,7 @@ import org.eclipse.jgit.util.IO; public class ObjectIdSerialization { - public static void writeCanBeNull(final OutputStream out, final AnyObjectId id) - throws IOException { + public static void writeCanBeNull(OutputStream out, AnyObjectId id) throws IOException { if (id != null) { out.write((byte) 1); writeNotNull(out, id); @@ -30,11 +29,11 @@ } } - public static void writeNotNull(final OutputStream out, final AnyObjectId id) throws IOException { + public static void writeNotNull(OutputStream out, AnyObjectId id) throws IOException { id.copyRawTo(out); } - public static ObjectId readCanBeNull(final InputStream in) throws IOException { + public static ObjectId readCanBeNull(InputStream in) throws IOException { switch (in.read()) { case 0: return null; @@ -45,7 +44,7 @@ } } - public static ObjectId readNotNull(final InputStream in) throws IOException { + public static ObjectId readNotNull(InputStream in) throws IOException { final byte[] b = new byte[20]; IO.readFully(in, b, 0, 20); return ObjectId.fromRaw(b);
diff --git a/gerrit-pgm/BUILD b/gerrit-pgm/BUILD index b66fd77..b58891b 100644 --- a/gerrit-pgm/BUILD +++ b/gerrit-pgm/BUILD
@@ -9,6 +9,7 @@ INIT_API_SRCS = glob([SRCS + "init/api/*.java"]) BASE_JETTY_DEPS = [ + "//gerrit-common:annotations", "//gerrit-common:server", "//gerrit-extension-api:api", "//gerrit-gwtexpui:linker_server", @@ -35,7 +36,7 @@ name = "init-api", srcs = INIT_API_SRCS, visibility = ["//visibility:public"], - deps = DEPS + ["//gerrit-common:annotations"], + deps = DEPS, ) java_library( @@ -46,7 +47,6 @@ deps = DEPS + [ ":init-api", ":util", - "//gerrit-common:annotations", "//gerrit-elasticsearch:elasticsearch", "//gerrit-launcher:launcher", # We want this dep to be provided_deps "//gerrit-lucene:lucene", @@ -156,6 +156,7 @@ name = "pgm_tests", srcs = glob(["src/test/java/**/*.java"]), deps = [ + ":http-jetty", ":init", ":init-api", ":pgm", @@ -163,6 +164,7 @@ "//gerrit-server:server", "//lib:guava", "//lib:junit", + "//lib:truth", "//lib/easymock", "//lib/guice", "//lib/jgit/org.eclipse.jgit:jgit",
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java index 803a146..757b130 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Daemon.java
@@ -14,12 +14,14 @@ package com.google.gerrit.pgm; +import static com.google.common.base.Preconditions.checkNotNull; import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER; import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.gerrit.common.EventBroker; +import com.google.gerrit.common.Nullable; import com.google.gerrit.elasticsearch.ElasticIndexModule; import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.gpg.GpgModule; @@ -69,14 +71,18 @@ import com.google.gerrit.server.index.DummyIndexModule; import com.google.gerrit.server.index.IndexModule; import com.google.gerrit.server.index.IndexModule.IndexType; +import com.google.gerrit.server.index.VersionManager; import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier; import com.google.gerrit.server.mail.receive.MailReceiver; import com.google.gerrit.server.mail.send.SmtpEmailSender; import com.google.gerrit.server.mime.MimeUtil2Module; +import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator; +import com.google.gerrit.server.notedb.rebuild.OnlineNoteDbMigrator; import com.google.gerrit.server.patch.DiffExecutorModule; import com.google.gerrit.server.plugins.PluginGuiceEnvironment; import com.google.gerrit.server.plugins.PluginModule; import com.google.gerrit.server.plugins.PluginRestApiModule; +import com.google.gerrit.server.project.DefaultPermissionBackendModule; import com.google.gerrit.server.schema.DataSourceProvider; import com.google.gerrit.server.schema.H2AccountPatchReviewStore; import com.google.gerrit.server.schema.JdbcAccountPatchReviewStore; @@ -110,6 +116,7 @@ import javax.servlet.http.HttpServletRequest; import org.eclipse.jgit.lib.Config; import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -161,6 +168,13 @@ @Option(name = "--stop-only", usage = "Stop the daemon", hidden = true) private boolean stopOnly; + @Option( + name = "--migrate-to-note-db", + usage = "(EXPERIMENTAL) Automatically migrate changes to NoteDb", + handler = ExplicitBooleanOptionHandler.class + ) + private boolean migrateToNoteDb; + private final LifecycleManager manager = new LifecycleManager(); private Injector dbInjector; private Injector cfgInjector; @@ -173,6 +187,7 @@ private boolean test; private AbstractModule luceneModule; private Module emailModule; + private Module testSysModule; private Runnable serverStarted; private IndexType indexType; @@ -190,6 +205,11 @@ sshd = enable; } + @VisibleForTesting + public boolean getEnableSshd() { + return sshd; + } + public void setEnableHttpd(boolean enable) { httpd = enable; } @@ -231,12 +251,9 @@ try { start(); RuntimeShutdown.add( - new Runnable() { - @Override - public void run() { - log.info("caught shutdown, cleaning up"); - stop(); - } + () -> { + log.info("caught shutdown, cleaning up"); + stop(); }); log.info("Gerrit Code Review " + myVersion() + " ready"); @@ -294,6 +311,11 @@ } @VisibleForTesting + public void setAdditionalSysModuleForTesting(@Nullable Module m) { + testSysModule = m; + } + + @VisibleForTesting public void start() throws IOException { if (dbInjector == null) { dbInjector = createDbInjector(true /* enableMetrics */, MULTI_USER); @@ -385,6 +407,7 @@ modules.add(cfgInjector.getInstance(GerritGlobalModule.class)); modules.add(new SearchingChangeCacheImpl.Module(slave)); modules.add(new InternalAccountDirectory.Module()); + modules.add(new DefaultPermissionBackendModule()); modules.add(new DefaultCacheFactory.Module()); modules.add(cfgInjector.getInstance(MailReceiver.Module.class)); if (emailModule != null) { @@ -438,9 +461,19 @@ modules.add(new ChangeCleanupRunner.Module()); } modules.addAll(LibModuleLoader.loadModules(cfgInjector)); + if (migrateToNoteDb()) { + modules.add(new OnlineNoteDbMigrator.Module()); + } + if (testSysModule != null) { + modules.add(testSysModule); + } return cfgInjector.createChildInjector(modules); } + private boolean migrateToNoteDb() { + return migrateToNoteDb || NoteDbMigrator.getAutoMigrate(checkNotNull(config)); + } + private Module createIndexModule() { if (slave) { return new DummyIndexModule(); @@ -448,11 +481,19 @@ if (luceneModule != null) { return luceneModule; } + boolean onlineUpgrade = + VersionManager.getOnlineUpgrade(config) + // Schema upgrade is handled by OnlineNoteDbMigrator in this case. + && !migrateToNoteDb(); switch (indexType) { case LUCENE: - return LuceneIndexModule.latestVersionWithOnlineUpgrade(); + return onlineUpgrade + ? LuceneIndexModule.latestVersionWithOnlineUpgrade() + : LuceneIndexModule.latestVersionWithoutOnlineUpgrade(); case ELASTICSEARCH: - return ElasticIndexModule.latestVersionWithOnlineUpgrade(); + return onlineUpgrade + ? ElasticIndexModule.latestVersionWithOnlineUpgrade() + : ElasticIndexModule.latestVersionWithoutOnlineUpgrade(); default: throw new IllegalStateException("unsupported index.type = " + indexType); }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java index b1a50d7..004486b 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java
@@ -45,16 +45,13 @@ manager.add(dbInjector); manager.start(); RuntimeShutdown.add( - new Runnable() { - @Override - public void run() { - try { - System.in.close(); - } catch (IOException e) { - // Ignored - } - manager.stop(); + () -> { + try { + System.in.close(); + } catch (IOException e) { + // Ignored } + manager.stop(); }); final QueryShell shell = shellFactory().create(System.in, System.out); shell.setOutputFormat(format);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java index e740ec8..e1a7bd4 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/JythonShell.java
@@ -165,7 +165,7 @@ execFile(GerritLauncher.getHomeDirectory(), STARTUP_FILE); } - protected void execResource(final String p) { + protected void execResource(String p) { try (InputStream in = JythonShell.class.getClassLoader().getResourceAsStream(p)) { if (in != null) { execStream(in, "resource " + p); @@ -177,7 +177,7 @@ } } - protected void execFile(final File parent, final String p) { + protected void execFile(File parent, String p) { try { File script = new File(parent, p); if (script.canExecute()) { @@ -200,7 +200,7 @@ } } - protected void execStream(final InputStream in, final String p) { + protected void execStream(InputStream in, String p) { try { runMethod0( console, @@ -213,7 +213,7 @@ } } - private static UnsupportedOperationException noShell(final String m, Throwable why) { + private static UnsupportedOperationException noShell(String m, Throwable why) { final String prefix = "Cannot create Jython shell: "; final String postfix = "\n (You might need to install jython.jar in the lib directory)"; return new UnsupportedOperationException(prefix + m + postfix, why);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java index 7457f40..3da3596 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -14,16 +14,17 @@ package com.google.gerrit.pgm; -import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT; import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER; import com.google.gerrit.lifecycle.LifecycleManager; import com.google.gerrit.pgm.util.SiteProgram; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.account.ExternalId; -import com.google.gerrit.server.account.ExternalIdsBatchUpdate; +import com.google.gerrit.server.account.externalids.DisabledExternalIdCache; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gerrit.server.account.externalids.ExternalIdsBatchUpdate; import com.google.gerrit.server.schema.SchemaVersionCheck; -import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Injector; import java.util.Collection; @@ -35,7 +36,7 @@ private final LifecycleManager manager = new LifecycleManager(); private final TextProgressMonitor monitor = new TextProgressMonitor(); - @Inject private SchemaFactory<ReviewDb> database; + @Inject private ExternalIds externalIds; @Inject private ExternalIdsBatchUpdate externalIdsBatchUpdate; @@ -44,19 +45,29 @@ Injector dbInjector = createDbInjector(MULTI_USER); manager.add(dbInjector, dbInjector.createChildInjector(SchemaVersionCheck.module())); manager.start(); - dbInjector.injectMembers(this); + dbInjector + .createChildInjector( + new AbstractModule() { + @Override + protected void configure() { + // The LocalUsernamesToLowerCase program needs to access all external IDs only + // once to update them. After the update they are not accessed again. Hence the + // LocalUsernamesToLowerCase program doesn't benefit from caching external IDs and + // the external ID cache can be disabled. + install(DisabledExternalIdCache.module()); + } + }) + .injectMembers(this); - try (ReviewDb db = database.open()) { - Collection<ExternalId> todo = ExternalId.from(db.accountExternalIds().all().toList()); - monitor.beginTask("Converting local usernames", todo.size()); + Collection<ExternalId> todo = externalIds.all(); + monitor.beginTask("Converting local usernames", todo.size()); - for (ExternalId extId : todo) { - convertLocalUserToLowerCase(extId); - monitor.update(1); - } - - externalIdsBatchUpdate.commit(db, "Convert local usernames to lower case"); + for (ExternalId extId : todo) { + convertLocalUserToLowerCase(extId); + monitor.update(1); } + + externalIdsBatchUpdate.commit("Convert local usernames to lower case"); monitor.endTask(); manager.stop(); return 0;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateToNoteDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateToNoteDb.java new file mode 100644 index 0000000..eeebddd --- /dev/null +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/MigrateToNoteDb.java
@@ -0,0 +1,188 @@ +// 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.pgm; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.extensions.config.FactoryModule; +import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; +import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.lifecycle.LifecycleManager; +import com.google.gerrit.pgm.util.BatchProgramModule; +import com.google.gerrit.pgm.util.RuntimeShutdown; +import com.google.gerrit.pgm.util.SiteProgram; +import com.google.gerrit.pgm.util.ThreadLimiter; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.change.ChangeResource; +import com.google.gerrit.server.index.DummyIndexModule; +import com.google.gerrit.server.index.change.ReindexAfterRefUpdate; +import com.google.gerrit.server.notedb.rebuild.NoteDbMigrator; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Provider; +import java.util.ArrayList; +import java.util.List; +import org.kohsuke.args4j.Option; +import org.kohsuke.args4j.spi.ExplicitBooleanOptionHandler; + +public class MigrateToNoteDb extends SiteProgram { + @Option(name = "--threads", usage = "Number of threads to use for rebuilding NoteDb") + private int threads = Runtime.getRuntime().availableProcessors(); + + @Option( + name = "--project", + usage = + "Only rebuild these projects, do no other migration; incompatible with --change;" + + " recommended for debugging only" + ) + private List<String> projects = new ArrayList<>(); + + @Option( + name = "--change", + usage = + "Only rebuild these changes, do no other migration; incompatible with --project;" + + " recommended for debugging only" + ) + private List<Integer> changes = new ArrayList<>(); + + @Option( + name = "--force", + usage = + "Force rebuilding changes where ReviewDb is still the source of truth, even if they" + + " were previously migrated" + ) + private boolean force; + + @Option( + name = "--trial", + usage = + "trial mode: migrate changes and turn on reading from NoteDb, but leave ReviewDb as" + + " the source of truth", + handler = ExplicitBooleanOptionHandler.class + ) + private boolean trial = true; // TODO(dborowitz): Default to false in 3.0. + + @Option( + name = "--sequence-gap", + usage = + "gap in change sequence numbers between last ReviewDb number and first NoteDb number;" + + " negative indicates using the value of noteDb.changes.initialSequenceGap (default" + + " 1000)" + ) + private int sequenceGap; + + @Option( + name = "--reindex", + usage = "Reindex all changes after migration; defaults to false in trial mode, true otherwise", + handler = ExplicitBooleanOptionHandler.class + ) + private Boolean reindex; + + private Injector dbInjector; + private Injector sysInjector; + private LifecycleManager dbManager; + private LifecycleManager sysManager; + + @Inject private Provider<NoteDbMigrator.Builder> migratorBuilderProvider; + + @Override + public int run() throws Exception { + RuntimeShutdown.add(this::stop); + try { + mustHaveValidSite(); + dbInjector = createDbInjector(MULTI_USER); + threads = ThreadLimiter.limitThreads(dbInjector, threads); + + dbManager = new LifecycleManager(); + dbManager.add(dbInjector); + dbManager.start(); + + sysInjector = createSysInjector(); + sysInjector.injectMembers(this); + sysManager = new LifecycleManager(); + sysManager.add(sysInjector); + sysManager.start(); + + try (NoteDbMigrator migrator = + migratorBuilderProvider + .get() + .setThreads(threads) + .setProgressOut(System.err) + .setProjects(projects.stream().map(Project.NameKey::new).collect(toList())) + .setChanges(changes.stream().map(Change.Id::new).collect(toList())) + .setTrialMode(trial) + .setForceRebuild(force) + .setSequenceGap(sequenceGap) + .build()) { + if (!projects.isEmpty() || !changes.isEmpty()) { + migrator.rebuild(); + } else { + migrator.migrate(); + } + } + } finally { + stop(); + } + + boolean reindex = firstNonNull(this.reindex, !trial); + if (!reindex) { + return 0; + } + // Reindex all indices, to save the user from having to run yet another program by hand while + // their server is offline. + List<String> reindexArgs = + ImmutableList.of( + "--site-path", getSitePath().toString(), "--threads", Integer.toString(threads)); + System.out.println("Migration complete, reindexing changes with:"); + System.out.println(" reindex " + reindexArgs.stream().collect(joining(" "))); + Reindex reindexPgm = new Reindex(); + return reindexPgm.main(reindexArgs.stream().toArray(String[]::new)); + } + + private Injector createSysInjector() { + return dbInjector.createChildInjector( + new FactoryModule() { + @Override + public void configure() { + install(dbInjector.getInstance(BatchProgramModule.class)); + DynamicSet.bind(binder(), GitReferenceUpdatedListener.class) + .to(ReindexAfterRefUpdate.class); + install(new DummyIndexModule()); + factory(ChangeResource.Factory.class); + } + }); + } + + private void stop() { + try { + LifecycleManager m = sysManager; + sysManager = null; + if (m != null) { + m.stop(); + } + } finally { + LifecycleManager m = dbManager; + dbManager = null; + if (m != null) { + m.stop(); + } + } + } +}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java index ad47f0c..d970856 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
@@ -85,13 +85,7 @@ Injector dbInjector = createDbInjector(SINGLE_USER); manager.add(dbInjector); manager.start(); - RuntimeShutdown.add( - new Runnable() { - @Override - public void run() { - manager.stop(); - } - }); + RuntimeShutdown.add(manager::stop); dbInjector.injectMembers(this); ProgressMonitor progress = new TextProgressMonitor();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java deleted file mode 100644 index d77717e..0000000 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/RebuildNoteDb.java +++ /dev/null
@@ -1,300 +0,0 @@ -// 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.pgm; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb; -import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER; -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.google.common.base.Predicates; -import com.google.common.base.Stopwatch; -import com.google.common.collect.ImmutableListMultimap; -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.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import com.google.gerrit.common.FormatUtil; -import com.google.gerrit.extensions.config.FactoryModule; -import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; -import com.google.gerrit.extensions.registration.DynamicSet; -import com.google.gerrit.lifecycle.LifecycleManager; -import com.google.gerrit.pgm.util.BatchProgramModule; -import com.google.gerrit.pgm.util.SiteProgram; -import com.google.gerrit.pgm.util.ThreadLimiter; -import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.reviewdb.client.RefNames; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.change.ChangeResource; -import com.google.gerrit.server.config.AllUsersName; -import com.google.gerrit.server.config.GerritServerConfig; -import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.git.WorkQueue; -import com.google.gerrit.server.index.DummyIndexModule; -import com.google.gerrit.server.index.change.ReindexAfterUpdate; -import com.google.gerrit.server.notedb.ChangeBundleReader; -import com.google.gerrit.server.notedb.NoteDbUpdateManager; -import com.google.gerrit.server.notedb.NotesMigration; -import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder; -import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException; -import com.google.gerrit.server.update.ChainedReceiveCommands; -import com.google.gwtorm.server.OrmException; -import com.google.gwtorm.server.SchemaFactory; -import com.google.inject.Inject; -import com.google.inject.Injector; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import org.eclipse.jgit.lib.BatchRefUpdate; -import org.eclipse.jgit.lib.Config; -import org.eclipse.jgit.lib.NullProgressMonitor; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.ObjectInserter; -import org.eclipse.jgit.lib.ObjectReader; -import org.eclipse.jgit.lib.ProgressMonitor; -import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.RefDatabase; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.lib.TextProgressMonitor; -import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.transport.ReceiveCommand; -import org.kohsuke.args4j.Option; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class RebuildNoteDb extends SiteProgram { - private static final Logger log = LoggerFactory.getLogger(RebuildNoteDb.class); - - @Option(name = "--threads", usage = "Number of threads to use for rebuilding NoteDb") - private int threads = Runtime.getRuntime().availableProcessors(); - - @Option(name = "--project", usage = "Projects to rebuild; recommended for debugging only") - private List<String> projects = new ArrayList<>(); - - @Option( - name = "--change", - usage = "Individual change numbers to rebuild; recommended for debugging only" - ) - private List<Integer> changes = new ArrayList<>(); - - private Injector dbInjector; - private Injector sysInjector; - - @Inject private AllUsersName allUsersName; - - @Inject private ChangeRebuilder rebuilder; - - @Inject @GerritServerConfig private Config cfg; - - @Inject private GitRepositoryManager repoManager; - - @Inject private NoteDbUpdateManager.Factory updateManagerFactory; - - @Inject private NotesMigration notesMigration; - - @Inject private SchemaFactory<ReviewDb> schemaFactory; - - @Inject private WorkQueue workQueue; - - @Inject private ChangeBundleReader bundleReader; - - @Override - public int run() throws Exception { - mustHaveValidSite(); - dbInjector = createDbInjector(MULTI_USER); - threads = ThreadLimiter.limitThreads(dbInjector, threads); - - LifecycleManager dbManager = new LifecycleManager(); - dbManager.add(dbInjector); - dbManager.start(); - - sysInjector = createSysInjector(); - sysInjector.injectMembers(this); - if (!notesMigration.enabled()) { - throw die("NoteDb is not enabled."); - } - LifecycleManager sysManager = new LifecycleManager(); - sysManager.add(sysInjector); - sysManager.start(); - - ListeningExecutorService executor = newExecutor(); - System.out.println("Rebuilding the NoteDb"); - - ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject = getChangesByProject(); - boolean ok; - Stopwatch sw = Stopwatch.createStarted(); - try (Repository allUsersRepo = repoManager.openRepository(allUsersName)) { - deleteRefs(RefNames.REFS_DRAFT_COMMENTS, allUsersRepo); - - List<ListenableFuture<Boolean>> futures = new ArrayList<>(); - List<Project.NameKey> projectNames = - Ordering.usingToString().sortedCopy(changesByProject.keySet()); - for (final Project.NameKey project : projectNames) { - ListenableFuture<Boolean> future = - executor.submit( - new Callable<Boolean>() { - @Override - public Boolean call() { - try (ReviewDb db = unwrapDb(schemaFactory.open())) { - return rebuildProject(db, changesByProject, project, allUsersRepo); - } catch (Exception e) { - log.error("Error rebuilding project " + project, e); - return false; - } - } - }); - futures.add(future); - } - - try { - ok = Iterables.all(Futures.allAsList(futures).get(), Predicates.equalTo(true)); - } catch (InterruptedException | ExecutionException e) { - log.error("Error rebuilding projects", e); - ok = false; - } - } - - double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d; - System.out.format( - "Rebuild %d changes in %.01fs (%.01f/s)\n", - changesByProject.size(), t, changesByProject.size() / t); - return ok ? 0 : 1; - } - - private static void execute(BatchRefUpdate bru, Repository repo) throws IOException { - try (RevWalk rw = new RevWalk(repo)) { - bru.execute(rw, NullProgressMonitor.INSTANCE); - } - for (ReceiveCommand command : bru.getCommands()) { - if (command.getResult() != ReceiveCommand.Result.OK) { - throw new IOException( - String.format("Command %s failed: %s", command.toString(), command.getResult())); - } - } - } - - private void deleteRefs(String prefix, Repository allUsersRepo) throws IOException { - RefDatabase refDb = allUsersRepo.getRefDatabase(); - Map<String, Ref> allRefs = refDb.getRefs(prefix); - BatchRefUpdate bru = refDb.newBatchUpdate(); - for (Map.Entry<String, Ref> ref : allRefs.entrySet()) { - bru.addCommand( - new ReceiveCommand( - ref.getValue().getObjectId(), ObjectId.zeroId(), prefix + ref.getKey())); - } - execute(bru, allUsersRepo); - } - - private Injector createSysInjector() { - return dbInjector.createChildInjector( - new FactoryModule() { - @Override - public void configure() { - install(dbInjector.getInstance(BatchProgramModule.class)); - DynamicSet.bind(binder(), GitReferenceUpdatedListener.class) - .to(ReindexAfterUpdate.class); - install(new DummyIndexModule()); - factory(ChangeResource.Factory.class); - } - }); - } - - private ListeningExecutorService newExecutor() { - if (threads > 0) { - return MoreExecutors.listeningDecorator(workQueue.createQueue(threads, "RebuildChange")); - } - return MoreExecutors.newDirectExecutorService(); - } - - private ImmutableListMultimap<Project.NameKey, Change.Id> getChangesByProject() - throws OrmException { - // Memorize all changes so we can close the db connection and allow - // rebuilder threads to use the full connection pool. - ListMultimap<Project.NameKey, Change.Id> changesByProject = - MultimapBuilder.hashKeys().arrayListValues().build(); - try (ReviewDb db = schemaFactory.open()) { - if (projects.isEmpty() && !changes.isEmpty()) { - Iterable<Change> todo = - unwrapDb(db).changes().get(Iterables.transform(changes, Change.Id::new)); - for (Change c : todo) { - changesByProject.put(c.getProject(), c.getId()); - } - } else { - for (Change c : unwrapDb(db).changes().all()) { - boolean include = false; - if (projects.isEmpty() && changes.isEmpty()) { - include = true; - } else if (!projects.isEmpty() && projects.contains(c.getProject().get())) { - include = true; - } else if (!changes.isEmpty() && changes.contains(c.getId().get())) { - include = true; - } - if (include) { - changesByProject.put(c.getProject(), c.getId()); - } - } - } - return ImmutableListMultimap.copyOf(changesByProject); - } - } - - private boolean rebuildProject( - ReviewDb db, - ImmutableListMultimap<Project.NameKey, Change.Id> allChanges, - Project.NameKey project, - Repository allUsersRepo) - throws IOException, OrmException { - checkArgument(allChanges.containsKey(project)); - boolean ok = true; - ProgressMonitor pm = - new TextProgressMonitor( - new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8)))); - pm.beginTask(FormatUtil.elide(project.get(), 50), allChanges.get(project).size()); - try (NoteDbUpdateManager manager = updateManagerFactory.create(project); - ObjectInserter allUsersInserter = allUsersRepo.newObjectInserter(); - ObjectReader reader = allUsersInserter.newReader(); - RevWalk allUsersRw = new RevWalk(reader)) { - manager.setAllUsersRepo( - allUsersRepo, allUsersRw, allUsersInserter, new ChainedReceiveCommands(allUsersRepo)); - for (Change.Id changeId : allChanges.get(project)) { - try { - rebuilder.buildUpdates(manager, bundleReader.fromReviewDb(db, changeId)); - } catch (NoPatchSetsException e) { - log.warn(e.getMessage()); - } catch (Throwable t) { - log.error("Failed to rebuild change " + changeId, t); - ok = false; - } - pm.update(1); - } - manager.execute(); - } finally { - pm.endTask(); - } - return ok; - } -}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java index 9f54634..011ebf0 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
@@ -14,10 +14,18 @@ package com.google.gerrit.pgm.http.jetty; +import static com.google.gerrit.httpd.restapi.RestApiServlet.XD_AUTHORIZATION; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.httpd.GetUserFilter; import com.google.gerrit.server.util.SystemLog; import com.google.inject.Inject; +import java.util.Iterator; import org.apache.log4j.AsyncAppender; import org.apache.log4j.Level; import org.apache.log4j.Logger; @@ -31,6 +39,7 @@ class HttpLog extends AbstractLifeCycle implements RequestLog { private static final Logger log = Logger.getLogger(HttpLog.class); private static final String LOG_NAME = "httpd_log"; + private static final ImmutableSet<String> REDACT_PARAM = ImmutableSet.of(XD_AUTHORIZATION); interface HttpLogFactory { HttpLog get(); @@ -49,7 +58,7 @@ private final AsyncAppender async; @Inject - HttpLog(final SystemLog systemLog) { + HttpLog(SystemLog systemLog) { async = systemLog.createAsyncAppender(LOG_NAME, new HttpLogLayout()); } @@ -62,7 +71,7 @@ } @Override - public void log(final Request req, final Response rsp) { + public void log(Request req, Response rsp) { final LoggingEvent event = new LoggingEvent( // Logger.class.getName(), // fqnOfCategoryClass @@ -78,10 +87,7 @@ ); String uri = req.getRequestURI(); - String qs = req.getQueryString(); - if (qs != null) { - uri = uri + "?" + qs; - } + uri = redactQueryString(uri, req.getQueryString()); String user = (String) req.getAttribute(GetUserFilter.REQ_ATTR_KEY); if (user != null) { @@ -100,6 +106,31 @@ async.append(event); } + @VisibleForTesting + static String redactQueryString(String uri, String qs) { + if (Strings.isNullOrEmpty(qs)) { + return uri; + } + + StringBuilder b = new StringBuilder(uri); + boolean first = true; + for (String kvPair : Splitter.on('&').split(qs)) { + Iterator<String> i = Splitter.on('=').limit(2).split(kvPair).iterator(); + String key = i.next(); + b.append(first ? '?' : '&').append(key); + first = false; + if (i.hasNext()) { + b.append('='); + if (REDACT_PARAM.contains(Url.decode(key))) { + b.append('*'); + } else { + b.append(i.next()); + } + } + } + return b.toString(); + } + private static void set(LoggingEvent event, String key, String val) { if (val != null && !val.isEmpty()) { event.setProperty(key, val);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLogLayout.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLogLayout.java index bfa4d64..2eea88d 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLogLayout.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLogLayout.java
@@ -96,7 +96,7 @@ } } - private void formatDate(final long now, final StringBuilder sbuf) { + private void formatDate(long now, StringBuilder sbuf) { final long rounded = now - (int) (now % 1000); if (rounded != lastTimeMillis) { synchronized (dateFormat) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyEnv.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyEnv.java index ebca467..1d3e1702 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyEnv.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyEnv.java
@@ -19,7 +19,7 @@ public class JettyEnv { final Injector webInjector; - public JettyEnv(final Injector webInjector) { + public JettyEnv(Injector webInjector) { this.webInjector = webInjector; } }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyModule.java index d356d96..c818276 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyModule.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyModule.java
@@ -21,7 +21,7 @@ public class JettyModule extends LifecycleModule { private final JettyEnv env; - public JettyModule(final JettyEnv env) { + public JettyModule(JettyEnv env) { this.env = env; }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java index c7606d6..79bc224 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -79,7 +79,7 @@ private final Config cfg; @Inject - Lifecycle(final JettyServer server, @GerritServerConfig final Config cfg) { + Lifecycle(JettyServer server, @GerritServerConfig Config cfg) { this.server = server; this.cfg = cfg; } @@ -297,7 +297,7 @@ return config; } - static boolean isReverseProxied(final URI[] listenUrls) { + static boolean isReverseProxied(URI[] listenUrls) { for (URI u : listenUrls) { if ("http".equals(u.getScheme()) || "https".equals(u.getScheme())) { return false; @@ -306,7 +306,7 @@ return true; } - static URI[] listenURLs(final Config cfg) { + static URI[] listenURLs(Config cfg) { String[] urls = cfg.getStringList("httpd", null, "listenurl"); if (urls.length == 0) { urls = new String[] {"http://*:8080/"}; @@ -352,7 +352,7 @@ return pool; } - private Handler makeContext(final JettyEnv env, final Config cfg) { + private Handler makeContext(JettyEnv env, Config cfg) { final Set<String> paths = new HashSet<>(); for (URI u : listenURLs(cfg)) { String p = u.getPath(); @@ -385,8 +385,7 @@ return r; } - private ContextHandler makeContext( - final String contextPath, final JettyEnv env, final Config cfg) { + private ContextHandler makeContext(final String contextPath, JettyEnv env, Config cfg) { final ServletContextHandler app = new ServletContextHandler(); // This enables the use of sessions in Jetty, feature available
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java index ccaee8f..8561dce 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
@@ -20,9 +20,9 @@ import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.git.QueueProvider; -import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.git.WorkQueue.CancelableRunnable; import com.google.gerrit.sshd.CommandExecutorQueueProvider; import com.google.inject.Inject; @@ -30,6 +30,8 @@ import com.google.inject.Singleton; import com.google.inject.servlet.ServletModule; import java.io.IOException; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.Filter; @@ -69,7 +71,6 @@ private static final Pattern URI_PATTERN = Pattern.compile(FILTER_RE); public static class Module extends ServletModule { - @Override protected void configureServlets() { bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON); @@ -77,18 +78,20 @@ } } + private final CapabilityControl.Factory capabilityFactory; private final Provider<CurrentUser> user; private final QueueProvider queue; - private final ServletContext context; private final long maxWait; @Inject ProjectQoSFilter( - final Provider<CurrentUser> user, + CapabilityControl.Factory capabilityFactory, + Provider<CurrentUser> user, QueueProvider queue, - final ServletContext context, - @GerritServerConfig final Config cfg) { + ServletContext context, + @GerritServerConfig Config cfg) { + this.capabilityFactory = capabilityFactory; this.user = user; this.queue = queue; this.context = context; @@ -102,18 +105,16 @@ final HttpServletResponse rsp = (HttpServletResponse) response; final Continuation cont = ContinuationSupport.getContinuation(req); - WorkQueue.Executor executor = getExecutor(); - if (cont.isInitial()) { - TaskThunk task = new TaskThunk(executor, cont, req); + TaskThunk task = new TaskThunk(cont, req); if (maxWait > 0) { cont.setTimeout(maxWait); } cont.suspend(rsp); - cont.addContinuationListener(task); cont.setAttribute(TASK, task); - executor.submit(task); + Future<?> f = getExecutor().submit(task); + cont.addContinuationListener(new Listener(f)); } else if (cont.isExpired()) { rsp.sendError(SC_SERVICE_UNAVAILABLE); @@ -136,8 +137,9 @@ } } - private WorkQueue.Executor getExecutor() { - return queue.getQueue(user.get().getCapabilities().getQueueType()); + private ScheduledThreadPoolExecutor getExecutor() { + QueueProvider.QueueType qt = capabilityFactory.create(user.get()).getQueueType(); + return queue.getQueue(qt); } @Override @@ -146,18 +148,30 @@ @Override public void destroy() {} - private final class TaskThunk implements CancelableRunnable, ContinuationListener { + private final class Listener implements ContinuationListener { + final Future<?> future; - private final WorkQueue.Executor executor; + Listener(Future<?> future) { + this.future = future; + } + + @Override + public void onComplete(Continuation self) {} + + @Override + public void onTimeout(Continuation self) { + future.cancel(true); + } + } + + private final class TaskThunk implements CancelableRunnable { private final Continuation cont; private final String name; private final Object lock = new Object(); private boolean done; private Thread worker; - TaskThunk( - final WorkQueue.Executor executor, final Continuation cont, final HttpServletRequest req) { - this.executor = executor; + TaskThunk(Continuation cont, HttpServletRequest req) { this.cont = cont; this.name = generateName(req); } @@ -202,14 +216,6 @@ } @Override - public void onComplete(Continuation self) {} - - @Override - public void onTimeout(Continuation self) { - executor.remove(this); - } - - @Override public String toString() { return name; }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java new file mode 100644 index 0000000..81435e0 --- /dev/null +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/AccountsOnInit.java
@@ -0,0 +1,91 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.init; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.pgm.init.api.InitFlags; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdentProvider; +import com.google.gerrit.server.account.Accounts; +import com.google.gerrit.server.account.AccountsUpdate; +import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import org.eclipse.jgit.internal.storage.file.FileRepository; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryCache.FileKey; +import org.eclipse.jgit.util.FS; + +public class AccountsOnInit { + private final InitFlags flags; + private final SitePaths site; + private final String allUsers; + + @Inject + public AccountsOnInit(InitFlags flags, SitePaths site, AllUsersNameOnInitProvider allUsers) { + this.flags = flags; + this.site = site; + this.allUsers = allUsers.get(); + } + + public void insert(ReviewDb db, Account account) throws OrmException, IOException { + db.accounts().insert(ImmutableSet.of(account)); + + File path = getPath(); + if (path != null) { + try (Repository repo = new FileRepository(path); + ObjectInserter oi = repo.newObjectInserter()) { + PersonIdent serverIdent = new GerritPersonIdentProvider(flags.cfg).get(); + AccountsUpdate.createUserBranch( + repo, + new Project.NameKey(allUsers), + GitReferenceUpdated.DISABLED, + null, + oi, + serverIdent, + serverIdent, + account.getId(), + account.getRegisteredOn()); + } + } + } + + public boolean hasAnyAccount() throws IOException { + File path = getPath(); + if (path == null) { + return false; + } + + try (Repository repo = new FileRepository(path)) { + return Accounts.hasAnyAccount(repo); + } + } + + private File getPath() { + Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath")); + checkArgument(basePath != null, "gerrit.basePath must be configured"); + return FileKey.resolve(basePath.resolve(allUsers).toFile(), FS.DETECTED); + } +}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java index ae5a598..49fd1f9 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -129,6 +129,7 @@ init.flags.dev = isDev() && init.site.isNew; init.flags.skipPlugins = skipPlugins(); init.flags.deleteCaches = getDeleteCaches(); + init.flags.isNew = init.site.isNew; final SiteRun run; try { @@ -437,11 +438,11 @@ } } - private SiteRun createSiteRun(final SiteInit init) { + private SiteRun createSiteRun(SiteInit init) { return createSysInjector(init).getInstance(SiteRun.class); } - private Injector createSysInjector(final SiteInit init) { + private Injector createSysInjector(SiteInit init) { if (sysInjector == null) { final List<Module> modules = new ArrayList<>(); modules.add(
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java index 8868a31..2e49e13 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
@@ -29,7 +29,7 @@ private final Config cfg; @Inject - Browser(@GerritServerConfig final Config cfg) { + Browser(@GerritServerConfig Config cfg) { this.cfg = cfg; } @@ -37,7 +37,7 @@ open(null /* root page */); } - public void open(final String link) throws Exception { + public void open(String link) throws Exception { String url = cfg.getString("gerrit", null, "canonicalWebUrl"); if (url == null) { url = cfg.getString("httpd", null, "listenUrl");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java index b80bf35..44f883a 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
@@ -22,7 +22,7 @@ private final SitePaths site; - public DatabaseConfigModule(final SitePaths site) { + public DatabaseConfigModule(SitePaths site) { this.site = site; }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DerbyInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DerbyInitializer.java index 5db4287..3aad0f4 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DerbyInitializer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DerbyInitializer.java
@@ -27,7 +27,7 @@ private final SitePaths site; @Inject - DerbyInitializer(final SitePaths site) { + DerbyInitializer(SitePaths site) { this.site = site; }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java index 86c5f45e..9906089 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
@@ -14,14 +14,11 @@ package com.google.gerrit.pgm.init; -import static com.google.gerrit.server.account.ExternalId.toAccountExternalIds; - import com.google.gerrit.pgm.init.api.InitFlags; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.GerritPersonIdentProvider; -import com.google.gerrit.server.account.ExternalId; -import com.google.gerrit.server.account.ExternalIds; -import com.google.gerrit.server.account.ExternalIdsUpdate; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIdReader; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; import com.google.gerrit.server.config.SitePaths; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -52,18 +49,17 @@ this.allUsers = allUsers.get(); } - public synchronized void insert(ReviewDb db, String commitMessage, Collection<ExternalId> extIds) + public synchronized void insert(String commitMessage, Collection<ExternalId> extIds) throws OrmException, IOException, ConfigInvalidException { - db.accountExternalIds().insert(toAccountExternalIds(extIds)); File path = getPath(); if (path != null) { try (Repository repo = new FileRepository(path); RevWalk rw = new RevWalk(repo); ObjectInserter ins = repo.newObjectInserter()) { - ObjectId rev = ExternalIds.readRevision(repo); + ObjectId rev = ExternalIdReader.readRevision(repo); - NoteMap noteMap = ExternalIds.readNoteMap(rw, rev); + NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev); for (ExternalId extId : extIds) { ExternalIdsUpdate.insert(rw, ins, noteMap, extId); }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java index 1f3fd0f..63aa6ec 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java
@@ -27,7 +27,7 @@ private final SitePaths site; @Inject - H2Initializer(final SitePaths site) { + H2Initializer(SitePaths site) { this.site = site; }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/HANAInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/HANAInitializer.java index bc39799..713392d 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/HANAInitializer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/HANAInitializer.java
@@ -16,27 +16,17 @@ import static com.google.gerrit.pgm.init.api.InitUtil.username; -import com.google.common.primitives.Ints; -import com.google.gerrit.pgm.init.api.InitUtil; import com.google.gerrit.pgm.init.api.Section; public class HANAInitializer implements DatabaseConfigInitializer { @Override public void initConfig(Section databaseSection) { - final String defInstanceNumber = "00"; + final String defPort = "(hana default)"; databaseSection.string("Server hostname", "hostname", "localhost"); - databaseSection.string("Instance number", "instance", defInstanceNumber, false); - String instance = databaseSection.get("instance"); - Integer instanceNumber = Ints.tryParse(instance); - if (instanceNumber == null || instanceNumber < 0 || instanceNumber > 99) { - instanceIsInvalid(); - } + databaseSection.string("Server port", "port", defPort, true); + databaseSection.string("Database name", "database", null); databaseSection.string("Database username", "username", username()); databaseSection.password("username", "password"); } - - private void instanceIsInvalid() { - throw InitUtil.die("database.instance must be in the range of 00 to 99"); - } }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java index 68b2b96..7162eab 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -29,7 +29,7 @@ import com.google.gerrit.reviewdb.client.AccountSshKey; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.AccountState; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.index.account.AccountIndex; import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gwtorm.server.SchemaFactory; @@ -47,6 +47,7 @@ public class InitAdminUser implements InitStep { private final ConsoleUI ui; private final InitFlags flags; + private final AccountsOnInit accounts; private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory; private final ExternalIdsOnInit externalIds; private SchemaFactory<ReviewDb> dbFactory; @@ -56,10 +57,12 @@ InitAdminUser( InitFlags flags, ConsoleUI ui, + AccountsOnInit accounts, VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory, ExternalIdsOnInit externalIds) { this.flags = flags; this.ui = ui; + this.accounts = accounts; this.authorizedKeysFactory = authorizedKeysFactory; this.externalIds = externalIds; } @@ -85,7 +88,7 @@ } try (ReviewDb db = dbFactory.open()) { - if (db.accounts().anyAccounts().toList().isEmpty()) { + if (!accounts.hasAnyAccount()) { ui.header("Gerrit Administrator"); if (ui.yesno(true, "Create administrator user")) { Account.Id id = new Account.Id(db.nextAccountId()); @@ -101,12 +104,12 @@ if (email != null) { extIds.add(ExternalId.createEmail(id, email)); } - externalIds.insert(db, "Add external IDs for initial admin user", extIds); + externalIds.insert("Add external IDs for initial admin user", extIds); Account a = new Account(id, TimeUtil.nowTs()); a.setFullName(name); a.setPreferredEmail(email); - db.accounts().insert(Collections.singleton(a)); + accounts.insert(db, a); AccountGroupName adminGroupName = db.accountGroupNames().get(new AccountGroup.NameKey("Administrators"));
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java index 3958069..dea45a71 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
@@ -41,7 +41,7 @@ private final Section container; @Inject - InitContainer(final ConsoleUI ui, final SitePaths site, final Section.Factory sections) { + InitContainer(ConsoleUI ui, SitePaths site, Section.Factory sections) { this.ui = ui; this.site = site; this.container = sections.get("container", null);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitExperimental.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitExperimental.java new file mode 100644 index 0000000..769cdb4 --- /dev/null +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitExperimental.java
@@ -0,0 +1,83 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.init; + +import static com.google.gerrit.server.notedb.ConfigNotesMigration.SECTION_NOTE_DB; +import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES; + +import com.google.gerrit.extensions.client.UiType; +import com.google.gerrit.pgm.init.api.ConsoleUI; +import com.google.gerrit.pgm.init.api.InitFlags; +import com.google.gerrit.pgm.init.api.InitStep; +import com.google.gerrit.pgm.init.api.Section; +import com.google.gerrit.server.notedb.ConfigNotesMigration; +import com.google.inject.Inject; +import java.util.Locale; +import javax.inject.Singleton; +import org.eclipse.jgit.lib.Config; + +@Singleton +class InitExperimental implements InitStep { + private final ConsoleUI ui; + private final InitFlags flags; + private final Section noteDbChanges; + private final Section gerrit; + + @Inject + InitExperimental(ConsoleUI ui, InitFlags flags, Section.Factory sections) { + this.ui = ui; + this.flags = flags; // Don't grab any flags yet; they aren't initialized until BaseInit#run. + this.noteDbChanges = sections.get(SECTION_NOTE_DB, CHANGES.key()); + this.gerrit = sections.get("gerrit", null); + } + + @Override + public void run() { + ui.header("Experimental features"); + if (!ui.yesno(false, "Enable any experimental features")) { + return; + } + + if (flags.isNew) { + initNoteDb(); + } + initUis(); + } + + private void initNoteDb() { + ui.message( + "Use experimental NoteDb for change metadata?\n" + + " NoteDb is not recommended for production servers." + + " Please familiarize yourself with the documentation:\n" + + " https://gerrit-review.googlesource.com/Documentation/dev-note-db.html\n"); + if (!ui.yesno(false, "Enable")) { + return; + } + + Config defaultConfig = ConfigNotesMigration.allEnabledConfig(); + for (String name : defaultConfig.getNames(SECTION_NOTE_DB, CHANGES.key())) { + noteDbChanges.set(name, defaultConfig.getString(SECTION_NOTE_DB, CHANGES.key(), name)); + } + } + + private void initUis() { + boolean pg = ui.yesno(true, "Default to PolyGerrit UI"); + UiType uiType = pg ? UiType.POLYGERRIT : UiType.GWT; + gerrit.set("ui", uiType.name().toLowerCase(Locale.US)); + if (pg) { + gerrit.set("enableGwtUi", Boolean.toString(ui.yesno(true, "Enable GWT UI"))); + } + } +}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java index fc42f9d..e57b6b9 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
@@ -31,7 +31,7 @@ private final Section gerrit; @Inject - InitGitManager(final ConsoleUI ui, final Section.Factory sections) { + InitGitManager(ConsoleUI ui, Section.Factory sections) { this.ui = ui; this.gerrit = sections.get("gerrit", null); }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java index f6b7e6a..f75d2dc 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
@@ -15,7 +15,6 @@ package com.google.gerrit.pgm.init; import com.google.gerrit.extensions.config.FactoryModule; -import com.google.gerrit.pgm.init.api.InitFlags; import com.google.gerrit.pgm.init.api.InitStep; import com.google.gerrit.pgm.init.api.Section; import com.google.gerrit.server.config.SitePaths; @@ -37,7 +36,6 @@ @Override protected void configure() { bind(SitePaths.class); - bind(InitFlags.class); bind(Libraries.class); bind(LibraryDownloader.class); factory(Section.Factory.class); @@ -64,6 +62,7 @@ step().to(InitCache.class); step().to(InitPlugins.class); step().to(InitDev.class); + step().to(InitExperimental.class); } protected LinkedBindingBuilder<InitStep> step() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java index 4526a87..9f02a56 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
@@ -43,8 +43,7 @@ final ConsoleUI ui; @Inject - public InitPluginStepsLoader( - final ConsoleUI ui, final SitePaths sitePaths, final Injector initInjector) { + public InitPluginStepsLoader(final ConsoleUI ui, SitePaths sitePaths, Injector initInjector) { this.pluginsDir = sitePaths.plugins_dir; this.initInjector = initInjector; this.ui = ui;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java index 97359b3..666b549 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
@@ -34,7 +34,7 @@ private final SitePaths site; @Inject - InitSendEmail(final ConsoleUI ui, final SitePaths site, final Section.Factory sections) { + InitSendEmail(ConsoleUI ui, SitePaths site, Section.Factory sections) { this.ui = ui; this.sendemail = sections.get("sendemail", null); this.site = site;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java index 3259f96..c599e99 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
@@ -122,7 +122,7 @@ return val; } - private static String read(final String p) throws IOException { + private static String read(String p) throws IOException { try (InputStream in = Libraries.class.getClassLoader().getResourceAsStream(p)) { if (in == null) { throw new FileNotFoundException("Cannot load resource " + p);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java index 0ba4083..e0589ed 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
@@ -65,16 +65,16 @@ this.needs = new ArrayList<>(2); } - void setName(final String name) { + void setName(String name) { this.name = name; } - void setJarUrl(final String url) { + void setJarUrl(String url) { this.jarUrl = url; download = jarUrl.startsWith("http"); } - void setSHA1(final String sha1) { + void setSHA1(String sha1) { this.sha1 = sha1; } @@ -230,7 +230,7 @@ Files.copy(p, dst); } - private static Path url2file(final String urlString) throws IOException { + private static Path url2file(String urlString) throws IOException { final URL url = new URL(urlString); try { return Paths.get(url.toURI()); @@ -280,7 +280,7 @@ System.err.flush(); return; } - Hasher h = Hashing.sha1().newHasher(); + Hasher h = Hashing.murmur3_128().newHasher(); try (InputStream in = Files.newInputStream(dst); OutputStream out = Funnels.asOutputStream(h)) { ByteStreams.copy(in, out);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java index 243ea09..be61061 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -165,7 +165,7 @@ chmod(0444, ex); } - private static List<InitStep> stepsOf(final Injector injector) { + private static List<InitStep> stepsOf(Injector injector) { final ArrayList<InitStep> r = new ArrayList<>(); for (Binding<InitStep> b : all(injector)) { r.add(b.getProvider().get()); @@ -173,7 +173,7 @@ return r; } - private static List<Binding<InitStep>> all(final Injector injector) { + private static List<Binding<InitStep>> all(Injector injector) { return injector.findBindingsByType(new TypeLiteral<InitStep>() {}); } }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java index d5d7e78..f994432 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
@@ -162,8 +162,7 @@ savePublic(cfg); } - private boolean convertUrl(final Section database, String url) - throws UnsupportedEncodingException { + private boolean convertUrl(Section database, String url) throws UnsupportedEncodingException { String username = null; String password = null; @@ -243,14 +242,14 @@ return false; } - private void sethost(final Section database, final InetSocketAddress addr) { + private void sethost(Section database, InetSocketAddress addr) { database.set("hostname", SocketUtil.hostname(addr)); if (0 < addr.getPort()) { database.set("port", String.valueOf(addr.getPort())); } } - private void setuser(final Section database, String username, String password) { + private void setuser(Section database, String username, String password) { if (username != null && !username.isEmpty()) { database.set("username", username); } @@ -278,7 +277,7 @@ throw new IOException("Cannot read " + name, e); } final Properties dbprop = new Properties(); - for (final Map.Entry<Object, Object> e : srvprop.entrySet()) { + for (Map.Entry<Object, Object> e : srvprop.entrySet()) { final String key = (String) e.getKey(); if (key.startsWith("database.")) { dbprop.put(key.substring("database.".length()), e.getValue());
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java index 18ccb1a..2068540 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/ConsoleUI.java
@@ -27,7 +27,7 @@ } /** Get a UI instance, possibly forcing batch mode. */ - public static ConsoleUI getInstance(final boolean batchMode) { + public static ConsoleUI getInstance(boolean batchMode) { Console console = batchMode ? null : System.console(); return console != null ? new Interactive(console) : new Batch(); } @@ -87,7 +87,7 @@ private static class Interactive extends ConsoleUI { private final Console console; - Interactive(final Console console) { + Interactive(Console console) { this.console = console; } @@ -164,7 +164,7 @@ console.printf("error: '%s' is not a valid choice\n", r); } console.printf(" Supported options are:\n"); - for (final String v : allowedValues) { + for (String v : allowedValues) { console.printf(" %s\n", v.toLowerCase()); } } @@ -207,7 +207,7 @@ if (r.isEmpty()) { return def; } - for (final T e : options) { + for (T e : options) { if (e.toString().equalsIgnoreCase(r)) { return e; } @@ -216,7 +216,7 @@ console.printf("error: '%s' is not a valid choice\n", r); } console.printf(" Supported options are:\n"); - for (final T e : options) { + for (T e : options) { console.printf(" %s\n", e.toString().toLowerCase()); } }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java index 691243f..7bb1366 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitFlags.java
@@ -31,6 +31,9 @@ /** Recursively delete the site path if initialization fails. */ public boolean deleteOnFailure; + /** Site is being newly created */ + public boolean isNew; + /** Run the daemon (and open the web UI in a browser) after initialization. */ public boolean autoStart;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitUtil.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitUtil.java index b80cb22..656f53a 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitUtil.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/InitUtil.java
@@ -46,7 +46,7 @@ return new Die(why, cause); } - public static void savePublic(final FileBasedConfig sec) throws IOException { + public static void savePublic(FileBasedConfig sec) throws IOException { if (modified(sec)) { sec.save(); } @@ -79,7 +79,7 @@ return SystemReader.getInstance().getHostname(); } - public static boolean isLocal(final String hostname) { + public static boolean isLocal(String hostname) { try { return InetAddress.getByName(hostname).isLoopbackAddress(); } catch (UnknownHostException e) { @@ -127,7 +127,7 @@ } } - private static InputStream open(final Class<?> sibling, final String name) { + private static InputStream open(Class<?> sibling, String name) { final InputStream in = sibling.getResourceAsStream(name); if (in == null) { String pkg = sibling.getName(); @@ -186,12 +186,12 @@ return new URI(url); } - public static boolean isAnyAddress(final URI u) { + public static boolean isAnyAddress(URI u) { return u.getHost() == null && (u.getAuthority().equals("*") || u.getAuthority().startsWith("*:")); } - public static int portOf(final URI uri) { + public static int portOf(URI uri) { int port = uri.getPort(); if (port < 0) { port = "https".equals(uri.getScheme()) ? 443 : 80;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java index d52005f..c1c8745 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/Section.java
@@ -58,7 +58,7 @@ return flags.cfg.getString(section, subsection, name); } - public void set(final String name, final String value) { + public void set(String name, String value) { final ArrayList<String> all = new ArrayList<>(); all.addAll(Arrays.asList(flags.cfg.getStringList(section, subsection, name))); @@ -78,7 +78,7 @@ } } - public <T extends Enum<?>> void set(final String name, final T value) { + public <T extends Enum<?>> void set(String name, T value) { if (value != null) { set(name, value.name()); } else { @@ -90,12 +90,11 @@ set(name, (String) null); } - public String string(final String title, final String name, final String dv) { + public String string(String title, String name, String dv) { return string(title, name, dv, false); } - public String string( - final String title, final String name, final String dv, final boolean nullIfDefault) { + 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); if (nullIfDefault && nv.equals(dv)) { @@ -107,7 +106,7 @@ return nv; } - public Path path(final String title, final String name, final String defValue) { + public Path path(String title, String name, String defValue) { return site.resolve(string(title, name, defValue)); } @@ -129,7 +128,7 @@ } public <T extends Enum<?>, A extends EnumSet<? extends T>> T select( - String title, String name, T defValue, A allowedValues, final boolean nullIfDefault) { + String title, String name, T defValue, A allowedValues, boolean nullIfDefault) { final boolean set = get(name) != null; T oldValue = flags.cfg.getEnum(section, subsection, name, defValue); T newValue = ui.readEnum(oldValue, allowedValues, "%s", title); @@ -146,8 +145,7 @@ return newValue; } - public String select( - final String title, final String name, final String dv, Set<String> allowedValues) { + public String select(final String title, String name, String dv, Set<String> allowedValues) { final String ov = get(name); String nv = ui.readString(ov != null ? ov : dv, allowedValues, "%s", title); if (!eq(ov, nv)) { @@ -156,7 +154,7 @@ return nv; } - public String password(final String username, final String password) { + public String password(String username, String password) { final String ov = getSecure(password); String user = flags.sec.get(section, subsection, username); @@ -219,7 +217,7 @@ return section; } - private static boolean eq(final String a, final String b) { + private static boolean eq(String a, String b) { if (a == null && b == null) { return true; }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java index 825bd70..fca5551 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
@@ -38,7 +38,7 @@ return n.toLowerCase(); } - public final int main(final String[] argv) throws Exception { + public final int main(String[] argv) throws Exception { final CmdLineParser clp = new CmdLineParser(OptionHandlers.empty(), this); try { clp.parseArgument(argv);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java index 5317edf..788f7ce 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchProgramModule.java
@@ -32,11 +32,11 @@ import com.google.gerrit.server.account.AccountVisibility; import com.google.gerrit.server.account.AccountVisibilityProvider; import com.google.gerrit.server.account.CapabilityCollection; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.FakeRealm; import com.google.gerrit.server.account.GroupCacheImpl; import com.google.gerrit.server.account.GroupIncludeCacheImpl; import com.google.gerrit.server.account.Realm; +import com.google.gerrit.server.account.externalids.ExternalIdModule; import com.google.gerrit.server.cache.CacheRemovalListener; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; import com.google.gerrit.server.change.ChangeJson; @@ -56,18 +56,19 @@ import com.google.gerrit.server.git.ReceiveCommitsExecutorModule; import com.google.gerrit.server.git.SearchingChangeCacheImpl; import com.google.gerrit.server.git.TagCache; +import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.group.GroupModule; import com.google.gerrit.server.mail.send.ReplacePatchSetSender; import com.google.gerrit.server.notedb.NoteDbModule; import com.google.gerrit.server.patch.DiffExecutorModule; import com.google.gerrit.server.patch.PatchListCacheImpl; -import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.CommentLinkProvider; +import com.google.gerrit.server.project.DefaultPermissionBackendModule; import com.google.gerrit.server.project.ProjectCacheImpl; -import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.project.SectionSortCache; import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.query.change.ChangeQueryProcessor; import com.google.gerrit.server.update.BatchUpdate; import com.google.inject.Inject; import com.google.inject.Module; @@ -112,6 +113,8 @@ bind(new TypeLiteral<List<CommentLinkInfo>>() {}) .toProvider(CommentLinkProvider.class) .in(SINGLETON); + bind(new TypeLiteral<DynamicMap<ChangeQueryProcessor.ChangeAttributeFactory>>() {}) + .toInstance(DynamicMap.<ChangeQueryProcessor.ChangeAttributeFactory>emptyMap()); bind(String.class) .annotatedWith(CanonicalWebUrl.class) .toProvider(CanonicalWebUrlProvider.class); @@ -127,6 +130,7 @@ factory(MergeUtil.Factory.class); factory(PatchSetInserter.Factory.class); factory(RebaseChangeOp.Factory.class); + factory(VisibleRefFilter.Factory.class); // As Reindex is a batch program, don't assume the index is available for // the change cache. @@ -141,16 +145,16 @@ bind(new TypeLiteral<Set<AccountGroup.UUID>>() {}) .annotatedWith(GitReceivePackGroups.class) .toInstance(Collections.<AccountGroup.UUID>emptySet()); - bind(ChangeControl.Factory.class); - factory(ProjectControl.AssistedFactory.class); install(new BatchGitModule()); + install(new DefaultPermissionBackendModule()); install(new DefaultCacheFactory.Module()); + install(new ExternalIdModule()); install(new GroupModule()); install(new NoteDbModule(cfg)); install(new PrologModule()); install(AccountByEmailCacheImpl.module()); - install(AccountCacheImpl.module(false)); + install(AccountCacheImpl.module()); install(GroupCacheImpl.module()); install(GroupIncludeCacheImpl.module()); install(ProjectCacheImpl.module()); @@ -159,7 +163,6 @@ install(MergeabilityCacheImpl.module()); install(TagCache.module()); factory(CapabilityCollection.Factory.class); - factory(CapabilityControl.Factory.class); factory(ChangeData.Factory.class); factory(ProjectState.Factory.class);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java index e5076c9..afb2fb4 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
@@ -49,8 +49,7 @@ root.addAppender(dst); } - public static LifecycleListener start(final Path sitePath, final Config config) - throws IOException { + public static LifecycleListener start(Path sitePath, Config config) throws IOException { Path logdir = FileUtil.mkdirsOrDie(new SitePaths(sitePath).logs_dir, "Cannot create log directory"); if (SystemLog.shouldConfigure()) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java index 7eed2ef..c9df7e7 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
@@ -23,7 +23,7 @@ private static final ShutdownCallback cb = new ShutdownCallback(); /** Add a task to be performed when graceful shutdown is requested. */ - public static void add(final Runnable task) { + public static void add(Runnable task) { if (!cb.add(task)) { // If the shutdown has already begun we cannot enqueue a new // task. Instead trigger the task in the caller, without any @@ -55,7 +55,7 @@ setName("ShutdownCallback"); } - boolean add(final Runnable newTask) { + boolean add(Runnable newTask) { synchronized (this) { if (!shutdownStarted && !shutdownComplete) { if (tasks.isEmpty()) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java index e7f5ffc..e786c35 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -82,7 +82,7 @@ this.sitePath = sitePath; } - protected SiteProgram(Path sitePath, final Provider<DataSource> dsProvider) { + protected SiteProgram(Path sitePath, Provider<DataSource> dsProvider) { this.sitePath = sitePath; this.dsProvider = dsProvider; } @@ -106,7 +106,7 @@ /** @return provides database connectivity and site path. */ protected Injector createDbInjector( - final boolean enableMetrics, final DataSourceProvider.Context context) { + final boolean enableMetrics, DataSourceProvider.Context context) { final Path sitePath = getSitePath(); final List<Module> modules = new ArrayList<>();
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh index 5952880..d331347 100755 --- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh +++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/gerrit.sh
@@ -45,6 +45,10 @@ # If set to "0" disables using start-stop-daemon. This may need to # be set on SuSE systems. +if test -f /lib/lsb/init-functions ; then + . /lib/lsb/init-functions +fi + usage() { me=`basename "$0"` echo >&2 "Usage: $me {start|stop|restart|check|status|run|supervise|threads} [-d site]"
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config index 26ac9d6..bc8a523 100644 --- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config +++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/init/libraries.config
@@ -13,15 +13,15 @@ # limitations under the License. [library "mysqlDriver"] - name = MySQL Connector/J 5.1.41 - url = https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.41/mysql-connector-java-5.1.41.jar - sha1 = b0878056f15616989144d6114d36d3942321d0d1 + name = MySQL Connector/J 5.1.42 + url = https://repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.42/mysql-connector-java-5.1.42.jar + sha1 = 80a448a3ec2178b649bb2e3cb3610fab06e11669 remove = mysql-connector-java-.*[.]jar [library "mariadbDriver"] - name = MariaDB Connector/J 1.5.9 - url = https://repo1.maven.org/maven2/org/mariadb/jdbc/mariadb-java-client/1.5.9/mariadb-java-client-1.5.9.jar - sha1 = 75d4d6e4cdb9a551a102e92a14c640768174e214 + name = MariaDB Connector/J 2.0.2 + url = https://repo1.maven.org/maven2/org/mariadb/jdbc/mariadb-java-client/2.0.2/mariadb-java-client-2.0.2.jar + sha1 = 36ab24223b0e915e6d81c98856d73c47628a31c1 remove = mariadb-java-client-.*[.]jar [library "oracleDriver"]
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/http/jetty/HttpLogRedactTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/http/jetty/HttpLogRedactTest.java new file mode 100644 index 0000000..7ed7f81 --- /dev/null +++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/http/jetty/HttpLogRedactTest.java
@@ -0,0 +1,46 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.http.jetty; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; + +public class HttpLogRedactTest { + @Test + public void includeQueryString() { + assertThat(HttpLog.redactQueryString("/changes/", null)).isEqualTo("/changes/"); + assertThat(HttpLog.redactQueryString("/changes/", "")).isEqualTo("/changes/"); + assertThat(HttpLog.redactQueryString("/changes/", "x")).isEqualTo("/changes/?x"); + assertThat(HttpLog.redactQueryString("/changes/", "x=y")).isEqualTo("/changes/?x=y"); + } + + @Test + public void redactAuth() { + assertThat(HttpLog.redactQueryString("/changes/", "query=status:open")) + .isEqualTo("/changes/?query=status:open"); + + assertThat(HttpLog.redactQueryString("/changes/", "query=status:open&access_token=foo")) + .isEqualTo("/changes/?query=status:open&access_token=*"); + + assertThat(HttpLog.redactQueryString("/changes/", "access_token=foo")) + .isEqualTo("/changes/?access_token=*"); + + assertThat( + HttpLog.redactQueryString( + "/changes/", "query=status:open&access_token=foo&access_token=bar")) + .isEqualTo("/changes/?query=status:open&access_token=*&access_token=*"); + } +}
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD index 2e768ee..cc01802 100644 --- a/gerrit-plugin-api/BUILD +++ b/gerrit-plugin-api/BUILD
@@ -19,7 +19,7 @@ "//gerrit-extension-api:api", "//gerrit-gwtexpui:server", "//gerrit-reviewdb:server", - "//gerrit-server/src/main/prolog:common", + "//gerrit-server:prolog-common", "//lib/commons:lang", "//lib/commons:lang3", "//lib/dropwizard:dropwizard-core", @@ -43,6 +43,7 @@ "//lib:args4j", "//lib:blame-cache", "//lib:guava", + "//lib:guava-retrying", "//lib:gson", "//lib:gwtorm", "//lib:icu4j",
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml index b6d1dc9..f9dc7e4 100644 --- a/gerrit-plugin-api/pom.xml +++ b/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>2.14.1</version> + <version>2.15-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Plugin API</name> <description>API for Gerrit Plugins</description>
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml index ba2dbf64..daabb46 100644 --- a/gerrit-plugin-gwtui/pom.xml +++ b/gerrit-plugin-gwtui/pom.xml
@@ -2,7 +2,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-plugin-gwtui</artifactId> - <version>2.14.1</version> + <version>2.15-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Plugin GWT UI</name> <description>Common Classes for Gerrit GWT UI Plugins</description>
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java index 7c478c1..bfcb3d6 100644 --- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java +++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/Plugin.java
@@ -121,12 +121,15 @@ * * @param extensionPoint the UI extension point for which the panel should be registered. * @param entry callback function invoked to create the panel widgets. + * @param name the name of the panel which can be used to specify panel ordering via project + * config */ - public void panel(GerritUiExtensionPoint extensionPoint, Panel.EntryPoint entry) { - panel(extensionPoint.name(), wrap(entry)); + public final void panel( + GerritUiExtensionPoint extensionPoint, Panel.EntryPoint entry, String name) { + panel(extensionPoint.name(), wrap(entry), name); } - private native void panel(String i, JavaScriptObject e) /*-{ this.panel(i, e) }-*/; + private native void panel(String i, JavaScriptObject e, String n) /*-{ this.panel(i, e, n) }-*/; protected Plugin() {}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java index 13e19ae..df5be2c 100644 --- a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java +++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/plugin/client/ui/GroupSuggestOracle.java
@@ -40,7 +40,7 @@ } @Override - public void requestSuggestions(final Request req, final Callback done) { + public void requestSuggestions(Request req, Callback done) { if (req.getQuery().length() < chars) { responseEmptySuggestion(req, done); return;
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java index 1b06f0f..61c807c 100644 --- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java +++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
@@ -24,8 +24,7 @@ private final int aSize; private final int bSize; - public EditList( - final List<Edit> edits, final int contextLines, final int aSize, final int bSize) { + public EditList(final List<Edit> edits, int contextLines, int aSize, int bSize) { this.edits = edits; this.context = contextLines; this.aSize = aSize; @@ -65,7 +64,7 @@ }; } - private int findCombinedEnd(final int i) { + private int findCombinedEnd(int i) { int end = i + 1; while (end < edits.size() && (combineA(end) || combineB(end))) { end++; @@ -73,14 +72,14 @@ return end - 1; } - private boolean combineA(final int i) { + private boolean combineA(int i) { final Edit s = edits.get(i); final Edit e = edits.get(i - 1); // + 1 to prevent '... skipping 1 common line ...' messages. return s.getBeginA() - e.getEndA() <= 2 * context + 1; } - private boolean combineB(final int i) { + private boolean combineB(int i) { final int s = edits.get(i).getBeginB(); final int e = edits.get(i - 1).getEndB(); // + 1 to prevent '... skipping 1 common line ...' messages. @@ -98,7 +97,7 @@ private final int aEnd; private final int bEnd; - private Hunk(final int ci, final int ei) { + private Hunk(int ci, int ei) { curIdx = ci; endIdx = ei; curEdit = edits.get(curIdx); @@ -172,7 +171,7 @@ return aCur < aEnd || bCur < bEnd; } - private boolean in(final Edit edit) { + private boolean in(Edit edit) { return aCur < edit.getEndA() || bCur < edit.getEndB(); } }
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java index 1dce0a0..348f9b2 100644 --- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java +++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
@@ -16,13 +16,10 @@ import java.util.ArrayList; import java.util.List; -import org.eclipse.jgit.diff.Edit; public class SparseFileContent { - protected String path; protected List<Range> ranges; protected int size; - protected boolean missingNewlineAtEnd; private transient int currentRangeIdx; @@ -34,40 +31,11 @@ return size; } - public void setSize(final int s) { + public void setSize(int s) { size = s; } - public boolean isMissingNewlineAtEnd() { - return missingNewlineAtEnd; - } - - public void setMissingNewlineAtEnd(final boolean missing) { - missingNewlineAtEnd = missing; - } - - public String getPath() { - return path; - } - - public void setPath(String filePath) { - path = filePath; - } - - public boolean isWholeFile() { - if (size == 0) { - return true; - - } else if (1 == ranges.size()) { - Range r = ranges.get(0); - return r.base == 0 && r.end() == size; - - } else { - return false; - } - } - - public String get(final int idx) { + public String get(int idx) { final String line = getLine(idx); if (line == null) { throw new ArrayIndexOutOfBoundsException(idx); @@ -75,7 +43,7 @@ return line; } - public boolean contains(final int idx) { + public boolean contains(int idx) { return getLine(idx) != null; } @@ -83,7 +51,7 @@ return ranges.isEmpty() ? size() : ranges.get(0).base; } - public int next(final int idx) { + public int next(int idx) { // Most requests are sequential in nature, fetching the next // line from the current range, or the immediate next range. // @@ -138,18 +106,7 @@ return size(); } - public int mapIndexToLine(int arrayIndex) { - final int origIndex = arrayIndex; - for (Range r : ranges) { - if (arrayIndex < r.lines.size()) { - return r.base + arrayIndex; - } - arrayIndex -= r.lines.size(); - } - throw new ArrayIndexOutOfBoundsException(origIndex); - } - - private String getLine(final int idx) { + private String getLine(int idx) { // Most requests are sequential in nature, fetching the next // line from the current range, or the next range. // @@ -191,7 +148,7 @@ return null; } - public void addLine(final int i, final String content) { + public void addLine(int i, String content) { final Range r; if (!ranges.isEmpty() && i == last().end()) { r = last(); @@ -206,58 +163,6 @@ return ranges.get(ranges.size() - 1); } - public String asString() { - final StringBuilder b = new StringBuilder(); - for (Range r : ranges) { - for (String l : r.lines) { - b.append(l); - b.append('\n'); - } - } - if (0 < b.length() && isMissingNewlineAtEnd()) { - b.setLength(b.length() - 1); - } - return b.toString(); - } - - public SparseFileContent apply(SparseFileContent a, List<Edit> edits) { - EditList list = new EditList(edits, size, a.size(), size); - ArrayList<String> lines = new ArrayList<>(size); - for (final EditList.Hunk hunk : list.getHunks()) { - while (hunk.next()) { - if (hunk.isContextLine()) { - if (contains(hunk.getCurB())) { - lines.add(get(hunk.getCurB())); - } else { - lines.add(a.get(hunk.getCurA())); - } - hunk.incBoth(); - continue; - } - - if (hunk.isDeletedA()) { - hunk.incA(); - } - - if (hunk.isInsertedB()) { - lines.add(get(hunk.getCurB())); - hunk.incB(); - } - } - } - - Range range = new Range(); - range.lines = lines; - - SparseFileContent r = new SparseFileContent(); - r.setSize(lines.size()); - r.setMissingNewlineAtEnd(isMissingNewlineAtEnd()); - r.setPath(getPath()); - r.ranges.add(range); - - return r; - } - @Override public String toString() { final StringBuilder b = new StringBuilder(); @@ -275,14 +180,14 @@ protected int base; protected List<String> lines; - private Range(final int b) { + private Range(int b) { base = b; lines = new ArrayList<>(); } protected Range() {} - private String get(final int i) { + private String get(int i) { return lines.get(i - base); } @@ -290,7 +195,7 @@ return base + lines.size(); } - private boolean contains(final int i) { + private boolean contains(int i) { return base <= i && i < end(); }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java deleted file mode 100644 index 3c8f2fa..0000000 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java +++ /dev/null
@@ -1,186 +0,0 @@ -// Copyright (C) 2008 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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.reviewdb.client; - -import com.google.gerrit.extensions.client.AuthType; -import com.google.gwtorm.client.Column; -import com.google.gwtorm.client.StringKey; -import java.util.Objects; - -/** Association of an external account identifier to a local {@link Account}. */ -public final class AccountExternalId { - /** - * Scheme used for {@link AuthType#LDAP}, {@link AuthType#CLIENT_SSL_CERT_LDAP}, {@link - * AuthType#HTTP_LDAP}, and {@link AuthType#LDAP_BIND} usernames. - * - * <p>The name {@code gerrit:} was a very poor choice. - */ - public static final String SCHEME_GERRIT = "gerrit:"; - - /** Scheme used for randomly created identities constructed by a UUID. */ - public static final String SCHEME_UUID = "uuid:"; - - /** Scheme used to represent only an email address. */ - public static final String SCHEME_MAILTO = "mailto:"; - - /** Scheme for the username used to authenticate an account, e.g. over SSH. */ - public static final String SCHEME_USERNAME = "username:"; - - /** Scheme used for GPG public keys. */ - public static final String SCHEME_GPGKEY = "gpgkey:"; - - /** Scheme for external auth used during authentication, e.g. OAuth Token */ - public static final String SCHEME_EXTERNAL = "external:"; - - public static class Key extends StringKey<com.google.gwtorm.client.Key<?>> { - private static final long serialVersionUID = 1L; - - @Column(id = 1) - protected String externalId; - - protected Key() {} - - public Key(String scheme, final String identity) { - if (!scheme.endsWith(":")) { - scheme += ":"; - } - externalId = scheme + identity; - } - - public Key(final String e) { - externalId = e; - } - - @Override - public String get() { - return externalId; - } - - @Override - protected void set(String newValue) { - externalId = newValue; - } - - public String getScheme() { - int c = externalId.indexOf(':'); - return 0 < c ? externalId.substring(0, c) : null; - } - } - - @Column(id = 1, name = Column.NONE) - protected Key key; - - @Column(id = 2) - protected Account.Id accountId; - - @Column(id = 3, notNull = false) - protected String emailAddress; - - // Encoded version of the hashed and salted password, to be interpreted by the - // {@link HashedPassword} class. - @Column(id = 4, notNull = false) - protected String password; - - /** <i>computed value</i> is this identity trusted by the site administrator? */ - protected boolean trusted; - - /** <i>computed value</i> can this identity be removed from the account? */ - protected boolean canDelete; - - protected AccountExternalId() {} - - /** - * Create a new binding to an external identity. - * - * @param who the account this binds to. - * @param k the binding key. - */ - public AccountExternalId(final Account.Id who, final AccountExternalId.Key k) { - accountId = who; - key = k; - } - - public AccountExternalId.Key getKey() { - return key; - } - - /** Get local id of this account, to link with in other entities */ - public Account.Id getAccountId() { - return accountId; - } - - public String getExternalId() { - return key.externalId; - } - - public String getEmailAddress() { - return emailAddress; - } - - public void setEmailAddress(final String e) { - emailAddress = e; - } - - public boolean isScheme(final String scheme) { - final String id = getExternalId(); - return id != null && id.startsWith(scheme); - } - - public String getSchemeRest() { - String scheme = key.getScheme(); - return null != scheme ? getExternalId().substring(scheme.length() + 1) : null; - } - - public void setPassword(String hashed) { - password = hashed; - } - - public String getPassword() { - return password; - } - - public boolean isTrusted() { - return trusted; - } - - public void setTrusted(final boolean t) { - trusted = t; - } - - public boolean canDelete() { - return canDelete; - } - - public void setCanDelete(final boolean t) { - canDelete = t; - } - - @Override - public boolean equals(Object o) { - if (o instanceof AccountExternalId) { - AccountExternalId extId = (AccountExternalId) o; - return Objects.equals(key, extId.key) - && Objects.equals(accountId, extId.accountId) - && Objects.equals(emailAddress, extId.emailAddress) - && Objects.equals(password, extId.password); - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(key, accountId, emailAddress, password); - } -}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java index c3b2908..74dadc5 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
@@ -17,9 +17,22 @@ import com.google.gwtorm.client.Column; import com.google.gwtorm.client.IntKey; import com.google.gwtorm.client.StringKey; +import java.sql.Timestamp; /** Named group of one or more accounts, typically used for access controls. */ public final class AccountGroup { + /** + * Time when the audit subsystem was implemented, used as the default value for {@link #createdOn} + * when one couldn't be determined from the audit log. + */ + // Can't use Instant here because GWT. This is verified against a readable time in the tests, + // which don't need to compile under GWT. + private static final long AUDIT_CREATION_INSTANT_MS = 1244489460000L; + + public static Timestamp auditCreationInstantTs() { + return new Timestamp(AUDIT_CREATION_INSTANT_MS); + } + /** Group name key */ public static class NameKey extends StringKey<com.google.gwtorm.client.Key<?>> { private static final long serialVersionUID = 1L; @@ -29,7 +42,7 @@ protected NameKey() {} - public NameKey(final String n) { + public NameKey(String n) { name = n; } @@ -53,7 +66,7 @@ protected UUID() {} - public UUID(final String n) { + public UUID(String n) { uuid = n; } @@ -68,7 +81,7 @@ } /** Parse an AccountGroup.UUID out of a string representation. */ - public static UUID parse(final String str) { + public static UUID parse(String str) { final UUID r = new UUID(); r.fromString(str); return r; @@ -89,7 +102,7 @@ protected Id() {} - public Id(final int id) { + public Id(int id) { this.id = id; } @@ -104,7 +117,7 @@ } /** Parse an AccountGroup.Id out of a string representation. */ - public static Id parse(final String str) { + public static Id parse(String str) { final Id r = new Id(); r.fromString(str); return r; @@ -145,17 +158,22 @@ @Column(id = 10) protected UUID ownerGroupUUID; + @Column(id = 11, notNull = false) + protected Timestamp createdOn; + protected AccountGroup() {} public AccountGroup( - final AccountGroup.NameKey newName, - final AccountGroup.Id newId, - final AccountGroup.UUID uuid) { + AccountGroup.NameKey newName, + AccountGroup.Id newId, + AccountGroup.UUID uuid, + Timestamp createdOn) { name = newName; groupId = newId; visibleToAll = false; groupUUID = uuid; ownerGroupUUID = groupUUID; + this.createdOn = createdOn; } public AccountGroup.Id getId() { @@ -170,7 +188,7 @@ return name; } - public void setNameKey(final AccountGroup.NameKey nameKey) { + public void setNameKey(AccountGroup.NameKey nameKey) { name = nameKey; } @@ -178,7 +196,7 @@ return description; } - public void setDescription(final String d) { + public void setDescription(String d) { description = d; } @@ -186,11 +204,11 @@ return ownerGroupUUID; } - public void setOwnerGroupUUID(final AccountGroup.UUID uuid) { + public void setOwnerGroupUUID(AccountGroup.UUID uuid) { ownerGroupUUID = uuid; } - public void setVisibleToAll(final boolean visibleToAll) { + public void setVisibleToAll(boolean visibleToAll) { this.visibleToAll = visibleToAll; } @@ -205,4 +223,12 @@ public void setGroupUUID(AccountGroup.UUID uuid) { groupUUID = uuid; } + + public Timestamp getCreatedOn() { + return createdOn != null ? createdOn : auditCreationInstantTs(); + } + + public void setCreatedOn(Timestamp createdOn) { + this.createdOn = createdOn; + } }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupById.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupById.java index b4bf783..99ff35be 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupById.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupById.java
@@ -33,7 +33,7 @@ includeUUID = new AccountGroup.UUID(); } - public Key(final AccountGroup.Id g, final AccountGroup.UUID u) { + public Key(AccountGroup.Id g, AccountGroup.UUID u) { groupId = g; includeUUID = u; } @@ -62,7 +62,7 @@ protected AccountGroupById() {} - public AccountGroupById(final AccountGroupById.Key k) { + public AccountGroupById(AccountGroupById.Key k) { key = k; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java index d1e72af..a127a70 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupByIdAud.java
@@ -37,7 +37,7 @@ includeUUID = new AccountGroup.UUID(); } - public Key(final AccountGroup.Id g, final AccountGroup.UUID u, final Timestamp t) { + public Key(AccountGroup.Id g, AccountGroup.UUID u, Timestamp t) { groupId = g; includeUUID = u; addedOn = t; @@ -76,8 +76,7 @@ protected AccountGroupByIdAud() {} - public AccountGroupByIdAud( - final AccountGroupById m, final Account.Id adder, final Timestamp when) { + public AccountGroupByIdAud(final AccountGroupById m, Account.Id adder, Timestamp when) { final AccountGroup.Id group = m.getGroupId(); final AccountGroup.UUID include = m.getIncludeUUID(); key = new AccountGroupByIdAud.Key(group, include, when); @@ -92,7 +91,7 @@ return removedOn == null; } - public void removed(final Account.Id deleter, final Timestamp when) { + public void removed(Account.Id deleter, Timestamp when) { removedBy = deleter; removedOn = when; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java index ce6999f..ce5b347 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
@@ -33,7 +33,7 @@ groupId = new AccountGroup.Id(); } - public Key(final Account.Id a, final AccountGroup.Id g) { + public Key(Account.Id a, AccountGroup.Id g) { accountId = a; groupId = g; } @@ -58,7 +58,7 @@ protected AccountGroupMember() {} - public AccountGroupMember(final AccountGroupMember.Key k) { + public AccountGroupMember(AccountGroupMember.Key k) { key = k; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java index 4f3992d..da19351 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
@@ -37,7 +37,7 @@ groupId = new AccountGroup.Id(); } - public Key(final Account.Id a, final AccountGroup.Id g, final Timestamp t) { + public Key(Account.Id a, AccountGroup.Id g, Timestamp t) { accountId = a; groupId = g; addedOn = t; @@ -76,8 +76,7 @@ protected AccountGroupMemberAudit() {} - public AccountGroupMemberAudit( - final AccountGroupMember m, final Account.Id adder, Timestamp addedOn) { + public AccountGroupMemberAudit(final AccountGroupMember m, Account.Id adder, Timestamp addedOn) { final Account.Id who = m.getAccountId(); final AccountGroup.Id group = m.getAccountGroupId(); key = new AccountGroupMemberAudit.Key(who, group, addedOn); @@ -92,7 +91,7 @@ return removedOn == null; } - public void removed(final Account.Id deleter, final Timestamp when) { + public void removed(Account.Id deleter, Timestamp when) { removedBy = deleter; removedOn = when; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java index 3645dac..372d644 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java
@@ -30,7 +30,7 @@ accountId = new Account.Id(); } - public Id(final Account.Id a, final int s) { + public Id(Account.Id a, int s) { accountId = a; seq = s; } @@ -63,7 +63,7 @@ protected AccountSshKey() {} - public AccountSshKey(final AccountSshKey.Id i, final String pub) { + public AccountSshKey(AccountSshKey.Id i, String pub) { id = i; sshPublicKey = pub.replace("\n", "").replace("\r", ""); valid = id.isValid();
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java index d0df7c6..fd8bbfd 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java
@@ -33,12 +33,12 @@ projectName = new Project.NameKey(); } - public NameKey(final Project.NameKey proj, final String branchName) { + public NameKey(Project.NameKey proj, String branchName) { projectName = proj; set(branchName); } - public NameKey(String proj, final String branchName) { + public NameKey(String proj, String branchName) { this(new Project.NameKey(proj), branchName); } @@ -68,7 +68,7 @@ protected Branch() {} - public Branch(final Branch.NameKey newName) { + public Branch(Branch.NameKey newName) { name = newName; } @@ -88,7 +88,7 @@ return revision; } - public void setRevision(final RevId id) { + public void setRevision(RevId id) { revision = id; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java index 9655edd..1a08d17 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -102,7 +102,7 @@ protected Id() {} - public Id(final int id) { + public Id(int id) { this.id = id; } @@ -130,7 +130,7 @@ } /** Parse a Change.Id out of a string representation. */ - public static Id parse(final String str) { + public static Id parse(String str) { final Id r = new Id(); r.fromString(str); return r; @@ -262,7 +262,7 @@ protected Key() {} - public Key(final String id) { + public Key(String id) { this.id = id; } @@ -291,7 +291,7 @@ } /** Parse a Change.Key out of a string representation. */ - public static Key parse(final String str) { + public static Key parse(String str) { final Key r = new Key(); r.fromString(str); return r; @@ -416,8 +416,8 @@ return changeStatus; } - public static Status forCode(final char c) { - for (final Status s : Status.values()) { + public static Status forCode(char c) { + for (Status s : Status.values()) { if (s.code == c) { return s; } @@ -512,6 +512,18 @@ @Column(id = 19, notNull = false) protected Account.Id assignee; + /** Whether the change is private. */ + @Column(id = 20) + protected boolean isPrivate; + + /** Whether the change is work in progress. */ + @Column(id = 21) + protected boolean workInProgress; + + /** Whether the change has started review. */ + @Column(id = 22) + protected boolean reviewStarted; + /** @see com.google.gerrit.server.notedb.NoteDbChangeState */ @Column(id = 101, notNull = false, length = Integer.MAX_VALUE) protected String noteDbState; @@ -548,6 +560,9 @@ originalSubject = other.originalSubject; submissionId = other.submissionId; topic = other.topic; + isPrivate = other.isPrivate; + workInProgress = other.workInProgress; + reviewStarted = other.reviewStarted; noteDbState = other.noteDbState; } @@ -566,7 +581,7 @@ return changeKey; } - public void setKey(final Change.Key k) { + public void setKey(Change.Key k) { changeKey = k; } @@ -638,7 +653,7 @@ return null; } - public void setCurrentPatchSet(final PatchSetInfo ps) { + public void setCurrentPatchSet(PatchSetInfo ps) { if (originalSubject == null && subject != null) { // Change was created before schema upgrade. Use the last subject // associated with this change, as the most recent discussion will @@ -694,6 +709,30 @@ this.topic = topic; } + public boolean isPrivate() { + return isPrivate; + } + + public void setPrivate(boolean isPrivate) { + this.isPrivate = isPrivate; + } + + public boolean isWorkInProgress() { + return workInProgress; + } + + public void setWorkInProgress(boolean workInProgress) { + this.workInProgress = workInProgress; + } + + public boolean hasReviewStarted() { + return reviewStarted; + } + + public void setReviewStarted(boolean reviewStarted) { + this.reviewStarted = reviewStarted; + } + public String getNoteDbState() { return noteDbState; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java index caf20c7..edc022f 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
@@ -34,7 +34,7 @@ changeId = new Change.Id(); } - public Key(final Change.Id change, final String uuid) { + public Key(Change.Id change, String uuid) { this.changeId = change; this.uuid = uuid; } @@ -84,8 +84,7 @@ protected ChangeMessage() {} - public ChangeMessage( - final ChangeMessage.Key k, final Account.Id a, final Timestamp wo, final PatchSet.Id psid) { + public ChangeMessage(final ChangeMessage.Key k, Account.Id a, Timestamp wo, PatchSet.Id psid) { key = k; author = a; writtenOn = wo; @@ -101,7 +100,7 @@ return author; } - public void setAuthor(final Account.Id accountId) { + public void setAuthor(Account.Id accountId) { if (author != null) { throw new IllegalStateException("Cannot modify author once assigned"); } @@ -129,7 +128,7 @@ return message; } - public void setMessage(final String s) { + public void setMessage(String s) { message = s; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Comment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Comment.java index cadd52c..4b3c652 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Comment.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Comment.java
@@ -15,6 +15,7 @@ package com.google.gerrit.reviewdb.client; import java.sql.Timestamp; +import java.util.Comparator; import java.util.Objects; /** @@ -130,7 +131,13 @@ } } - public static class Range { + public static class Range implements Comparable<Range> { + private static final Comparator<Range> RANGE_COMPARATOR = + Comparator.<Range>comparingInt(range -> range.startLine) + .thenComparingInt(range -> range.startChar) + .thenComparingInt(range -> range.endLine) + .thenComparingInt(range -> range.endChar); + public int startLine; // 1-based, inclusive public int startChar; // 0-based, inclusive public int endLine; // 1-based, exclusive @@ -186,6 +193,11 @@ .append('}') .toString(); } + + @Override + public int compareTo(Range otherRange) { + return RANGE_COMPARATOR.compare(this, otherRange); + } } public Key key;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CurrentSchemaVersion.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CurrentSchemaVersion.java index 9d61186..6a3b69c 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CurrentSchemaVersion.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CurrentSchemaVersion.java
@@ -35,7 +35,7 @@ } @Override - protected void set(final String newValue) { + protected void set(String newValue) { assert get().equals(newValue); } }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/LabelId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/LabelId.java index c38078e..e69cab2 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/LabelId.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/LabelId.java
@@ -31,7 +31,7 @@ public LabelId() {} - public LabelId(final String n) { + public LabelId(String n) { id = n; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java index 269b6d4..0492c6c 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
@@ -49,7 +49,7 @@ patchSetId = new PatchSet.Id(); } - public Key(final PatchSet.Id ps, final String name) { + public Key(PatchSet.Id ps, String name) { this.patchSetId = ps; this.fileName = name; } @@ -70,7 +70,7 @@ } /** Parse a Patch.Id out of a string representation. */ - public static Key parse(final String str) { + public static Key parse(String str) { final Key r = new Key(); r.fromString(str); return r; @@ -103,7 +103,7 @@ private final char code; - ChangeType(final char c) { + ChangeType(char c) { code = c; } @@ -116,8 +116,8 @@ return s != null && s.length() == 1 && s.charAt(0) == code; } - public static ChangeType forCode(final char c) { - for (final ChangeType s : ChangeType.values()) { + public static ChangeType forCode(char c) { + for (ChangeType s : ChangeType.values()) { if (s.code == c) { return s; } @@ -156,7 +156,7 @@ private final char code; - PatchType(final char c) { + PatchType(char c) { code = c; } @@ -165,8 +165,8 @@ return code; } - public static PatchType forCode(final char c) { - for (final PatchType s : PatchType.values()) { + public static PatchType forCode(char c) { + for (PatchType s : PatchType.values()) { if (s.code == c) { return s; } @@ -203,7 +203,7 @@ protected Patch() {} - public Patch(final Patch.Key newId) { + public Patch(Patch.Key newId) { key = newId; setChangeType(ChangeType.MODIFIED); setPatchType(PatchType.UNIFIED); @@ -217,7 +217,7 @@ return nbrComments; } - public void setCommentCount(final int n) { + public void setCommentCount(int n) { nbrComments = n; } @@ -225,7 +225,7 @@ return nbrDrafts; } - public void setDraftCount(final int n) { + public void setDraftCount(int n) { nbrDrafts = n; } @@ -249,7 +249,7 @@ return ChangeType.forCode(changeType); } - public void setChangeType(final ChangeType type) { + public void setChangeType(ChangeType type) { changeType = type.getCode(); } @@ -257,7 +257,7 @@ return PatchType.forCode(patchType); } - public void setPatchType(final PatchType type) { + public void setPatchType(PatchType type) { patchType = type.getCode(); } @@ -269,7 +269,7 @@ return sourceFileName; } - public void setSourceFileName(final String n) { + public void setSourceFileName(String n) { sourceFileName = n; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java index 90552b8..de953dc 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -48,7 +48,7 @@ patchKey = new Patch.Key(); } - public Key(final Patch.Key p, final String uuid) { + public Key(Patch.Key p, String uuid) { this.patchKey = p; this.uuid = uuid; } @@ -84,7 +84,7 @@ private final char code; - Status(final char c) { + Status(char c) { code = c; } @@ -92,8 +92,8 @@ return code; } - public static Status forCode(final char c) { - for (final Status s : Status.values()) { + public static Status forCode(char c) { + for (Status s : Status.values()) { if (s.code == c) { return s; } @@ -247,7 +247,7 @@ return Status.forCode(status); } - public void setStatus(final Status s) { + public void setStatus(Status s) { status = s.getCode(); } @@ -255,7 +255,7 @@ return side; } - public void setSide(final short s) { + public void setSide(short s) { side = s; } @@ -263,7 +263,7 @@ return message; } - public void setMessage(final String s) { + public void setMessage(String s) { message = s; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java index 138da5a..0cc76ed 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -86,7 +86,7 @@ changeId = new Change.Id(); } - public Id(final Change.Id change, final int id) { + public Id(Change.Id change, int id) { this.changeId = change; this.patchSetId = id; } @@ -111,7 +111,7 @@ } /** Parse a PatchSet.Id out of a string representation. */ - public static Id parse(final String str) { + public static Id parse(String str) { final Id r = new Id(); r.fromString(str); return r; @@ -183,7 +183,7 @@ @Column(id = 6, notNull = false, length = Integer.MAX_VALUE) protected String groups; - //DELETED id = 7 (pushCertficate) + // DELETED id = 7 (pushCertficate) /** Certificate sent with a push that created this patch set. */ @Column(id = 8, notNull = false, length = Integer.MAX_VALUE) @@ -200,7 +200,7 @@ protected PatchSet() {} - public PatchSet(final PatchSet.Id k) { + public PatchSet(PatchSet.Id k) { id = k; } @@ -227,7 +227,7 @@ return revision; } - public void setRevision(final RevId i) { + public void setRevision(RevId i) { revision = i; } @@ -235,7 +235,7 @@ return uploader; } - public void setUploader(final Account.Id who) { + public void setUploader(Account.Id who) { uploader = who; } @@ -243,7 +243,7 @@ return createdOn; } - public void setCreatedOn(final Timestamp ts) { + public void setCreatedOn(Timestamp ts) { createdOn = ts; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java index ef2732b..0f3e4e1 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
@@ -40,7 +40,7 @@ categoryId = new LabelId(); } - public Key(final PatchSet.Id ps, final Account.Id a, final LabelId c) { + public Key(PatchSet.Id ps, Account.Id a, LabelId c) { this.patchSetId = ps; this.accountId = a; this.categoryId = c; @@ -111,7 +111,7 @@ setGranted(ts); } - public PatchSetApproval(final PatchSet.Id psId, final PatchSetApproval src) { + public PatchSetApproval(PatchSet.Id psId, PatchSetApproval src) { key = new PatchSetApproval.Key(psId, src.getAccountId(), src.getLabelId()); value = src.getValue(); granted = src.granted; @@ -153,7 +153,7 @@ return value; } - public void setValue(final short v) { + public void setValue(short v) { value = v; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java index 4970db1..f949013 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java
@@ -22,7 +22,7 @@ public RevId id; public String shortMessage; - public ParentInfo(final RevId id, final String shortMessage) { + public ParentInfo(RevId id, String shortMessage) { this.id = id; this.shortMessage = shortMessage; } @@ -55,7 +55,7 @@ protected PatchSetInfo() {} - public PatchSetInfo(final PatchSet.Id k) { + public PatchSetInfo(PatchSet.Id k) { key = k; } @@ -67,7 +67,7 @@ return subject; } - public void setSubject(final String s) { + public void setSubject(String s) { if (s != null && s.length() > 255) { subject = s.substring(0, 255); } else { @@ -79,7 +79,7 @@ return message; } - public void setMessage(final String m) { + public void setMessage(String m) { message = m; } @@ -87,7 +87,7 @@ return author; } - public void setAuthor(final UserIdentity u) { + public void setAuthor(UserIdentity u) { author = u; } @@ -95,11 +95,11 @@ return committer; } - public void setCommitter(final UserIdentity u) { + public void setCommitter(UserIdentity u) { committer = u; } - public void setParents(final List<ParentInfo> p) { + public void setParents(List<ParentInfo> p) { parents = p; } @@ -107,7 +107,7 @@ return parents; } - public void setRevId(final String s) { + public void setRevId(String s) { revId = s; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java index ba83c58..4cc868e 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -31,7 +31,7 @@ protected NameKey() {} - public NameKey(final String n) { + public NameKey(String n) { name = n; } @@ -59,7 +59,7 @@ } /** Parse a Project.NameKey out of a string representation. */ - public static NameKey parse(final String str) { + public static NameKey parse(String str) { final NameKey r = new NameKey(); r.fromString(str); return r; @@ -99,6 +99,10 @@ protected InheritableBoolean rejectImplicitMerges; + protected InheritableBoolean enableReviewerByEmail; + + protected InheritableBoolean matchAuthorToCommitterDate; + protected Project() {} public Project(Project.NameKey nameKey) { @@ -112,6 +116,8 @@ createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT; enableSignedPush = InheritableBoolean.INHERIT; requireSignedPush = InheritableBoolean.INHERIT; + enableReviewerByEmail = InheritableBoolean.INHERIT; + matchAuthorToCommitterDate = InheritableBoolean.INHERIT; } public Project.NameKey getNameKey() { @@ -126,7 +132,7 @@ return description; } - public void setDescription(final String d) { + public void setDescription(String d) { description = d; } @@ -154,19 +160,35 @@ return rejectImplicitMerges; } - public void setUseContributorAgreements(final InheritableBoolean u) { + public InheritableBoolean getEnableReviewerByEmail() { + return enableReviewerByEmail; + } + + public void setEnableReviewerByEmail(InheritableBoolean enable) { + enableReviewerByEmail = enable; + } + + public InheritableBoolean getMatchAuthorToCommitterDate() { + return matchAuthorToCommitterDate; + } + + public void setMatchAuthorToCommitterDate(InheritableBoolean match) { + matchAuthorToCommitterDate = match; + } + + public void setUseContributorAgreements(InheritableBoolean u) { useContributorAgreements = u; } - public void setUseSignedOffBy(final InheritableBoolean sbo) { + public void setUseSignedOffBy(InheritableBoolean sbo) { useSignedOffBy = sbo; } - public void setUseContentMerge(final InheritableBoolean cm) { + public void setUseContentMerge(InheritableBoolean cm) { useContentMerge = cm; } - public void setRequireChangeID(final InheritableBoolean cid) { + public void setRequireChangeID(InheritableBoolean cid) { requireChangeID = cid; } @@ -194,7 +216,7 @@ requireSignedPush = require; } - public void setMaxObjectSizeLimit(final String limit) { + public void setMaxObjectSizeLimit(String limit) { maxObjectSizeLimit = limit; } @@ -206,7 +228,7 @@ return submitType; } - public void setSubmitType(final SubmitType type) { + public void setSubmitType(SubmitType type) { submitType = type; } @@ -214,7 +236,7 @@ return state; } - public void setState(final ProjectState newState) { + public void setState(ProjectState newState) { state = newState; } @@ -222,7 +244,7 @@ return defaultDashboardId; } - public void setDefaultDashboard(final String defaultDashboardId) { + public void setDefaultDashboard(String defaultDashboardId) { this.defaultDashboardId = defaultDashboardId; } @@ -230,7 +252,7 @@ return localDefaultDashboardId; } - public void setLocalDefaultDashboard(final String localDefaultDashboardId) { + public void setLocalDefaultDashboard(String localDefaultDashboardId) { this.localDefaultDashboardId = localDefaultDashboardId; } @@ -238,11 +260,11 @@ return themeName; } - public void setThemeName(final String themeName) { + public void setThemeName(String themeName) { this.themeName = themeName; } - public void copySettingsFrom(final Project update) { + public void copySettingsFrom(Project update) { description = update.description; useContributorAgreements = update.useContributorAgreements; useSignedOffBy = update.useSignedOffBy; @@ -271,7 +293,7 @@ * @param allProjectsName name key of the wild project * @return name key of the parent project, {@code null} if this project is the wild project */ - public Project.NameKey getParent(final Project.NameKey allProjectsName) { + public Project.NameKey getParent(Project.NameKey allProjectsName) { if (parent != null) { return parent; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java index b892e3d..8407bc6 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RefNames.java
@@ -105,6 +105,17 @@ return r.toString(); } + public static boolean isNoteDbMetaRef(String ref) { + if (ref.startsWith(REFS_CHANGES) + && (ref.endsWith(META_SUFFIX) || ref.endsWith(ROBOT_COMMENTS_SUFFIX))) { + return true; + } + if (ref.startsWith(REFS_DRAFT_COMMENTS) || ref.startsWith(REFS_STARRED_CHANGES)) { + return true; + } + return false; + } + public static String refsUsers(Account.Id accountId) { StringBuilder r = new StringBuilder(); r.append(REFS_USERS);
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java index d04f8e6..d2a3bd6 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
@@ -25,7 +25,7 @@ protected RevId() {} - public RevId(final String str) { + public RevId(String str) { id = str; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/SystemConfig.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/SystemConfig.java index 9abc744..cd42dd1 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/SystemConfig.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/SystemConfig.java
@@ -35,7 +35,7 @@ } @Override - protected void set(final String newValue) { + protected void set(String newValue) { assert get().equals(newValue); } }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java index 8cc9737..2f6008f 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java
@@ -32,7 +32,7 @@ protected Id() {} - public Id(final String id) { + public Id(String id) { this.id = id; } @@ -56,7 +56,7 @@ protected System() {} - public System(final String s) { + public System(String s) { this.system = s; } @@ -89,7 +89,7 @@ trackingSystem = new System(); } - protected Key(final Change.Id ch, final Id id, final System s) { + protected Key(Change.Id ch, Id id, System s) { changeId = ch; trackingKey = id; trackingSystem = s; @@ -119,11 +119,11 @@ protected TrackingId() {} - public TrackingId(final Change.Id ch, final TrackingId.Id id, final TrackingId.System s) { + public TrackingId(Change.Id ch, TrackingId.Id id, TrackingId.System s) { key = new Key(ch, id, s); } - public TrackingId(final Change.Id ch, final String id, final String s) { + public TrackingId(Change.Id ch, String id, String s) { key = new Key(ch, new TrackingId.Id(id), new TrackingId.System(s)); } @@ -149,7 +149,7 @@ } @Override - public boolean equals(final Object obj) { + public boolean equals(Object obj) { if (obj instanceof TrackingId) { final TrackingId tr = (TrackingId) obj; return key.equals(tr.key);
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/UserIdentity.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/UserIdentity.java index ddc1297..0b7aee3 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/UserIdentity.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/UserIdentity.java
@@ -39,7 +39,7 @@ return name; } - public void setName(final String n) { + public void setName(String n) { name = n; } @@ -47,7 +47,7 @@ return email; } - public void setEmail(final String e) { + public void setEmail(String e) { email = e; } @@ -59,7 +59,7 @@ return when; } - public void setDate(final Timestamp d) { + public void setDate(Timestamp d) { when = d; } @@ -67,7 +67,7 @@ return tz; } - public void setTimeZone(final int offset) { + public void setTimeZone(int offset) { tz = offset; } @@ -75,7 +75,7 @@ return accountId; } - public void setAccount(final Account.Id id) { + public void setAccount(Account.Id id) { accountId = id; } }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java index b015af8..db74caa 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
@@ -31,22 +31,6 @@ @Query("WHERE preferredEmail = ? LIMIT 2") ResultSet<Account> byPreferredEmail(String email) throws OrmException; - @Query("WHERE fullName = ? LIMIT 2") - ResultSet<Account> byFullName(String name) throws OrmException; - - @Query("WHERE fullName >= ? AND fullName <= ? ORDER BY fullName LIMIT ?") - ResultSet<Account> suggestByFullName(String nameA, String nameB, int limit) throws OrmException; - - @Query("WHERE preferredEmail >= ? AND preferredEmail <= ? ORDER BY preferredEmail LIMIT ?") - ResultSet<Account> suggestByPreferredEmail(String nameA, String nameB, int limit) - throws OrmException; - - @Query("LIMIT 1") - ResultSet<Account> anyAccounts() throws OrmException; - - @Query("ORDER BY accountId LIMIT ?") - ResultSet<Account> firstNById(int n) throws OrmException; - @Query("ORDER BY accountId") ResultSet<Account> all() throws OrmException; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java deleted file mode 100644 index 9124301..0000000 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java +++ /dev/null
@@ -1,35 +0,0 @@ -// Copyright (C) 2008 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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.reviewdb.server; - -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AccountExternalId; -import com.google.gwtorm.server.Access; -import com.google.gwtorm.server.OrmException; -import com.google.gwtorm.server.PrimaryKey; -import com.google.gwtorm.server.Query; -import com.google.gwtorm.server.ResultSet; - -public interface AccountExternalIdAccess extends Access<AccountExternalId, AccountExternalId.Key> { - @Override - @PrimaryKey("key") - AccountExternalId get(AccountExternalId.Key key) throws OrmException; - - @Query("WHERE accountId = ?") - ResultSet<AccountExternalId> byAccount(Account.Id id) throws OrmException; - - @Query - ResultSet<AccountExternalId> all() throws OrmException; -}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java index 82660cb..b8bc9f0 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
@@ -29,8 +29,4 @@ @Query("ORDER BY name") ResultSet<AccountGroupName> all() throws OrmException; - - @Query("WHERE name.name >= ? AND name.name <= ? ORDER BY name LIMIT ?") - ResultSet<AccountGroupName> suggestByName(String nameA, String nameB, int limit) - throws OrmException; }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java index 9791572..d4c6354 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/DisallowReadFromChangesReviewDbWrapper.java
@@ -14,7 +14,6 @@ package com.google.gerrit.reviewdb.server; -import com.google.common.util.concurrent.CheckedFuture; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; @@ -82,8 +81,10 @@ throw new UnsupportedOperationException(MSG); } + @SuppressWarnings("deprecation") @Override - public CheckedFuture<Change, OrmException> getAsync(Change.Id key) { + public com.google.common.util.concurrent.CheckedFuture<Change, OrmException> getAsync( + Change.Id key) { throw new UnsupportedOperationException(MSG); } @@ -113,8 +114,10 @@ throw new UnsupportedOperationException(MSG); } + @SuppressWarnings("deprecation") @Override - public CheckedFuture<PatchSetApproval, OrmException> getAsync(PatchSetApproval.Key key) { + public com.google.common.util.concurrent.CheckedFuture<PatchSetApproval, OrmException> getAsync( + PatchSetApproval.Key key) { throw new UnsupportedOperationException(MSG); } @@ -149,8 +152,10 @@ throw new UnsupportedOperationException(MSG); } + @SuppressWarnings("deprecation") @Override - public CheckedFuture<ChangeMessage, OrmException> getAsync(ChangeMessage.Key key) { + public com.google.common.util.concurrent.CheckedFuture<ChangeMessage, OrmException> getAsync( + ChangeMessage.Key key) { throw new UnsupportedOperationException(MSG); } @@ -190,8 +195,10 @@ throw new UnsupportedOperationException(MSG); } + @SuppressWarnings("deprecation") @Override - public CheckedFuture<PatchSet, OrmException> getAsync(PatchSet.Id key) { + public com.google.common.util.concurrent.CheckedFuture<PatchSet, OrmException> getAsync( + PatchSet.Id key) { throw new UnsupportedOperationException(MSG); } @@ -221,8 +228,10 @@ throw new UnsupportedOperationException(MSG); } + @SuppressWarnings("deprecation") @Override - public CheckedFuture<PatchLineComment, OrmException> getAsync(PatchLineComment.Key key) { + public com.google.common.util.concurrent.CheckedFuture<PatchLineComment, OrmException> getAsync( + PatchLineComment.Key key) { throw new UnsupportedOperationException(MSG); }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java index 9b4e1ed..165578a 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -50,8 +50,7 @@ @Relation(id = 6) AccountAccess accounts(); - @Relation(id = 7) - AccountExternalIdAccess accountExternalIds(); + // Deleted @Relation(id = 7) // Deleted @Relation(id = 8) @@ -118,4 +117,8 @@ @Sequence(startWith = FIRST_CHANGE_ID) @Deprecated int nextChangeId() throws OrmException; + + default boolean changesTablesEnabled() { + return true; + } }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java index 6dd3701..c8c4fb2 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -16,7 +16,6 @@ import static com.google.common.base.Preconditions.checkNotNull; -import com.google.common.util.concurrent.CheckedFuture; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; @@ -83,11 +82,6 @@ } @Override - public AccountExternalIdAccess accountExternalIds() { - return delegate.accountExternalIds(); - } - - @Override public AccountGroupAccess accountGroups() { return delegate.accountGroups(); } @@ -158,6 +152,11 @@ return delegate.nextChangeId(); } + @Override + public boolean changesTablesEnabled() { + return delegate.changesTablesEnabled(); + } + public static class ChangeAccessWrapper implements ChangeAccess { protected final ChangeAccess delegate; @@ -190,8 +189,10 @@ return delegate.toMap(c); } + @SuppressWarnings("deprecation") @Override - public CheckedFuture<Change, OrmException> getAsync(Change.Id key) { + public com.google.common.util.concurrent.CheckedFuture<Change, OrmException> getAsync( + Change.Id key) { return delegate.getAsync(key); } @@ -278,8 +279,10 @@ return delegate.toMap(c); } + @SuppressWarnings("deprecation") @Override - public CheckedFuture<PatchSetApproval, OrmException> getAsync(PatchSetApproval.Key key) { + public com.google.common.util.concurrent.CheckedFuture<PatchSetApproval, OrmException> getAsync( + PatchSetApproval.Key key) { return delegate.getAsync(key); } @@ -384,8 +387,10 @@ return delegate.toMap(c); } + @SuppressWarnings("deprecation") @Override - public CheckedFuture<ChangeMessage, OrmException> getAsync(ChangeMessage.Key key) { + public com.google.common.util.concurrent.CheckedFuture<ChangeMessage, OrmException> getAsync( + ChangeMessage.Key key) { return delegate.getAsync(key); } @@ -483,8 +488,10 @@ return delegate.toMap(c); } + @SuppressWarnings("deprecation") @Override - public CheckedFuture<PatchSet, OrmException> getAsync(PatchSet.Id key) { + public com.google.common.util.concurrent.CheckedFuture<PatchSet, OrmException> getAsync( + PatchSet.Id key) { return delegate.getAsync(key); } @@ -577,8 +584,10 @@ return delegate.toMap(c); } + @SuppressWarnings("deprecation") @Override - public CheckedFuture<PatchLineComment, OrmException> getAsync(PatchLineComment.Key key) { + public com.google.common.util.concurrent.CheckedFuture<PatchLineComment, OrmException> getAsync( + PatchLineComment.Key key) { return delegate.getAsync(key); }
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql index deceab9..5a83dc4 100644 --- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql +++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -6,21 +6,10 @@ -- ********************************************************************* -- AccountAccess --- covers: byPreferredEmail, suggestByPreferredEmail +-- covers: byPreferredEmail CREATE INDEX accounts_byPreferredEmail ON accounts (preferred_email); --- covers: suggestByFullName -CREATE INDEX accounts_byFullName -ON accounts (full_name); - - --- ********************************************************************* --- AccountExternalIdAccess --- covers: byAccount -CREATE INDEX account_external_ids_byAccount -ON account_external_ids (account_id); - -- ********************************************************************* -- AccountGroupMemberAccess
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql index 1ec8ea6..98f05ca 100644 --- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql +++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
@@ -7,24 +7,11 @@ -- ********************************************************************* -- AccountAccess --- covers: byPreferredEmail, suggestByPreferredEmail +-- covers: byPreferredEmail CREATE INDEX accounts_byPreferredEmail ON accounts (preferred_email) # --- covers: suggestByFullName -CREATE INDEX accounts_byFullName -ON accounts (full_name) -# - - --- ********************************************************************* --- AccountExternalIdAccess --- covers: byAccount -CREATE INDEX account_external_ids_byAccount -ON account_external_ids (account_id) -# - -- ********************************************************************* -- AccountGroupMemberAccess
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql index a11c86b..dde86a4 100644 --- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql +++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -53,21 +53,10 @@ -- ********************************************************************* -- AccountAccess --- covers: byPreferredEmail, suggestByPreferredEmail +-- covers: byPreferredEmail CREATE INDEX accounts_byPreferredEmail ON accounts (preferred_email); --- covers: suggestByFullName -CREATE INDEX accounts_byFullName -ON accounts (full_name); - - --- ********************************************************************* --- AccountExternalIdAccess --- covers: byAccount -CREATE INDEX account_external_ids_byAccount -ON account_external_ids (account_id); - -- ********************************************************************* -- AccountGroupMemberAccess
diff --git a/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountGroupTest.java b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountGroupTest.java new file mode 100644 index 0000000..02b6dd8 --- /dev/null +++ b/gerrit-reviewdb/src/test/java/com/google/gerrit/reviewdb/client/AccountGroupTest.java
@@ -0,0 +1,32 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.reviewdb.client; + +import static com.google.common.truth.Truth.assertThat; + +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.ZoneOffset; +import org.junit.Test; + +public class AccountGroupTest { + @Test + public void auditCreationInstant() { + Instant instant = LocalDateTime.of(2009, Month.JUNE, 8, 19, 31).toInstant(ZoneOffset.UTC); + assertThat(AccountGroup.auditCreationInstantTs()).isEqualTo(Timestamp.from(instant)); + } +}
diff --git a/gerrit-server/BUILD b/gerrit-server/BUILD index adfe7a4..aa7962e 100644 --- a/gerrit-server/BUILD +++ b/gerrit-server/BUILD
@@ -1,3 +1,4 @@ +load("//lib/prolog:prolog.bzl", "prolog_cafe_library") load("//tools/bzl:junit.bzl", "junit_tests") CONSTANTS_SRC = [ @@ -17,6 +18,13 @@ visibility = ["//visibility:public"], ) +prolog_cafe_library( + name = "prolog-common", + srcs = ["src/main/prolog/gerrit_common.pl"], + visibility = ["//visibility:public"], + deps = [":server"], +) + java_library( name = "server", srcs = SRCS, @@ -138,6 +146,22 @@ ], ) +CUSTOM_TRUTH_SUBJECTS = glob([ + "src/test/java/com/google/gerrit/server/**/*Subject.java", +]) + +java_library( + name = "custom-truth-subjects", + testonly = 1, + srcs = CUSTOM_TRUTH_SUBJECTS, + deps = [ + ":server", + "//gerrit-extension-api:api", + "//gerrit-test-util:test_util", + "//lib:truth", + ], +) + PROLOG_TEST_CASE = [ "src/test/java/com/google/gerrit/rules/PrologTestCase.java", ] @@ -169,9 +193,9 @@ srcs = PROLOG_TESTS, resources = glob(["src/test/resources/com/google/gerrit/rules/**/*"]), deps = TESTUTIL_DEPS + [ + ":prolog-common", ":prolog_test_case", ":testutil", - "//gerrit-server/src/main/prolog:common", "//lib/prolog:runtime", ], ) @@ -186,10 +210,10 @@ srcs = QUERY_TESTS, visibility = ["//visibility:public"], deps = TESTUTIL_DEPS + [ + ":prolog-common", ":testutil", "//gerrit-antlr:query_exception", "//gerrit-antlr:query_parser", - "//gerrit-server/src/main/prolog:common", "//lib/antlr:java_runtime", ], ) @@ -200,10 +224,10 @@ srcs = QUERY_TESTS, visibility = ["//visibility:public"], deps = TESTUTIL_DEPS + [ + ":prolog-common", ":testutil", "//gerrit-antlr:query_exception", "//gerrit-antlr:query_parser", - "//gerrit-server/src/main/prolog:common", "//lib/antlr:java_runtime", ], ) @@ -213,15 +237,16 @@ size = "large", srcs = glob( ["src/test/java/**/*.java"], - exclude = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS, + exclude = TESTUTIL + CUSTOM_TRUTH_SUBJECTS + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS, ), resources = glob(["src/test/resources/com/google/gerrit/server/**/*"]), visibility = ["//visibility:public"], deps = TESTUTIL_DEPS + [ + ":custom-truth-subjects", + ":prolog-common", ":testutil", "//gerrit-antlr:query_exception", "//gerrit-patch-jgit:server", - "//gerrit-server/src/main/prolog:common", "//gerrit-test-util:test_util", "//lib:args4j", "//lib:grappa", @@ -229,6 +254,7 @@ "//lib:guava", "//lib:guava-retrying", "//lib:protobuf", + "//lib:truth-java8-extension", "//lib/bouncycastle:bcprov", "//lib/bouncycastle:bcpkix", "//lib/dropwizard:dropwizard-core", @@ -238,6 +264,18 @@ ], ) +junit_tests( + name = "testutil_test", + size = "small", + srcs = [ + "src/test/java/com/google/gerrit/testutil/IndexVersionsTest.java", + ], + visibility = ["//visibility:public"], + deps = TESTUTIL_DEPS + [ + ":testutil", + ], +) + load("//tools/bzl:javadoc.bzl", "java_doc") java_doc(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java b/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java index 4603141..3cc7335 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java +++ b/gerrit-server/src/main/java/com/google/gerrit/common/EventBroker.java
@@ -16,6 +16,7 @@ import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.lifecycle.LifecycleModule; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; @@ -28,6 +29,9 @@ import com.google.gerrit.server.events.ProjectEvent; import com.google.gerrit.server.events.RefEvent; import com.google.gerrit.server.notedb.ChangeNotes; +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.project.NoSuchChangeException; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectControl; @@ -58,6 +62,7 @@ /** Listeners to receive all changes as they happen. */ protected final DynamicSet<EventListener> unrestrictedListeners; + private final PermissionBackend permissionBackend; protected final ProjectCache projectCache; protected final ChangeNotes.Factory notesFactory; @@ -68,11 +73,13 @@ public EventBroker( DynamicSet<UserScopedEventListener> listeners, DynamicSet<EventListener> unrestrictedListeners, + PermissionBackend permissionBackend, ProjectCache projectCache, ChangeNotes.Factory notesFactory, Provider<ReviewDb> dbProvider) { this.listeners = listeners; this.unrestrictedListeners = unrestrictedListeners; + this.permissionBackend = permissionBackend; this.projectCache = projectCache; this.notesFactory = notesFactory; this.dbProvider = dbProvider; @@ -141,11 +148,12 @@ } protected boolean isVisibleTo(Project.NameKey project, CurrentUser user) { - ProjectState pe = projectCache.get(project); - if (pe == null) { + try { + permissionBackend.user(user).project(project).check(ProjectPermission.ACCESS); + return true; + } catch (AuthException | PermissionBackendException e) { return false; } - return pe.controlFor(user).isVisible(); } protected boolean isVisibleTo(Change change, CurrentUser user) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java index 9773869..880ba24 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/MetricMaker.java
@@ -79,20 +79,13 @@ * @param value only value of the metric. * @param desc description of the metric. */ - public <V> void newConstantMetric(String name, final V value, Description desc) { + public <V> void newConstantMetric(String name, V value, Description desc) { desc.setConstant(); @SuppressWarnings("unchecked") Class<V> type = (Class<V>) value.getClass(); - final CallbackMetric0<V> metric = newCallbackMetric(name, type, desc); - newTrigger( - metric, - new Runnable() { - @Override - public void run() { - metric.set(value); - } - }); + CallbackMetric0<V> metric = newCallbackMetric(name, type, desc); + newTrigger(metric, () -> metric.set(value)); } /** @@ -116,16 +109,9 @@ * @param trigger function to compute the value of the metric. */ public <V> void newCallbackMetric( - String name, Class<V> valueClass, Description desc, final Supplier<V> trigger) { - final CallbackMetric0<V> metric = newCallbackMetric(name, valueClass, desc); - newTrigger( - metric, - new Runnable() { - @Override - public void run() { - metric.set(trigger.get()); - } - }); + String name, Class<V> valueClass, Description desc, Supplier<V> trigger) { + CallbackMetric0<V> metric = newCallbackMetric(name, valueClass, desc); + newTrigger(metric, () -> metric.set(trigger.get())); } /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java index 6910d22..5e25651 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/CallbackMetricImpl0.java
@@ -68,7 +68,7 @@ } @Override - public void register(final Runnable trigger) { + public void register(Runnable trigger) { registry.register( name, new com.codahale.metrics.Gauge<V>() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java index 52e35c3..f0ae97e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java
@@ -17,10 +17,14 @@ import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.server.CurrentUser; +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 org.kohsuke.args4j.Option; class GetMetric implements RestReadView<MetricResource> { + private final PermissionBackend permissionBackend; private final CurrentUser user; private final DropWizardMetricMaker metrics; @@ -28,16 +32,16 @@ boolean dataOnly; @Inject - GetMetric(CurrentUser user, DropWizardMetricMaker metrics) { + GetMetric(PermissionBackend permissionBackend, CurrentUser user, DropWizardMetricMaker metrics) { + this.permissionBackend = permissionBackend; this.user = user; this.metrics = metrics; } @Override - public MetricJson apply(MetricResource resource) throws AuthException { - if (!user.getCapabilities().canViewCaches()) { - throw new AuthException("restricted to viewCaches"); - } + public MetricJson apply(MetricResource resource) + throws AuthException, PermissionBackendException { + permissionBackend.user(user).check(GlobalPermission.VIEW_CACHES); return new MetricJson( resource.getMetric(), metrics.getAnnotations(resource.getName()), dataOnly); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java index 891f4ac..59f6b97 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
@@ -19,6 +19,9 @@ import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.config.ConfigResource; +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.ArrayList; import java.util.List; @@ -28,6 +31,7 @@ import org.kohsuke.args4j.Option; class ListMetrics implements RestReadView<ConfigResource> { + private final PermissionBackend permissionBackend; private final CurrentUser user; private final DropWizardMetricMaker metrics; @@ -43,16 +47,17 @@ List<String> query = new ArrayList<>(); @Inject - ListMetrics(CurrentUser user, DropWizardMetricMaker metrics) { + ListMetrics( + PermissionBackend permissionBackend, CurrentUser user, DropWizardMetricMaker metrics) { + this.permissionBackend = permissionBackend; this.user = user; this.metrics = metrics; } @Override - public Map<String, MetricJson> apply(ConfigResource resource) throws AuthException { - if (!user.getCapabilities().canViewCaches()) { - throw new AuthException("restricted to viewCaches"); - } + public Map<String, MetricJson> apply(ConfigResource resource) + throws AuthException, PermissionBackendException { + permissionBackend.user(user).check(GlobalPermission.VIEW_CACHES); SortedMap<String, MetricJson> out = new TreeMap<>(); List<String> prefixes = new ArrayList<>(query.size());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java index 2686f1f..6abf17c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java
@@ -23,6 +23,9 @@ import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.config.ConfigResource; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -31,6 +34,7 @@ class MetricsCollection implements ChildCollection<ConfigResource, MetricResource> { private final DynamicMap<RestView<MetricResource>> views; private final Provider<ListMetrics> list; + private final PermissionBackend permissionBackend; private final Provider<CurrentUser> user; private final DropWizardMetricMaker metrics; @@ -38,10 +42,12 @@ MetricsCollection( DynamicMap<RestView<MetricResource>> views, Provider<ListMetrics> list, + PermissionBackend permissionBackend, Provider<CurrentUser> user, DropWizardMetricMaker metrics) { this.views = views; this.list = list; + this.permissionBackend = permissionBackend; this.user = user; this.metrics = metrics; } @@ -58,10 +64,8 @@ @Override public MetricResource parse(ConfigResource parent, IdString id) - throws ResourceNotFoundException, AuthException { - if (!user.get().getCapabilities().canViewCaches()) { - throw new AuthException("restricted to viewCaches"); - } + throws ResourceNotFoundException, AuthException, PermissionBackendException { + permissionBackend.user(user).check(GlobalPermission.VIEW_CACHES); Metric metric = metrics.getMetric(id.get()); if (metric == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/ProcMetricModule.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/ProcMetricModule.java index 11f8e50..8978e99 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/ProcMetricModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/proc/ProcMetricModule.java
@@ -103,7 +103,7 @@ } private void procJvmMemory(MetricMaker metrics) { - final CallbackMetric0<Long> heapCommitted = + CallbackMetric0<Long> heapCommitted = metrics.newCallbackMetric( "proc/jvm/memory/heap_committed", Long.class, @@ -111,7 +111,7 @@ .setGauge() .setUnit(Units.BYTES)); - final CallbackMetric0<Long> heapUsed = + CallbackMetric0<Long> heapUsed = metrics.newCallbackMetric( "proc/jvm/memory/heap_used", Long.class, @@ -119,7 +119,7 @@ .setGauge() .setUnit(Units.BYTES)); - final CallbackMetric0<Long> nonHeapCommitted = + CallbackMetric0<Long> nonHeapCommitted = metrics.newCallbackMetric( "proc/jvm/memory/non_heap_committed", Long.class, @@ -127,7 +127,7 @@ .setGauge() .setUnit(Units.BYTES)); - final CallbackMetric0<Long> nonHeapUsed = + CallbackMetric0<Long> nonHeapUsed = metrics.newCallbackMetric( "proc/jvm/memory/non_heap_used", Long.class, @@ -135,7 +135,7 @@ .setGauge() .setUnit(Units.BYTES)); - final CallbackMetric0<Integer> objectPendingFinalizationCount = + CallbackMetric0<Integer> objectPendingFinalizationCount = metrics.newCallbackMetric( "proc/jvm/memory/object_pending_finalization_count", Integer.class, @@ -143,39 +143,36 @@ .setGauge() .setUnit("objects")); - final MemoryMXBean memory = ManagementFactory.getMemoryMXBean(); + MemoryMXBean memory = ManagementFactory.getMemoryMXBean(); metrics.newTrigger( ImmutableSet.<CallbackMetric<?>>of( heapCommitted, heapUsed, nonHeapCommitted, nonHeapUsed, objectPendingFinalizationCount), - new Runnable() { - @Override - public void run() { - try { - MemoryUsage stats = memory.getHeapMemoryUsage(); - heapCommitted.set(stats.getCommitted()); - heapUsed.set(stats.getUsed()); - } catch (IllegalArgumentException e) { - // MXBean may throw due to a bug in Java 7; ignore. - } - - MemoryUsage stats = memory.getNonHeapMemoryUsage(); - nonHeapCommitted.set(stats.getCommitted()); - nonHeapUsed.set(stats.getUsed()); - - objectPendingFinalizationCount.set(memory.getObjectPendingFinalizationCount()); + () -> { + try { + MemoryUsage stats = memory.getHeapMemoryUsage(); + heapCommitted.set(stats.getCommitted()); + heapUsed.set(stats.getUsed()); + } catch (IllegalArgumentException e) { + // MXBean may throw due to a bug in Java 7; ignore. } + + MemoryUsage stats = memory.getNonHeapMemoryUsage(); + nonHeapCommitted.set(stats.getCommitted()); + nonHeapUsed.set(stats.getUsed()); + + objectPendingFinalizationCount.set(memory.getObjectPendingFinalizationCount()); }); } private void procJvmGc(MetricMaker metrics) { - final CallbackMetric1<String, Long> gcCount = + CallbackMetric1<String, Long> gcCount = metrics.newCallbackMetric( "proc/jvm/gc/count", Long.class, new Description("Number of GCs").setCumulative(), Field.ofString("gc_name", "The name of the garbage collector")); - final CallbackMetric1<String, Long> gcTime = + CallbackMetric1<String, Long> gcTime = metrics.newCallbackMetric( "proc/jvm/gc/time", Long.class, @@ -187,34 +184,26 @@ metrics.newTrigger( gcCount, gcTime, - new Runnable() { - @Override - public void run() { - for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) { - long count = gc.getCollectionCount(); - if (count != -1) { - gcCount.set(gc.getName(), count); - } - long time = gc.getCollectionTime(); - if (time != -1) { - gcTime.set(gc.getName(), time); - } + () -> { + for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) { + long count = gc.getCollectionCount(); + if (count != -1) { + gcCount.set(gc.getName(), count); + } + long time = gc.getCollectionTime(); + if (time != -1) { + gcTime.set(gc.getName(), time); } } }); } private void procJvmThread(MetricMaker metrics) { - final ThreadMXBean thread = ManagementFactory.getThreadMXBean(); + ThreadMXBean thread = ManagementFactory.getThreadMXBean(); metrics.newCallbackMetric( "proc/jvm/thread/num_live", Integer.class, new Description("Current live thread count").setGauge().setUnit("threads"), - new Supplier<Integer>() { - @Override - public Integer get() { - return thread.getThreadCount(); - } - }); + () -> thread.getThreadCount()); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateClassLoader.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateClassLoader.java index c2643de..3478694 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateClassLoader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateClassLoader.java
@@ -26,7 +26,7 @@ LinkedHashMultimap.create(); public PredicateClassLoader( - final DynamicSet<PredicateProvider> predicateProviders, final ClassLoader parent) { + final DynamicSet<PredicateProvider> predicateProviders, ClassLoader parent) { super(parent); for (PredicateProvider predicateProvider : predicateProviders) { @@ -37,10 +37,10 @@ } @Override - protected Class<?> findClass(final String className) throws ClassNotFoundException { + protected Class<?> findClass(String className) throws ClassNotFoundException { final Collection<ClassLoader> classLoaders = packageClassLoaderMap.get(getPackageName(className)); - for (final ClassLoader cl : classLoaders) { + for (ClassLoader cl : classLoaders) { try { return Class.forName(className, true, cl); } catch (ClassNotFoundException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java index 9538121a4..36cb4cc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java +++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
@@ -20,6 +20,7 @@ import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.patch.PatchSetInfoFactory; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.project.ProjectCache; import com.google.inject.Inject; import com.google.inject.Provider; @@ -136,7 +137,7 @@ /** Release resources stored in interpreter's hash manager. */ public void close() { - for (final Iterator<Runnable> i = cleanup.iterator(); i.hasNext(); ) { + for (Iterator<Runnable> i = cleanup.iterator(); i.hasNext(); ) { try { i.next().run(); } catch (Throwable err) { @@ -166,6 +167,7 @@ } private final ProjectCache projectCache; + private final PermissionBackend permissionBackend; private final GitRepositoryManager repositoryManager; private final PatchListCache patchListCache; private final PatchSetInfoFactory patchSetInfoFactory; @@ -177,6 +179,7 @@ @Inject Args( ProjectCache projectCache, + PermissionBackend permissionBackend, GitRepositoryManager repositoryManager, PatchListCache patchListCache, PatchSetInfoFactory patchSetInfoFactory, @@ -184,6 +187,7 @@ Provider<AnonymousUser> anonymousUser, @GerritServerConfig Config config) { this.projectCache = projectCache; + this.permissionBackend = permissionBackend; this.repositoryManager = repositoryManager; this.patchListCache = patchListCache; this.patchSetInfoFactory = patchSetInfoFactory; @@ -213,6 +217,10 @@ return projectCache; } + public PermissionBackend getPermissionBackend() { + return permissionBackend; + } + public GitRepositoryManager getGitRepositoryManager() { return repositoryManager; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java index 34fcb52..0f4216a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java +++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -26,6 +26,7 @@ import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.patch.PatchList; import com.google.gerrit.server.patch.PatchListCache; @@ -33,6 +34,7 @@ import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.query.change.ChangeData; import com.google.gwtorm.server.OrmException; @@ -45,6 +47,7 @@ import org.eclipse.jgit.lib.Repository; public final class StoredValues { + public static final StoredValue<AccountCache> ACCOUNT_CACHE = create(AccountCache.class); public static final StoredValue<ReviewDb> REVIEW_DB = create(ReviewDb.class); public static final StoredValue<ChangeData> CHANGE_DATA = create(ChangeData.class); @@ -119,23 +122,26 @@ GitRepositoryManager gitMgr = env.getArgs().getGitRepositoryManager(); Change change = getChange(engine); Project.NameKey projectKey = change.getProject(); - final Repository repo; + Repository repo; try { repo = gitMgr.openRepository(projectKey); } catch (IOException e) { throw new SystemException(e.getMessage()); } - env.addToCleanup( - new Runnable() { - @Override - public void run() { - repo.close(); - } - }); + env.addToCleanup(repo::close); return repo; } }; + public static final StoredValue<PermissionBackend> PERMISSION_BACKEND = + new StoredValue<PermissionBackend>() { + @Override + protected PermissionBackend createValue(Prolog engine) { + PrologEnvironment env = (PrologEnvironment) engine.control; + return env.getArgs().getPermissionBackend(); + } + }; + public static final StoredValue<AnonymousUser> ANONYMOUS_USER = new StoredValue<AnonymousUser>() { @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java index de8e9a4..c96d61a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
@@ -14,20 +14,13 @@ package com.google.gerrit.server; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.GroupMembership; import com.google.gerrit.server.account.ListGroupMembership; import com.google.gerrit.server.group.SystemGroupBackend; -import com.google.inject.Inject; import java.util.Collections; /** An anonymous user who has not yet authenticated. */ public class AnonymousUser extends CurrentUser { - @Inject - AnonymousUser(CapabilityControl.Factory capabilityControlFactory) { - super(capabilityControlFactory); - } - @Override public GroupMembership getEffectiveGroups() { return new ListGroupMembership(Collections.singleton(SystemGroupBackend.ANONYMOUS_USERS));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java index cb65ed3..6771616 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -20,6 +20,7 @@ import com.google.common.collect.HashBasedTable; import com.google.common.collect.ListMultimap; import com.google.common.collect.Table; +import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.extensions.client.ChangeKind; import com.google.gerrit.reviewdb.client.Account; @@ -27,8 +28,8 @@ import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.change.ChangeKindCache; -import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.LabelNormalizer; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectState; @@ -41,8 +42,9 @@ import java.util.Collections; import java.util.List; import java.util.TreeMap; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; /** * Copies approvals between patch sets. @@ -52,7 +54,6 @@ */ @Singleton public class ApprovalCopier { - private final GitRepositoryManager repoManager; private final ProjectCache projectCache; private final ChangeKindCache changeKindCache; private final LabelNormalizer labelNormalizer; @@ -61,13 +62,11 @@ @Inject ApprovalCopier( - GitRepositoryManager repoManager, ProjectCache projectCache, ChangeKindCache changeKindCache, LabelNormalizer labelNormalizer, ChangeData.Factory changeDataFactory, PatchSetUtil psUtil) { - this.repoManager = repoManager; this.projectCache = projectCache; this.changeKindCache = changeKindCache; this.labelNormalizer = labelNormalizer; @@ -81,10 +80,18 @@ * @param db review database. * @param ctl change control for user uploading PatchSet * @param ps new PatchSet + * @param rw open walk that can read the patch set commit; null to open the repo on demand. + * @param repoConfig repo config used for change kind detection; null to read from repo on demand. * @throws OrmException */ - public void copy(ReviewDb db, ChangeControl ctl, PatchSet ps) throws OrmException { - copy(db, ctl, ps, Collections.<PatchSetApproval>emptyList()); + public void copyInReviewDb( + ReviewDb db, + ChangeControl ctl, + PatchSet ps, + @Nullable RevWalk rw, + @Nullable Config repoConfig) + throws OrmException { + copyInReviewDb(db, ctl, ps, rw, repoConfig, Collections.emptyList()); } /** @@ -93,31 +100,56 @@ * @param db review database. * @param ctl change control for user uploading PatchSet * @param ps new PatchSet + * @param rw open walk that can read the patch set commit; null to open the repo on demand. + * @param repoConfig repo config used for change kind detection; null to read from repo on demand. * @param dontCopy PatchSetApprovals indicating which (account, label) pairs should not be copied * @throws OrmException */ - public void copy(ReviewDb db, ChangeControl ctl, PatchSet ps, Iterable<PatchSetApproval> dontCopy) + public void copyInReviewDb( + ReviewDb db, + ChangeControl ctl, + PatchSet ps, + @Nullable RevWalk rw, + @Nullable Config repoConfig, + Iterable<PatchSetApproval> dontCopy) throws OrmException { - db.patchSetApprovals().insert(getForPatchSet(db, ctl, ps, dontCopy)); - } - - Iterable<PatchSetApproval> getForPatchSet(ReviewDb db, ChangeControl ctl, PatchSet.Id psId) - throws OrmException { - return getForPatchSet(db, ctl, psId, Collections.<PatchSetApproval>emptyList()); + if (PrimaryStorage.of(ctl.getChange()) == PrimaryStorage.REVIEW_DB) { + db.patchSetApprovals().insert(getForPatchSet(db, ctl, ps, rw, repoConfig, dontCopy)); + } } Iterable<PatchSetApproval> getForPatchSet( - ReviewDb db, ChangeControl ctl, PatchSet.Id psId, Iterable<PatchSetApproval> dontCopy) + ReviewDb db, + ChangeControl ctl, + PatchSet.Id psId, + @Nullable RevWalk rw, + @Nullable Config repoConfig) + throws OrmException { + return getForPatchSet(db, ctl, psId, rw, repoConfig, Collections.<PatchSetApproval>emptyList()); + } + + Iterable<PatchSetApproval> getForPatchSet( + ReviewDb db, + ChangeControl ctl, + PatchSet.Id psId, + @Nullable RevWalk rw, + @Nullable Config repoConfig, + Iterable<PatchSetApproval> dontCopy) throws OrmException { PatchSet ps = psUtil.get(db, ctl.getNotes(), psId); if (ps == null) { return Collections.emptyList(); } - return getForPatchSet(db, ctl, ps, dontCopy); + return getForPatchSet(db, ctl, ps, rw, repoConfig, dontCopy); } private Iterable<PatchSetApproval> getForPatchSet( - ReviewDb db, ChangeControl ctl, PatchSet ps, Iterable<PatchSetApproval> dontCopy) + ReviewDb db, + ChangeControl ctl, + PatchSet ps, + @Nullable RevWalk rw, + @Nullable Config repoConfig, + Iterable<PatchSetApproval> dontCopy) throws OrmException { checkNotNull(ps, "ps should not be null"); ChangeData cd = changeDataFactory.create(db, ctl); @@ -140,39 +172,38 @@ TreeMap<Integer, PatchSet> patchSets = getPatchSets(cd); - try (Repository repo = repoManager.openRepository(project.getProject().getNameKey())) { - // Walk patch sets strictly less than current in descending order. - Collection<PatchSet> allPrior = - patchSets.descendingMap().tailMap(ps.getId().get(), false).values(); - for (PatchSet priorPs : allPrior) { - List<PatchSetApproval> priorApprovals = all.get(priorPs.getId()); - if (priorApprovals.isEmpty()) { + // Walk patch sets strictly less than current in descending order. + Collection<PatchSet> allPrior = + patchSets.descendingMap().tailMap(ps.getId().get(), false).values(); + for (PatchSet priorPs : allPrior) { + List<PatchSetApproval> priorApprovals = all.get(priorPs.getId()); + if (priorApprovals.isEmpty()) { + continue; + } + + ChangeKind kind = + changeKindCache.getChangeKind( + project.getProject().getNameKey(), + rw, + repoConfig, + ObjectId.fromString(priorPs.getRevision().get()), + ObjectId.fromString(ps.getRevision().get())); + + for (PatchSetApproval psa : priorApprovals) { + if (wontCopy.contains(psa.getLabel(), psa.getAccountId())) { continue; } - - ChangeKind kind = - changeKindCache.getChangeKind( - project.getProject().getNameKey(), - repo, - ObjectId.fromString(priorPs.getRevision().get()), - ObjectId.fromString(ps.getRevision().get())); - - for (PatchSetApproval psa : priorApprovals) { - if (wontCopy.contains(psa.getLabel(), psa.getAccountId())) { - continue; - } - if (byUser.contains(psa.getLabel(), psa.getAccountId())) { - continue; - } - if (!canCopy(project, psa, ps.getId(), kind)) { - wontCopy.put(psa.getLabel(), psa.getAccountId(), psa); - continue; - } - byUser.put(psa.getLabel(), psa.getAccountId(), copy(psa, ps.getId())); + if (byUser.contains(psa.getLabel(), psa.getAccountId())) { + continue; } + if (!canCopy(project, psa, ps.getId(), kind)) { + wontCopy.put(psa.getLabel(), psa.getAccountId(), psa); + continue; + } + byUser.put(psa.getLabel(), psa.getAccountId(), copy(psa, ps.getId())); } - return labelNormalizer.normalize(ctl, byUser.values()).getNormalized(); } + return labelNormalizer.normalize(ctl, byUser.values()).getNormalized(); } catch (IOException e) { throw new OrmException(e); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java index 910fbc2..e4cca59 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -28,6 +28,7 @@ import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import com.google.common.primitives.Shorts; +import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; import com.google.gerrit.common.data.Permission; @@ -60,6 +61,8 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.revwalk.RevWalk; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -97,7 +100,7 @@ } private static Iterable<PatchSetApproval> filterApprovals( - Iterable<PatchSetApproval> psas, final Account.Id accountId) { + Iterable<PatchSetApproval> psas, Account.Id accountId) { return Iterables.filter(psas, a -> Objects.equals(a.getAccountId(), accountId)); } @@ -322,7 +325,7 @@ accountId, ps.getUploader()); if (approvals.isEmpty()) { - return Collections.emptyList(); + return ImmutableList.of(); } checkApprovals(approvals, changeCtl); List<PatchSetApproval> cells = new ArrayList<>(approvals.size()); @@ -376,20 +379,31 @@ return notes.load().getApprovals(); } - public Iterable<PatchSetApproval> byPatchSet(ReviewDb db, ChangeControl ctl, PatchSet.Id psId) + public Iterable<PatchSetApproval> byPatchSet( + ReviewDb db, + ChangeControl ctl, + PatchSet.Id psId, + @Nullable RevWalk rw, + @Nullable Config repoConfig) throws OrmException { if (!migration.readChanges()) { return sortApprovals(db.patchSetApprovals().byPatchSet(psId)); } - return copier.getForPatchSet(db, ctl, psId); + return copier.getForPatchSet(db, ctl, psId, rw, repoConfig); } public Iterable<PatchSetApproval> byPatchSetUser( - ReviewDb db, ChangeControl ctl, PatchSet.Id psId, Account.Id accountId) throws OrmException { + ReviewDb db, + ChangeControl ctl, + PatchSet.Id psId, + Account.Id accountId, + @Nullable RevWalk rw, + @Nullable Config repoConfig) + throws OrmException { if (!migration.readChanges()) { return sortApprovals(db.patchSetApprovals().byPatchSetUser(psId, accountId)); } - return filterApprovals(byPatchSet(db, ctl, psId), accountId); + return filterApprovals(byPatchSet(db, ctl, psId, rw, repoConfig), accountId); } public PatchSetApproval getSubmitter(ReviewDb db, ChangeNotes notes, PatchSet.Id c) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java index 2f3a76f..ff7a5ce 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
@@ -14,9 +14,15 @@ package com.google.gerrit.server; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Sets; import com.google.common.primitives.Ints; import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.change.ChangeTriplet; +import com.google.gerrit.server.index.IndexConfig; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.query.change.ChangeData; @@ -29,14 +35,27 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.Set; +import org.eclipse.jgit.errors.RepositoryNotFoundException; @Singleton public class ChangeFinder { + + private final IndexConfig indexConfig; private final Provider<InternalChangeQuery> queryProvider; + private final Provider<ReviewDb> reviewDb; + private final ChangeControl.GenericFactory changeControlFactory; @Inject - ChangeFinder(Provider<InternalChangeQuery> queryProvider) { + ChangeFinder( + IndexConfig indexConfig, + Provider<InternalChangeQuery> queryProvider, + Provider<ReviewDb> reviewDb, + ChangeControl.GenericFactory changeControlFactory) { + this.indexConfig = indexConfig; this.queryProvider = queryProvider; + this.reviewDb = reviewDb; + this.changeControlFactory = changeControlFactory; } /** @@ -49,12 +68,63 @@ * @throws OrmException if an error occurred querying the database. */ public List<ChangeControl> find(String id, CurrentUser user) throws OrmException { + if (id.isEmpty()) { + return Collections.emptyList(); + } + // Use the index to search for changes, but don't return any stored fields, // to force rereading in case the index is stale. InternalChangeQuery query = queryProvider.get().noFields(); - // Try legacy id - if (!id.isEmpty() && id.charAt(0) != '0') { + int numTwiddles = 0; + for (char c : id.toCharArray()) { + if (c == '~') { + numTwiddles++; + } + } + + if (numTwiddles == 1) { + // Try project~numericChangeId + String project = id.substring(0, id.indexOf('~')); + Integer n = Ints.tryParse(id.substring(project.length() + 1)); + if (n != null) { + Change.Id changeId = new Change.Id(n); + try { + return ImmutableList.of( + changeControlFactory.controlFor( + reviewDb.get(), Project.NameKey.parse(project), changeId, user)); + } catch (NoSuchChangeException e) { + return Collections.emptyList(); + } catch (IllegalArgumentException e) { + String changeNotFound = String.format("change %s not found in ReviewDb", changeId); + String projectNotFound = + String.format( + "passed project %s when creating ChangeNotes for %s, but actual project is", + project, changeId); + if (e.getMessage().equals(changeNotFound) || e.getMessage().startsWith(projectNotFound)) { + return Collections.emptyList(); + } + throw e; + } catch (OrmException e) { + // Distinguish between a RepositoryNotFoundException (project argument invalid) and + // other OrmExceptions (failure in the persistence layer). + if (Throwables.getRootCause(e) instanceof RepositoryNotFoundException) { + return Collections.emptyList(); + } + throw e; + } + } + } else if (numTwiddles == 2) { + // Try change triplet + Optional<ChangeTriplet> triplet = ChangeTriplet.parse(id); + if (triplet.isPresent()) { + return asChangeControls( + query.byBranchKey(triplet.get().branch(), triplet.get().id()), user); + } + } + + // Try numeric changeId + if (id.charAt(0) != '0') { Integer n = Ints.tryParse(id); if (n != null) { return asChangeControls(query.byLegacyChangeId(new Change.Id(n)), user); @@ -62,17 +132,7 @@ } // Try isolated changeId - if (!id.contains("~")) { - return asChangeControls(query.byKeyPrefix(id), user); - } - - // Try change triplet - Optional<ChangeTriplet> triplet = ChangeTriplet.parse(id); - if (triplet.isPresent()) { - return asChangeControls(query.byBranchKey(triplet.get().branch(), triplet.get().id()), user); - } - - return Collections.emptyList(); + return asChangeControls(query.byKeyPrefix(id), user); } public ChangeControl findOne(Change.Id id, CurrentUser user) throws OrmException { @@ -93,8 +153,24 @@ private List<ChangeControl> asChangeControls(List<ChangeData> cds, CurrentUser user) throws OrmException { List<ChangeControl> ctls = new ArrayList<>(cds.size()); + if (!indexConfig.separateChangeSubIndexes()) { + for (ChangeData cd : cds) { + ctls.add(cd.changeControl(user)); + } + return ctls; + } + + // If an index implementation uses separate non-atomic subindexes, it's possible to temporarily + // observe a change as present in both subindexes, if this search is concurrent with a write. + // Dedup to avoid confusing the caller. We can choose an arbitrary ChangeData instance because + // the index results have no stored fields, so the data is already reloaded. (It's also possible + // that a change might appear in zero subindexes, but there's nothing we can do here to help + // this case.) + Set<Change.Id> seen = Sets.newHashSetWithExpectedSize(cds.size()); for (ChangeData cd : cds) { - ctls.add(cd.changeControl(user)); + if (seen.add(cd.getId())) { + ctls.add(cd.changeControl(user)); + } } return ctls; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java index d277bf9..9aae00b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -43,20 +43,33 @@ */ @Singleton public class ChangeMessagesUtil { - public static final String TAG_ABANDON = "autogenerated:gerrit:abandon"; - public static final String TAG_CHERRY_PICK_CHANGE = "autogenerated:gerrit:cherryPickChange"; - public static final String TAG_DELETE_ASSIGNEE = "autogenerated:gerrit:deleteAssignee"; - public static final String TAG_DELETE_REVIEWER = "autogenerated:gerrit:deleteReviewer"; - public static final String TAG_DELETE_VOTE = "autogenerated:gerrit:deleteVote"; - public static final String TAG_MERGED = "autogenerated:gerrit:merged"; - public static final String TAG_MOVE = "autogenerated:gerrit:move"; - public static final String TAG_RESTORE = "autogenerated:gerrit:restore"; - public static final String TAG_REVERT = "autogenerated:gerrit:revert"; - public static final String TAG_SET_ASSIGNEE = "autogenerated:gerrit:setAssignee"; - public static final String TAG_SET_DESCRIPTION = "autogenerated:gerrit:setPsDescription"; - public static final String TAG_SET_HASHTAGS = "autogenerated:gerrit:setHashtag"; - public static final String TAG_SET_TOPIC = "autogenerated:gerrit:setTopic"; - public static final String TAG_UPLOADED_PATCH_SET = "autogenerated:gerrit:newPatchSet"; + public static final String AUTOGENERATED_TAG_PREFIX = "autogenerated:"; + + public static final String TAG_ABANDON = AUTOGENERATED_TAG_PREFIX + "gerrit:abandon"; + public static final String TAG_CHERRY_PICK_CHANGE = + AUTOGENERATED_TAG_PREFIX + "gerrit:cherryPickChange"; + public static final String TAG_DELETE_ASSIGNEE = + AUTOGENERATED_TAG_PREFIX + "gerrit:deleteAssignee"; + public static final String TAG_DELETE_REVIEWER = + AUTOGENERATED_TAG_PREFIX + "gerrit:deleteReviewer"; + public static final String TAG_DELETE_VOTE = AUTOGENERATED_TAG_PREFIX + "gerrit:deleteVote"; + public static final String TAG_MERGED = AUTOGENERATED_TAG_PREFIX + "gerrit:merged"; + public static final String TAG_MOVE = AUTOGENERATED_TAG_PREFIX + "gerrit:move"; + public static final String TAG_RESTORE = AUTOGENERATED_TAG_PREFIX + "gerrit:restore"; + public static final String TAG_REVERT = AUTOGENERATED_TAG_PREFIX + "gerrit:revert"; + public static final String TAG_SET_ASSIGNEE = AUTOGENERATED_TAG_PREFIX + "gerrit:setAssignee"; + public static final String TAG_SET_DESCRIPTION = + AUTOGENERATED_TAG_PREFIX + "gerrit:setPsDescription"; + public static final String TAG_SET_HASHTAGS = AUTOGENERATED_TAG_PREFIX + "gerrit:setHashtag"; + public static final String TAG_SET_PRIVATE = AUTOGENERATED_TAG_PREFIX + "gerrit:setPrivate"; + public static final String TAG_SET_READY = AUTOGENERATED_TAG_PREFIX + "gerrit:setReadyForReview"; + public static final String TAG_SET_TOPIC = AUTOGENERATED_TAG_PREFIX + "gerrit:setTopic"; + public static final String TAG_SET_WIP = AUTOGENERATED_TAG_PREFIX + "gerrit:setWorkInProgress"; + public static final String TAG_UNSET_PRIVATE = AUTOGENERATED_TAG_PREFIX + "gerrit:unsetPrivate"; + public static final String TAG_UPLOADED_PATCH_SET = + AUTOGENERATED_TAG_PREFIX + "gerrit:newPatchSet"; + public static final String TAG_UPLOADED_WIP_PATCH_SET = + AUTOGENERATED_TAG_PREFIX + "gerrit:newWipPatchSet"; public static ChangeMessage newMessage(ChangeContext ctx, String body, @Nullable String tag) { return newMessage(ctx.getChange().currentPatchSetId(), ctx.getUser(), ctx.getWhen(), body, tag); @@ -78,6 +91,10 @@ return m; } + public static String uploadedPatchSetTag(boolean workInProgress) { + return workInProgress ? TAG_UPLOADED_WIP_PATCH_SET : TAG_UPLOADED_PATCH_SET; + } + private static List<ChangeMessage> sortChangeMessages(Iterable<ChangeMessage> changeMessage) { return ChangeNotes.MESSAGE_BY_TIME.sortedCopy(changeMessage); } @@ -116,4 +133,12 @@ update.setTag(changeMessage.getTag()); db.changeMessages().insert(Collections.singleton(changeMessage)); } + + /** + * @param tag value of a tag, or null. + * @return whether the tag starts with the autogenerated prefix. + */ + public static boolean isAutogenerated(@Nullable String tag) { + return tag != null && tag.startsWith(AUTOGENERATED_TAG_PREFIX); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java index 10ae60c..c9b726b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -16,16 +16,18 @@ import static java.util.Comparator.comparingInt; +import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.common.io.BaseEncoding; +import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.inject.Singleton; import java.io.IOException; import java.security.SecureRandom; import java.util.Map; import java.util.Random; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.lib.Repository; @Singleton @@ -47,7 +49,16 @@ return UUID_ENCODING.encode(buf, 0, 4) + '_' + UUID_ENCODING.encode(buf, 4, 4); } - public static PatchSet.Id nextPatchSetId(Map<String, Ref> allRefs, PatchSet.Id id) { + /** + * Get the next patch set ID from a previously-read map of all refs. + * + * @param allRefs map of full ref name to ref, in the same format returned by {@link + * org.eclipse.jgit.lib.RefDatabase#getRefs(String)} when passing {@code ""}. + * @param id previous patch set ID. + * @return next unused patch set ID for the same change, skipping any IDs whose corresponding ref + * names appear in the {@code allRefs} map. + */ + public static PatchSet.Id nextPatchSetIdFromAllRefsMap(Map<String, Ref> allRefs, PatchSet.Id id) { PatchSet.Id next = nextPatchSetId(id); while (allRefs.containsKey(next.toRefName())) { next = nextPatchSetId(next); @@ -55,12 +66,55 @@ return next; } + /** + * Get the next patch set ID from a previously-read map of refs below the change prefix. + * + * @param changeRefs map of ref suffix to SHA-1, where the keys are ref names with the {@code + * refs/changes/CD/ABCD/} prefix stripped. All refs should be under {@code id}'s change ref + * prefix. The keys match the format returned by {@link + * org.eclipse.jgit.lib.RefDatabase#getRefs(String)} when passing the appropriate {@code + * refs/changes/CD/ABCD}. + * @param id previous patch set ID. + * @return next unused patch set ID for the same change, skipping any IDs whose corresponding ref + * names appear in the {@code changeRefs} map. + */ + public static PatchSet.Id nextPatchSetIdFromChangeRefsMap( + Map<String, ObjectId> changeRefs, PatchSet.Id id) { + int prefixLen = id.getParentKey().toRefPrefix().length(); + PatchSet.Id next = nextPatchSetId(id); + while (changeRefs.containsKey(next.toRefName().substring(prefixLen))) { + next = nextPatchSetId(next); + } + return next; + } + + /** + * Get the next patch set ID just looking at a single previous patch set ID. + * + * <p>This patch set ID may or may not be available in the database; callers that want a + * previously-unused ID should use {@link #nextPatchSetIdFromAllRefsMap} or {@link + * #nextPatchSetIdFromChangeRefsMap}. + * + * @param id previous patch set ID. + * @return next patch set ID for the same change, incrementing by 1. + */ public static PatchSet.Id nextPatchSetId(PatchSet.Id id) { return new PatchSet.Id(id.getParentKey(), id.get() + 1); } + /** + * Get the next patch set ID from scanning refs in the repo. + * + * @param git repository to scan for patch set refs. + * @param id previous patch set ID. + * @return next unused patch set ID for the same change, skipping any IDs whose corresponding ref + * names appear in the repository. + */ public static PatchSet.Id nextPatchSetId(Repository git, PatchSet.Id id) throws IOException { - return nextPatchSetId(git.getRefDatabase().getRefs(RefDatabase.ALL), id); + return nextPatchSetIdFromChangeRefsMap( + Maps.transformValues( + git.getRefDatabase().getRefs(id.getParentKey().toRefPrefix()), Ref::getObjectId), + id); } public static String cropSubject(String subject) { @@ -78,5 +132,9 @@ return subject; } + public static String status(Change c) { + return c != null ? c.getStatus().name().toLowerCase() : "deleted"; + } + private ChangeUtil() {} }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java index 8d2289a..37b6435 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
@@ -16,7 +16,9 @@ import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.gerrit.reviewdb.client.PatchLineComment.Status.PUBLISHED; import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; import com.google.common.collect.ComparisonChain; import com.google.common.collect.FluentIterable; @@ -24,6 +26,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; +import com.google.common.collect.Streams; import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.client.Side; import com.google.gerrit.extensions.common.CommentInfo; @@ -38,14 +41,17 @@ import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.client.RobotComment; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.reviewdb.server.ReviewDbUtil; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.config.GerritServerId; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.patch.PatchListNotAvailableException; +import com.google.gerrit.server.update.BatchUpdateReviewDb; import com.google.gerrit.server.update.ChangeContext; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.ResultSet; @@ -56,9 +62,9 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Predicate; -import java.util.stream.StreamSupport; import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; @@ -125,6 +131,8 @@ private final GitRepositoryManager repoManager; private final AllUsersName allUsers; private final NotesMigration migration; + private final PatchListCache patchListCache; + private final PatchSetUtil psUtil; private final String serverId; @Inject @@ -132,10 +140,14 @@ GitRepositoryManager repoManager, AllUsersName allUsers, NotesMigration migration, + PatchListCache patchListCache, + PatchSetUtil psUtil, @GerritServerId String serverId) { this.repoManager = repoManager; this.allUsers = allUsers; this.migration = migration; + this.patchListCache = patchListCache; + this.psUtil = psUtil; this.serverId = serverId; } @@ -215,8 +227,7 @@ public List<Comment> publishedByChange(ReviewDb db, ChangeNotes notes) throws OrmException { if (!migration.readChanges()) { - return sort( - byCommentStatus(db.patchComments().byChange(notes.getChangeId()), Status.PUBLISHED)); + return sort(byCommentStatus(db.patchComments().byChange(notes.getChangeId()), PUBLISHED)); } notes.load(); @@ -248,7 +259,7 @@ } private List<Comment> byCommentStatus( - ResultSet<PatchLineComment> comments, final PatchLineComment.Status status) { + ResultSet<PatchLineComment> comments, PatchLineComment.Status status) { return toComments( serverId, Lists.newArrayList(Iterables.filter(comments, c -> c.getStatus() == status))); } @@ -335,7 +346,7 @@ public List<Comment> draftByChangeAuthor(ReviewDb db, ChangeNotes notes, Account.Id author) throws OrmException { if (!migration.readChanges()) { - return StreamSupport.stream(db.patchComments().draftByAuthor(author).spliterator(), false) + return Streams.stream(db.patchComments().draftByAuthor(author)) .filter(c -> c.getPatchSetId().getParentKey().equals(notes.getChangeId())) .map(plc -> plc.asComment(serverId)) .sorted(COMMENT_ORDER) @@ -395,6 +406,31 @@ .delete(toPatchLineComments(update.getId(), PatchLineComment.Status.DRAFT, comments)); } + public void deleteCommentByRewritingHistory( + ReviewDb db, ChangeUpdate update, Comment.Key commentKey, PatchSet.Id psId, String newMessage) + throws OrmException { + if (PrimaryStorage.of(update.getChange()).equals(PrimaryStorage.REVIEW_DB)) { + PatchLineComment.Key key = + new PatchLineComment.Key(new Patch.Key(psId, commentKey.filename), commentKey.uuid); + + if (db instanceof BatchUpdateReviewDb) { + db = ((BatchUpdateReviewDb) db).unsafeGetDelegate(); + } + db = ReviewDbUtil.unwrapDb(db); + + PatchLineComment patchLineComment = db.patchComments().get(key); + + if (!patchLineComment.getStatus().equals(PUBLISHED)) { + throw new OrmException(String.format("comment %s is not published", key)); + } + + patchLineComment.setMessage(newMessage); + db.patchComments().upsert(Collections.singleton(patchLineComment)); + } + + update.deleteCommentByRewritingHistory(commentKey.uuid, newMessage); + } + public void deleteAllDraftsFromAllUsers(Change.Id changeId) throws IOException { try (Repository repo = repoManager.openRepository(allUsers); RevWalk rw = new RevWalk(repo)) { @@ -500,4 +536,35 @@ return COMMENT_ORDER.sortedCopy( FluentIterable.from(comments).transform(plc -> plc.asComment(serverId))); } + + public void publish( + ChangeContext ctx, PatchSet.Id psId, Collection<Comment> drafts, @Nullable String tag) + throws OrmException { + ChangeNotes notes = ctx.getNotes(); + checkArgument(notes != null); + if (drafts.isEmpty()) { + return; + } + + Map<PatchSet.Id, PatchSet> patchSets = + psUtil.getAsMap( + ctx.getDb(), notes, drafts.stream().map(d -> psId(notes, d)).collect(toSet())); + for (Comment d : drafts) { + PatchSet ps = patchSets.get(psId(notes, d)); + if (ps == null) { + throw new OrmException("patch set " + ps + " not found"); + } + d.writtenOn = ctx.getWhen(); + d.tag = tag; + // Draft may have been created by a different real user; copy the current real user. (Only + // applies to X-Gerrit-RunAs, since modifying drafts via on_behalf_of is not allowed.) + ctx.getUser().updateRealAccountId(d::setRealAuthor); + setCommentRevId(d, patchListCache, notes.getChange(), ps); + } + putComments(ctx.getDb(), ctx.getUpdate(psId), PUBLISHED, drafts); + } + + private static PatchSet.Id psId(ChangeNotes notes, Comment c) { + return new PatchSet.Id(notes.getChangeId(), c.key.patchSetId); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java index 029b54d..7ac8fda 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -16,9 +16,8 @@ import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.server.account.CapabilityControl; -import com.google.gerrit.server.account.ExternalId; import com.google.gerrit.server.account.GroupMembership; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.inject.servlet.RequestScoped; import java.util.function.Consumer; @@ -40,16 +39,9 @@ private PropertyKey() {} } - private final CapabilityControl.Factory capabilityControlFactory; private AccessPath accessPath = AccessPath.UNKNOWN; - - private CapabilityControl capabilities; private PropertyKey<ExternalId.Key> lastLoginExternalIdPropertyKey = PropertyKey.create(); - protected CurrentUser(CapabilityControl.Factory capabilityControlFactory) { - this.capabilityControlFactory = capabilityControlFactory; - } - /** How this user is accessing the Gerrit Code Review application. */ public final AccessPath getAccessPath() { return accessPath; @@ -98,14 +90,6 @@ return null; } - /** Capabilities available to this user account. */ - public CapabilityControl getCapabilities() { - if (capabilities == null) { - capabilities = capabilityControlFactory.create(this); - } - return capabilities; - } - /** Check if user is the IdentifiedUser */ public boolean isIdentifiedUser() { return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java new file mode 100644 index 0000000..6267dca --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/DynamicOptions.java
@@ -0,0 +1,173 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 com.google.gerrit.extensions.registration.DynamicMap; +import com.google.gerrit.server.plugins.DelegatingClassLoader; +import com.google.gerrit.util.cli.CmdLineParser; +import com.google.inject.Injector; +import com.google.inject.Provider; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** Helper class to define and parse options from plugins on ssh and RestAPI commands. */ +public class DynamicOptions { + /** + * To provide additional options, bind a DynamicBean. For example: + * + * <pre> + * bind(com.google.gerrit.server.DynamicOptions.DynamicBean.class) + * .annotatedWith(Exports.named(com.google.gerrit.sshd.commands.Query.class)) + * .to(MyOptions.class); + * </pre> + * + * To define the additional options, implement this interface. For example: + * + * <pre> + * public class MyOptions implements DynamicOptions.DynamicBean { + * {@literal @}Option(name = "--verbose", aliases = {"-v"} + * usage = "Make the operation more talkative") + * public boolean verbose; + * } + * </pre> + * + * The option will be prefixed by the plugin name. In the example above, if the plugin name was + * my-plugin, then the --verbose option as used by the caller would be --my-plugin--verbose. + */ + public interface DynamicBean {} + + /** + * Implement this if your DynamicBean needs an opportunity to act on the Bean directly before or + * after argument parsing. + */ + public interface BeanParseListener extends DynamicBean { + void onBeanParseStart(String plugin, Object bean); + + void onBeanParseEnd(String plugin, Object bean); + } + + /** + * The entity which provided additional options may need a way to receive a reference to the + * DynamicBean it provided. To do so, the existing class should implement BeanReceiver (a setter) + * and then provide some way for the plugin to request its DynamicBean (a getter.) For example: + * + * <pre> + * public class Query extends SshCommand implements DynamicOptions.BeanReceiver { + * public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) { + * dynamicBeans.put(plugin, dynamicBean); + * } + * + * public DynamicOptions.DynamicBean getDynamicBean(String plugin) { + * return dynamicBeans.get(plugin); + * } + * ... + * } + * } + * </pre> + */ + public interface BeanReceiver { + void setDynamicBean(String plugin, DynamicBean dynamicBean); + } + + protected Object bean; + protected Map<String, DynamicBean> beansByPlugin; + protected Injector injector; + + /** + * Internal: For Gerrit to include options from DynamicBeans, setup a DynamicMap and instantiate + * this class so the following methods can be called if desired: + * + * <pre> + * DynamicOptions pluginOptions = new DynamicOptions(bean, injector, dynamicBeans); + * pluginOptions.parseDynamicBeans(clp); + * pluginOptions.setDynamicBeans(); + * pluginOptions.onBeanParseStart(); + * + * // parse arguments here: clp.parseArgument(argv); + * + * pluginOptions.onBeanParseEnd(); + * </pre> + */ + public DynamicOptions(Object bean, Injector injector, DynamicMap<DynamicBean> dynamicBeans) { + this.bean = bean; + this.injector = injector; + beansByPlugin = new HashMap<>(); + for (String plugin : dynamicBeans.plugins()) { + Provider<DynamicBean> provider = + dynamicBeans.byPlugin(plugin).get(bean.getClass().getCanonicalName()); + if (provider != null) { + beansByPlugin.put(plugin, getDynamicBean(bean, provider.get())); + } + } + } + + @SuppressWarnings("unchecked") + public DynamicBean getDynamicBean(Object bean, DynamicBean dynamicBean) { + ClassLoader coreCl = getClass().getClassLoader(); + ClassLoader beanCl = bean.getClass().getClassLoader(); + if (beanCl != coreCl) { // bean from a plugin? + ClassLoader dynamicBeanCl = dynamicBean.getClass().getClassLoader(); + if (beanCl != dynamicBeanCl) { // in a different plugin? + ClassLoader mergedCL = new DelegatingClassLoader(beanCl, dynamicBeanCl); + try { + return injector + .createChildInjector() + .getInstance( + (Class<DynamicOptions.DynamicBean>) + mergedCL.loadClass(dynamicBean.getClass().getCanonicalName())); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + return dynamicBean; + } + + public void parseDynamicBeans(CmdLineParser clp) { + for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) { + clp.parseWithPrefix(e.getKey(), e.getValue()); + } + } + + public void setDynamicBeans() { + if (bean instanceof BeanReceiver) { + BeanReceiver receiver = (BeanReceiver) bean; + for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) { + receiver.setDynamicBean(e.getKey(), e.getValue()); + } + } + } + + public void onBeanParseStart() { + for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) { + DynamicBean instance = e.getValue(); + if (instance instanceof BeanParseListener) { + BeanParseListener listener = (BeanParseListener) instance; + listener.onBeanParseStart(e.getKey(), bean); + } + } + } + + public void onBeanParseEnd() { + for (Entry<String, DynamicBean> e : beansByPlugin.entrySet()) { + DynamicBean instance = e.getValue(); + if (instance instanceof BeanParseListener) { + BeanParseListener listener = (BeanParseListener) instance; + listener.onBeanParseEnd(e.getKey(), bean); + } + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/GerritPersonIdentProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/GerritPersonIdentProvider.java index 8c68270..87ba55a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/GerritPersonIdentProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/GerritPersonIdentProvider.java
@@ -29,7 +29,7 @@ private final String email; @Inject - public GerritPersonIdentProvider(@GerritServerConfig final Config cfg) { + public GerritPersonIdentProvider(@GerritServerConfig Config cfg) { String name = cfg.getString("user", null, "name"); if (name == null) { name = "Gerrit Code Review";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java index 2c4c61c..e39dc69 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -21,7 +21,6 @@ import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountState; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.GroupMembership; import com.google.gerrit.server.account.ListGroupMembership; @@ -55,7 +54,6 @@ /** Create an IdentifiedUser, ignoring any per-request state. */ @Singleton public static class GenericFactory { - private final CapabilityControl.Factory capabilityControlFactory; private final AuthConfig authConfig; private final Realm realm; private final String anonymousCowardName; @@ -66,7 +64,6 @@ @Inject public GenericFactory( - @Nullable CapabilityControl.Factory capabilityControlFactory, AuthConfig authConfig, Realm realm, @AnonymousCowardName String anonymousCowardName, @@ -74,7 +71,6 @@ @DisableReverseDnsLookup Boolean disableReverseDnsLookup, AccountCache accountCache, GroupBackend groupBackend) { - this.capabilityControlFactory = capabilityControlFactory; this.authConfig = authConfig; this.realm = realm; this.anonymousCowardName = anonymousCowardName; @@ -86,7 +82,6 @@ public IdentifiedUser create(AccountState state) { return new IdentifiedUser( - capabilityControlFactory, authConfig, realm, anonymousCowardName, @@ -110,7 +105,6 @@ public IdentifiedUser runAs( SocketAddress remotePeer, Account.Id id, @Nullable CurrentUser caller) { return new IdentifiedUser( - capabilityControlFactory, authConfig, realm, anonymousCowardName, @@ -132,7 +126,6 @@ */ @Singleton public static class RequestFactory { - private final CapabilityControl.Factory capabilityControlFactory; private final AuthConfig authConfig; private final Realm realm; private final String anonymousCowardName; @@ -144,7 +137,6 @@ @Inject RequestFactory( - CapabilityControl.Factory capabilityControlFactory, AuthConfig authConfig, Realm realm, @AnonymousCowardName String anonymousCowardName, @@ -153,7 +145,6 @@ GroupBackend groupBackend, @DisableReverseDnsLookup Boolean disableReverseDnsLookup, @RemotePeer Provider<SocketAddress> remotePeerProvider) { - this.capabilityControlFactory = capabilityControlFactory; this.authConfig = authConfig; this.realm = realm; this.anonymousCowardName = anonymousCowardName; @@ -166,7 +157,6 @@ public IdentifiedUser create(Account.Id id) { return new IdentifiedUser( - capabilityControlFactory, authConfig, realm, anonymousCowardName, @@ -181,7 +171,6 @@ public IdentifiedUser runAs(Account.Id id, CurrentUser caller) { return new IdentifiedUser( - capabilityControlFactory, authConfig, realm, anonymousCowardName, @@ -219,7 +208,6 @@ private Map<PropertyKey<Object>, Object> properties; private IdentifiedUser( - CapabilityControl.Factory capabilityControlFactory, AuthConfig authConfig, Realm realm, String anonymousCowardName, @@ -231,7 +219,6 @@ AccountState state, @Nullable CurrentUser realUser) { this( - capabilityControlFactory, authConfig, realm, anonymousCowardName, @@ -246,7 +233,6 @@ } private IdentifiedUser( - CapabilityControl.Factory capabilityControlFactory, AuthConfig authConfig, Realm realm, String anonymousCowardName, @@ -257,7 +243,6 @@ @Nullable Provider<SocketAddress> remotePeerProvider, Account.Id id, @Nullable CurrentUser realUser) { - super(capabilityControlFactory); this.canonicalUrl = canonicalUrl; this.accountCache = accountCache; this.groupBackend = groupBackend; @@ -349,7 +334,7 @@ return newRefLogIdent(new Date(), TimeZone.getDefault()); } - public PersonIdent newRefLogIdent(final Date when, final TimeZone tz) { + public PersonIdent newRefLogIdent(Date when, TimeZone tz) { final Account ua = getAccount(); String name = ua.getFullName(); @@ -369,7 +354,7 @@ return new PersonIdent(name, user + "@" + guessHost(), when, tz); } - public PersonIdent newCommitterIdent(final Date when, final TimeZone tz) { + public PersonIdent newCommitterIdent(Date when, TimeZone tz) { final Account ua = getAccount(); String name = ua.getFullName(); String email = ua.getPreferredEmail(); @@ -465,7 +450,6 @@ * @return copy of the identified user */ public IdentifiedUser materializedCopy() { - CapabilityControl capabilities = getCapabilities(); Provider<SocketAddress> remotePeer; try { remotePeer = Providers.of(remotePeerProvider.get()); @@ -479,13 +463,6 @@ }; } return new IdentifiedUser( - new CapabilityControl.Factory() { - - @Override - public CapabilityControl create(CurrentUser user) { - return capabilities; - } - }, authConfig, realm, anonymousCowardName, @@ -517,7 +494,7 @@ return host; } - private String getHost(final InetAddress in) { + private String getHost(InetAddress in) { if (Boolean.FALSE.equals(disableReverseDnsLookup)) { return in.getCanonicalHostName(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java index bc99ec1..821a0c6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -14,10 +14,7 @@ package com.google.gerrit.server; -import com.google.common.annotations.VisibleForTesting; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.GroupMembership; -import com.google.inject.Inject; /** * User identity for plugin code that needs an identity. @@ -33,12 +30,6 @@ InternalUser create(); } - @VisibleForTesting - @Inject - public InternalUser(CapabilityControl.Factory capabilityControlFactory) { - super(capabilityControlFactory); - } - @Override public GroupMembership getEffectiveGroups() { return GroupMembership.EMPTY;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java index ab942ca..c23d990 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/PatchSetUtil.java
@@ -16,11 +16,17 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static com.google.gerrit.server.ChangeUtil.PS_ID_ORDER; import static com.google.gerrit.server.notedb.PatchSetState.DRAFT; import static com.google.gerrit.server.notedb.PatchSetState.PUBLISHED; +import static java.util.function.Function.identity; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.google.common.collect.Streams; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.RevId; @@ -36,6 +42,7 @@ import java.sql.Timestamp; import java.util.Collections; import java.util.List; +import java.util.Set; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.RevWalk; @@ -63,8 +70,7 @@ public ImmutableCollection<PatchSet> byChange(ReviewDb db, ChangeNotes notes) throws OrmException { if (!migration.readChanges()) { - return ChangeUtil.PS_ID_ORDER.immutableSortedCopy( - db.patchSets().byChange(notes.getChangeId())); + return PS_ID_ORDER.immutableSortedCopy(db.patchSets().byChange(notes.getChangeId())); } return notes.load().getPatchSets().values(); } @@ -73,8 +79,7 @@ throws OrmException { if (!migration.readChanges()) { ImmutableMap.Builder<PatchSet.Id, PatchSet> result = ImmutableMap.builder(); - for (PatchSet ps : - ChangeUtil.PS_ID_ORDER.sortedCopy(db.patchSets().byChange(notes.getChangeId()))) { + for (PatchSet ps : PS_ID_ORDER.sortedCopy(db.patchSets().byChange(notes.getChangeId()))) { result.put(ps.getId(), ps); } return result.build(); @@ -82,6 +87,17 @@ return notes.load().getPatchSets(); } + public ImmutableMap<PatchSet.Id, PatchSet> getAsMap( + ReviewDb db, ChangeNotes notes, Set<PatchSet.Id> patchSetIds) throws OrmException { + if (!migration.readChanges()) { + patchSetIds = Sets.filter(patchSetIds, p -> p.getParentKey().equals(notes.getChangeId())); + return Streams.stream(db.patchSets().get(patchSetIds)) + .sorted(PS_ID_ORDER) + .collect(toImmutableMap(PatchSet::getId, identity())); + } + return ImmutableMap.copyOf(Maps.filterKeys(notes.load().getPatchSets(), patchSetIds::contains)); + } + public PatchSet insert( ReviewDb db, RevWalk rw,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java index 263bb50..8a8b67a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
@@ -14,7 +14,6 @@ package com.google.gerrit.server; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.GroupMembership; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -32,9 +31,7 @@ private final SocketAddress peer; @Inject - protected PeerDaemonUser( - CapabilityControl.Factory capabilityControlFactory, @Assisted SocketAddress peer) { - super(capabilityControlFactory); + protected PeerDaemonUser(@Assisted SocketAddress peer) { this.peer = peer; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java index 13e04c5..09f5043 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java
@@ -14,7 +14,6 @@ package com.google.gerrit.server; -import com.google.gerrit.server.account.CapabilityControl; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -27,9 +26,7 @@ private final String pluginName; @Inject - protected PluginUser( - CapabilityControl.Factory capabilityControlFactory, @Assisted String pluginName) { - super(capabilityControlFactory); + protected PluginUser(@Assisted String pluginName) { this.pluginName = pluginName; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java index f3ab21d..7688f1d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
@@ -32,8 +32,7 @@ * @throws RepositoryNotFoundException the repository of the branch's project does not exist. * @throws IOException error while retrieving the branch from the repository. */ - public static boolean branchExists( - final GitRepositoryManager repoManager, final Branch.NameKey branch) + public static boolean branchExists(final GitRepositoryManager repoManager, Branch.NameKey branch) throws RepositoryNotFoundException, IOException { try (Repository repo = repoManager.openRepository(branch.getParentKey())) { boolean exists = repo.getRefDatabase().exactRef(branch.get()) != null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java b/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java index 72b361c..ea60682 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
@@ -30,7 +30,7 @@ private boolean ran; /** Register a task to be completed after the request ends. */ - public void add(final Runnable task) { + public void add(Runnable task) { synchronized (cleanup) { if (ran) { throw new IllegalStateException("Request has already been cleaned up"); @@ -43,7 +43,7 @@ public void run() { synchronized (cleanup) { ran = true; - for (final Iterator<Runnable> i = cleanup.iterator(); i.hasNext(); ) { + for (Iterator<Runnable> i = cleanup.iterator(); i.hasNext(); ) { try { i.next().run(); } catch (Throwable err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerByEmailSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerByEmailSet.java new file mode 100644 index 0000000..c16c9c8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerByEmailSet.java
@@ -0,0 +1,79 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableTable; +import com.google.common.collect.Table; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.notedb.ReviewerStateInternal; +import java.sql.Timestamp; + +/** + * Set of reviewers on a change that do not have a Gerrit account and were added by email instead. + * + * <p>A given account may appear in multiple states and at different timestamps. No reviewers with + * state {@link ReviewerStateInternal#REMOVED} are ever exposed by this interface. + */ +public class ReviewerByEmailSet { + private static final ReviewerByEmailSet EMPTY = new ReviewerByEmailSet(ImmutableTable.of()); + + public static ReviewerByEmailSet fromTable( + Table<ReviewerStateInternal, Address, Timestamp> table) { + return new ReviewerByEmailSet(table); + } + + public static ReviewerByEmailSet empty() { + return EMPTY; + } + + private final ImmutableTable<ReviewerStateInternal, Address, Timestamp> table; + private ImmutableSet<Address> users; + + private ReviewerByEmailSet(Table<ReviewerStateInternal, Address, Timestamp> table) { + this.table = ImmutableTable.copyOf(table); + } + + public ImmutableSet<Address> all() { + if (users == null) { + // Idempotent and immutable, don't bother locking. + users = ImmutableSet.copyOf(table.columnKeySet()); + } + return users; + } + + public ImmutableSet<Address> byState(ReviewerStateInternal state) { + return table.row(state).keySet(); + } + + public ImmutableTable<ReviewerStateInternal, Address, Timestamp> asTable() { + return table; + } + + @Override + public boolean equals(Object o) { + return (o instanceof ReviewerByEmailSet) && table.equals(((ReviewerByEmailSet) o).table); + } + + @Override + public int hashCode() { + return table.hashCode(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + table; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java index b79f496..2fad708 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewerRecommender.java
@@ -44,6 +44,7 @@ import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; @@ -60,6 +61,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import org.apache.commons.lang.mutable.MutableDouble; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,7 +75,7 @@ new double[] { BASE_REVIEWER_WEIGHT, BASE_OWNER_WEIGHT, BASE_COMMENT_WEIGHT, }; - private static final long PLUGIN_QUERY_TIMEOUT = 500; //ms + private static final long PLUGIN_QUERY_TIMEOUT = 500; // ms private final ChangeQueryBuilder changeQueryBuilder; private final Config config; @@ -108,7 +110,7 @@ SuggestReviewers suggestReviewers, ProjectControl projectControl, List<Account.Id> candidateList) - throws OrmException { + throws OrmException, IOException, ConfigInvalidException { String query = suggestReviewers.getQuery(); double baseWeight = config.getInt("addReviewer", "baseWeight", 1); @@ -196,7 +198,7 @@ } private Map<Account.Id, MutableDouble> baseRankingForEmptyQuery(double baseWeight) - throws OrmException { + throws OrmException, IOException, ConfigInvalidException { // Get the user's last 25 changes, check approvals try { List<ChangeData> result = @@ -225,7 +227,7 @@ private Map<Account.Id, MutableDouble> baseRankingForCandidateList( List<Account.Id> candidates, ProjectControl projectControl, double baseWeight) - throws OrmException { + throws OrmException, IOException, ConfigInvalidException { // Get each reviewer's activity based on number of applied labels // (weighted 10d), number of comments (weighted 0.5d) and number of owned // changes (weighted 1d).
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java index 410dc5c..d3083e8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -56,6 +56,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; public class ReviewersUtil { @Singleton @@ -146,7 +147,7 @@ ProjectControl projectControl, VisibilityControl visibilityControl, boolean excludeGroups) - throws IOException, OrmException { + throws IOException, OrmException, ConfigInvalidException { String query = suggestReviewers.getQuery(); int limit = suggestReviewers.getLimit(); @@ -212,7 +213,7 @@ SuggestReviewers suggestReviewers, ProjectControl projectControl, List<Account.Id> candidateList) - throws OrmException { + throws OrmException, IOException, ConfigInvalidException { try (Timer0.Context ctx = metrics.recommendAccountsLatency.start()) { return reviewerRecommender.suggestReviewers( changeNotes, suggestReviewers, projectControl, candidateList);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java b/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java index 4ab42f3..010ed32 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/Sequences.java
@@ -18,6 +18,11 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.gerrit.metrics.Description; +import com.google.gerrit.metrics.Description.Units; +import com.google.gerrit.metrics.Field; +import com.google.gerrit.metrics.MetricMaker; +import com.google.gerrit.metrics.Timer2; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.config.GerritServerConfig; @@ -32,50 +37,64 @@ import java.util.List; import org.eclipse.jgit.lib.Config; -@SuppressWarnings("deprecation") @Singleton public class Sequences { - public static final String CHANGES = "changes"; + public static final String NAME_CHANGES = "changes"; + + public static int getChangeSequenceGap(Config cfg) { + return cfg.getInt("noteDb", "changes", "initialSequenceGap", 1000); + } + + private enum SequenceType { + CHANGES; + } private final Provider<ReviewDb> db; private final NotesMigration migration; private final RepoSequence changeSeq; + private final Timer2<SequenceType, Boolean> nextIdLatency; @Inject Sequences( @GerritServerConfig Config cfg, - final Provider<ReviewDb> db, + Provider<ReviewDb> db, NotesMigration migration, GitRepositoryManager repoManager, - AllProjectsName allProjects) { + AllProjectsName allProjects, + MetricMaker metrics) { this.db = db; this.migration = migration; - final int gap = cfg.getInt("noteDb", "changes", "initialSequenceGap", 0); - changeSeq = - new RepoSequence( - repoManager, - allProjects, - CHANGES, - new RepoSequence.Seed() { - @Override - public int get() throws OrmException { - return db.get().nextChangeId() + gap; - } - }, - cfg.getInt("noteDb", "changes", "sequenceBatchSize", 20)); + int gap = getChangeSequenceGap(cfg); + @SuppressWarnings("deprecation") + RepoSequence.Seed seed = () -> db.get().nextChangeId() + gap; + int batchSize = cfg.getInt("noteDb", "changes", "sequenceBatchSize", 20); + changeSeq = new RepoSequence(repoManager, allProjects, NAME_CHANGES, seed, batchSize); + + nextIdLatency = + metrics.newTimer( + "sequence/next_id_latency", + new Description("Latency of requesting IDs from repo sequences") + .setCumulative() + .setUnit(Units.MILLISECONDS), + Field.ofEnum(SequenceType.class, "sequence"), + Field.ofBoolean("multiple")); } public int nextChangeId() throws OrmException { if (!migration.readChangeSequence()) { - return db.get().nextChangeId(); + return nextChangeId(db.get()); } - return changeSeq.next(); + try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, false)) { + return changeSeq.next(); + } } public ImmutableList<Integer> nextChangeIds(int count) throws OrmException { if (migration.readChangeSequence()) { - return changeSeq.next(count); + try (Timer2.Context timer = nextIdLatency.start(SequenceType.CHANGES, count > 1)) { + return changeSeq.next(count); + } } if (count == 0) { @@ -85,7 +104,7 @@ List<Integer> ids = new ArrayList<>(count); ReviewDb db = this.db.get(); for (int i = 0; i < count; i++) { - ids.add(db.nextChangeId()); + ids.add(nextChangeId(db)); } return ImmutableList.copyOf(ids); } @@ -94,4 +113,9 @@ public RepoSequence getChangeIdRepoSequence() { return changeSeq; } + + @SuppressWarnings("deprecation") + private static int nextChangeId(ReviewDb db) throws OrmException { + return db.nextChangeId(); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java index 5ca9e19..26cb3e3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -17,7 +17,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toSet; import com.google.auto.value.AutoValue; import com.google.common.base.CharMatcher; @@ -147,6 +146,7 @@ public static final String DEFAULT_LABEL = "star"; public static final String IGNORE_LABEL = "ignore"; + public static final String MUTE_LABEL = "mute"; public static final ImmutableSortedSet<String> DEFAULT_LABELS = ImmutableSortedSet.of(DEFAULT_LABEL); @@ -266,51 +266,6 @@ } } - public Set<Account.Id> byChange(final Change.Id changeId, final String label) - throws OrmException { - try (Repository repo = repoManager.openRepository(allUsers)) { - return getRefNames(repo, RefNames.refsStarredChangesPrefix(changeId)) - .stream() - .map(Account.Id::parse) - .filter(accountId -> hasStar(repo, changeId, accountId, label)) - .collect(toSet()); - } catch (IOException e) { - throw new OrmException( - String.format("Get accounts that starred change %d failed", changeId.get()), e); - } - } - - @Deprecated - // To be used only for IsStarredByLegacyPredicate. - public Set<Change.Id> byAccount(final Account.Id accountId, final String label) - throws OrmException { - try (Repository repo = repoManager.openRepository(allUsers)) { - return getRefNames(repo, RefNames.REFS_STARRED_CHANGES) - .stream() - .filter(refPart -> refPart.endsWith("/" + accountId.get())) - .map(Change.Id::fromRefPart) - .filter(changeId -> hasStar(repo, changeId, accountId, label)) - .collect(toSet()); - } catch (IOException e) { - throw new OrmException( - String.format("Get changes that were starred by %d failed", accountId.get()), e); - } - } - - private boolean hasStar(Repository repo, Change.Id changeId, Account.Id accountId, String label) { - try { - return readLabels(repo, RefNames.refsStarredChanges(changeId, accountId)) - .labels() - .contains(label); - } catch (IOException e) { - log.error( - String.format( - "Cannot query stars by account %d on change %d", accountId.get(), changeId.get()), - e); - return false; - } - } - public ImmutableListMultimap<Account.Id, String> byChangeFromIndex(Change.Id changeId) throws OrmException { Set<String> fields = ImmutableSet.of(ChangeField.ID.getName(), ChangeField.STAR.getName()); @@ -341,6 +296,48 @@ } } + public void ignore(Account.Id accountId, Project.NameKey project, Change.Id changeId) + throws OrmException { + star(accountId, project, changeId, ImmutableSet.of(IGNORE_LABEL), ImmutableSet.of()); + } + + public void unignore(Account.Id accountId, Project.NameKey project, Change.Id changeId) + throws OrmException { + star(accountId, project, changeId, ImmutableSet.of(), ImmutableSet.of(IGNORE_LABEL)); + } + + public boolean isIgnoredBy(Change.Id changeId, Account.Id accountId) throws OrmException { + return getLabels(accountId, changeId).contains(IGNORE_LABEL); + } + + private static String getMuteLabel(Change change) { + return MUTE_LABEL + "/" + change.currentPatchSetId().get(); + } + + public void mute(Account.Id accountId, Project.NameKey project, Change change) + throws OrmException { + star( + accountId, + project, + change.getId(), + ImmutableSet.of(getMuteLabel(change)), + ImmutableSet.of()); + } + + public void unmute(Account.Id accountId, Project.NameKey project, Change change) + throws OrmException { + star( + accountId, + project, + change.getId(), + ImmutableSet.of(), + ImmutableSet.of(getMuteLabel(change))); + } + + public boolean isMutedBy(Change change, Account.Id accountId) throws OrmException { + return getLabels(accountId, change.getId()).contains(getMuteLabel(change)); + } + private static StarRef readLabels(Repository repo, String refName) throws IOException { Ref ref = repo.exactRef(refName); if (ref == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java index 83b6ec6..891dec2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java
@@ -33,7 +33,7 @@ * hex escape (\x00, \x01, ...) or as a C-style escape sequence (\a, \b, \t, \n, \v, \f, or \r). * Backslashes in the input string are doubled (\\). */ - public static String escapeString(final String str) { + public static String escapeString(String str) { // Allocate a buffer big enough to cover the case with a string needed // very excessive escaping without having to reallocate the buffer. final StringBuilder result = new StringBuilder(3 * str.length());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/UrlEncoded.java b/gerrit-server/src/main/java/com/google/gerrit/server/UrlEncoded.java index adad11c..2b7b618 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/UrlEncoded.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/UrlEncoded.java
@@ -25,7 +25,7 @@ public UrlEncoded() {} - public UrlEncoded(final String url) { + public UrlEncoded(String url) { this.url = url; } @@ -37,7 +37,7 @@ separator = '?'; buffer.append(url); } - for (final Map.Entry<String, String> entry : entrySet()) { + for (Map.Entry<String, String> entry : entrySet()) { final String key = entry.getKey(); final String val = entry.getValue(); if (separator != 0) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java index 533ed9d..64a3874 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/WebLinks.java
@@ -181,7 +181,7 @@ * @param project Project name. * @return Links for projects. */ - public List<WebLinkInfo> getProjectLinks(final String project) { + public List<WebLinkInfo> getProjectLinks(String project) { return filterLinks(projectLinks, webLink -> webLink.getProjectWeblink(project)); } @@ -190,7 +190,7 @@ * @param branch Branch name * @return Links for branches. */ - public List<WebLinkInfo> getBranchLinks(final String project, final String branch) { + public List<WebLinkInfo> getBranchLinks(String project, String branch) { return filterLinks(branchLinks, webLink -> webLink.getBranchWebLink(project, branch)); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java index 024c610..492d0e8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/access/ListAccess.java
@@ -20,6 +20,7 @@ import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.GetAccess; import com.google.inject.Inject; import java.io.IOException; @@ -48,11 +49,11 @@ @Override public Map<String, ProjectAccessInfo> apply(TopLevelResource resource) - throws ResourceNotFoundException, ResourceConflictException, IOException { + throws ResourceNotFoundException, ResourceConflictException, IOException, + PermissionBackendException { Map<String, ProjectAccessInfo> access = new TreeMap<>(); for (String p : projects) { - Project.NameKey projectName = new Project.NameKey(p); - access.put(p, getAccess.apply(projectName)); + access.put(p, getAccess.apply(new Project.NameKey(p))); } return access; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java index 0f9ec8d..e61736d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AbstractRealm.java
@@ -18,6 +18,7 @@ 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;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java index d45ecd8..255078a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -14,14 +14,13 @@ package com.google.gerrit.server.account; +import static com.google.common.collect.ImmutableSet.toImmutableSet; + import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; -import com.google.common.collect.ImmutableSet; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.account.externalids.ExternalIds; import com.google.gerrit.server.cache.CacheModule; -import com.google.gerrit.server.query.account.InternalAccountQuery; -import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.Module; import com.google.inject.Provider; @@ -29,7 +28,6 @@ import com.google.inject.TypeLiteral; import com.google.inject.name.Named; import java.util.Collections; -import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutionException; import org.slf4j.Logger; @@ -60,7 +58,7 @@ } @Override - public Set<Account.Id> get(final String email) { + public Set<Account.Id> get(String email) { try { return cache.get(email); } catch (ExecutionException e) { @@ -70,41 +68,29 @@ } @Override - public void evict(final String email) { + public void evict(String email) { if (email != null) { cache.invalidate(email); } } static class Loader extends CacheLoader<String, Set<Account.Id>> { - private final SchemaFactory<ReviewDb> schema; - private final Provider<InternalAccountQuery> accountQueryProvider; + // This must be a provider to prevent a cyclic dependency within Google-internal glue code. + private final Provider<ExternalIds> externalIds; @Inject - Loader(SchemaFactory<ReviewDb> schema, Provider<InternalAccountQuery> accountQueryProvider) { - this.schema = schema; - this.accountQueryProvider = accountQueryProvider; + Loader(Provider<ExternalIds> externalIds) { + this.externalIds = externalIds; } @Override public Set<Account.Id> load(String email) throws Exception { - try (ReviewDb db = schema.open()) { - Set<Account.Id> r = new HashSet<>(); - for (Account a : db.accounts().byPreferredEmail(email)) { - r.add(a.getId()); - } - for (AccountState accountState : accountQueryProvider.get().byEmailPrefix(email)) { - if (accountState - .getExternalIds() - .stream() - .filter(e -> email.equals(e.email())) - .findAny() - .isPresent()) { - r.add(accountState.getAccount().getId()); - } - } - return ImmutableSet.copyOf(r); - } + return externalIds + .get() + .byEmail(email) + .stream() + .map(e -> e.accountId()) + .collect(toImmutableSet()); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java index df6b122..b44de2d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
@@ -42,21 +42,18 @@ @Nullable AccountState getOrNull(Account.Id accountId); - /** - * Returns an {@code AccountState} instance for the given account ID if it is present in the - * cache. - * - * @param accountId ID of the account that should be retrieved - * @return {@code AccountState} instance for the given account ID if it is present in the cache, - * otherwise {@code null} - */ - AccountState getIfPresent(Account.Id accountId); - AccountState getByUsername(String username); + /** + * Evicts the account from the cache and triggers a reindex for it. + * + * @param accountId account ID of the account that should be evicted + * @throws IOException thrown if reindexing fails + */ void evict(Account.Id accountId) throws IOException; void evictByUsername(String username); - void evictAll() throws IOException; + /** Evict all accounts from the cache, but doesn't trigger reindex of all accounts. */ + void evictAllNoReindex(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java index 1828cca..2bda26d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -14,7 +14,7 @@ package com.google.gerrit.server.account; -import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -23,12 +23,12 @@ import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; +import com.google.gerrit.server.account.externalids.ExternalIds; import com.google.gerrit.server.cache.CacheModule; import com.google.gerrit.server.index.account.AccountIndexer; import com.google.gerrit.server.query.account.InternalAccountQuery; @@ -59,7 +59,7 @@ private static final String BYID_NAME = "accounts"; private static final String BYUSER_NAME = "accounts_byname"; - public static Module module(boolean useReviewdb) { + public static Module module() { return new CacheModule() { @Override protected void configure() { @@ -67,7 +67,7 @@ .loader(ByIdLoader.class); cache(BYUSER_NAME, String.class, new TypeLiteral<Optional<Account.Id>>() {}) - .loader(useReviewdb ? ByNameReviewDbLoader.class : ByNameLoader.class); + .loader(ByNameLoader.class); bind(AccountCacheImpl.class); bind(AccountCache.class).to(AccountCacheImpl.class); @@ -111,12 +111,6 @@ } @Override - public AccountState getIfPresent(Account.Id accountId) { - Optional<AccountState> state = byId.getIfPresent(accountId); - return state != null ? state.orElse(missing(accountId)) : null; - } - - @Override public AccountState getByUsername(String username) { try { Optional<Account.Id> id = byName.get(username); @@ -136,11 +130,8 @@ } @Override - public void evictAll() throws IOException { + public void evictAllNoReindex() { byId.invalidateAll(); - for (Account.Id accountId : byId.asMap().keySet()) { - indexer.get().index(accountId); - } } @Override @@ -160,23 +151,29 @@ static class ByIdLoader extends CacheLoader<Account.Id, Optional<AccountState>> { private final SchemaFactory<ReviewDb> schema; + private final Accounts accounts; private final GroupCache groupCache; private final GeneralPreferencesLoader loader; private final LoadingCache<String, Optional<Account.Id>> byName; private final Provider<WatchConfig.Accessor> watchConfig; + private final ExternalIds externalIds; @Inject ByIdLoader( SchemaFactory<ReviewDb> sf, + Accounts accounts, GroupCache groupCache, GeneralPreferencesLoader loader, @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername, - Provider<WatchConfig.Accessor> watchConfig) { + Provider<WatchConfig.Accessor> watchConfig, + ExternalIds externalIds) { + this.accounts = accounts; this.schema = sf; this.groupCache = groupCache; this.loader = loader; this.byName = byUsername; this.watchConfig = watchConfig; + this.externalIds = externalIds; } @Override @@ -194,16 +191,13 @@ } } - private Optional<AccountState> load(final ReviewDb db, final Account.Id who) + private Optional<AccountState> load(ReviewDb db, Account.Id who) throws OrmException, IOException, ConfigInvalidException { - Account account = db.accounts().get(who); + Account account = accounts.get(db, who); if (account == null) { return Optional.empty(); } - Set<ExternalId> externalIds = - ExternalId.from(db.accountExternalIds().byAccount(who).toList()); - Set<AccountGroup.UUID> internalGroups = new HashSet<>(); for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) { final AccountGroup.Id groupId = g.getAccountGroupId(); @@ -223,26 +217,10 @@ return Optional.of( new AccountState( - account, internalGroups, externalIds, watchConfig.get().getProjectWatches(who))); - } - } - - static class ByNameReviewDbLoader extends CacheLoader<String, Optional<Account.Id>> { - private final SchemaFactory<ReviewDb> dbProvider; - - @Inject - public ByNameReviewDbLoader(SchemaFactory<ReviewDb> dbProvider) { - this.dbProvider = dbProvider; - } - - @Override - public Optional<Account.Id> load(String username) throws Exception { - try (ReviewDb db = dbProvider.open()) { - return Optional.ofNullable( - db.accountExternalIds() - .get(new AccountExternalId.Key(SCHEME_USERNAME + ":" + username))) - .map(AccountExternalId::getAccountId); - } + account, + internalGroups, + externalIds.byAccount(who), + watchConfig.get().getProjectWatches(who))); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java new file mode 100644 index 0000000..70ed29e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountConfig.java
@@ -0,0 +1,260 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account; + +import static com.google.common.base.Preconditions.checkState; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.git.ValidationError; +import com.google.gerrit.server.git.VersionedMetaData; +import com.google.gerrit.server.mail.send.OutgoingEmailValidator; +import com.google.gwtorm.server.OrmDuplicateKeyException; +import java.io.IOException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevSort; + +/** + * ‘account.config’ file in the user branch in the All-Users repository that contains the properties + * of the account. + * + * <p>The 'account.config' file is a git config file that has one 'account' section with the + * properties of the account: + * + * <pre> + * [account] + * active = false + * fullName = John Doe + * preferredEmail = john.doe@foo.com + * status = Overloaded with reviews + * </pre> + * + * <p>All keys are optional. This means 'account.config' may not exist on the user branch if no + * properties are set. + * + * <p>Not setting a key and setting a key to an empty string are treated the same way and result in + * a {@code null} value. + * + * <p>If no value for 'active' is specified, by default the account is considered as active. + * + * <p>The commit date of the first commit on the user branch is used as registration date of the + * account. The first commit may be an empty commit (if no properties were set and 'account.config' + * doesn't exist). + */ +public class AccountConfig extends VersionedMetaData implements ValidationError.Sink { + public static final String ACCOUNT_CONFIG = "account.config"; + public static final String ACCOUNT = "account"; + public static final String KEY_ACTIVE = "active"; + public static final String KEY_FULL_NAME = "fullName"; + public static final String KEY_PREFERRED_EMAIL = "preferredEmail"; + public static final String KEY_STATUS = "status"; + + @Nullable private final OutgoingEmailValidator emailValidator; + private final Account.Id accountId; + private final String ref; + + private boolean isLoaded; + private Account account; + private Timestamp registeredOn; + private List<ValidationError> validationErrors; + + public AccountConfig(@Nullable OutgoingEmailValidator emailValidator, Account.Id accountId) { + this.emailValidator = emailValidator; + this.accountId = accountId; + this.ref = RefNames.refsUsers(accountId); + } + + @Override + protected String getRefName() { + return ref; + } + + /** + * Get the loaded account. + * + * @return loaded account. + * @throws IllegalStateException if the account was not loaded yet + */ + public Account getAccount() { + checkLoaded(); + return account; + } + + /** + * Sets the account. This means the loaded account will be overwritten with the given account. + * + * <p>Changing the registration date of an account is not supported. + * + * @param account account that should be set + * @throws IllegalStateException if the account was not loaded yet + */ + public void setAccount(Account account) { + checkLoaded(); + this.account = account; + this.registeredOn = account.getRegisteredOn(); + } + + /** + * Creates a new account. + * + * @return the new account + * @throws OrmDuplicateKeyException if the user branch already exists + */ + public Account getNewAccount() throws OrmDuplicateKeyException { + checkLoaded(); + if (revision != null) { + throw new OrmDuplicateKeyException(String.format("account %s already exists", accountId)); + } + this.registeredOn = TimeUtil.nowTs(); + this.account = new Account(accountId, registeredOn); + return account; + } + + @Override + protected void onLoad() throws IOException, ConfigInvalidException { + if (revision != null) { + rw.markStart(revision); + rw.sort(RevSort.REVERSE); + registeredOn = new Timestamp(rw.next().getCommitTime() * 1000L); + + Config cfg = readConfig(ACCOUNT_CONFIG); + + account = parse(cfg); + } + + isLoaded = true; + } + + private Account parse(Config cfg) { + Account account = new Account(accountId, registeredOn); + account.setActive(cfg.getBoolean(ACCOUNT, null, KEY_ACTIVE, true)); + account.setFullName(get(cfg, KEY_FULL_NAME)); + + String preferredEmail = get(cfg, KEY_PREFERRED_EMAIL); + account.setPreferredEmail(preferredEmail); + if (emailValidator != null && !emailValidator.isValid(preferredEmail)) { + error( + new ValidationError( + ACCOUNT_CONFIG, String.format("Invalid preferred email: %s", preferredEmail))); + } + + account.setStatus(get(cfg, KEY_STATUS)); + return account; + } + + @Override + protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException { + checkLoaded(); + + if (revision != null) { + commit.setMessage("Update account\n"); + } else if (account != null) { + commit.setMessage("Create account\n"); + commit.setAuthor(new PersonIdent(commit.getAuthor(), registeredOn)); + commit.setCommitter(new PersonIdent(commit.getCommitter(), registeredOn)); + } + + Config cfg = readConfig(ACCOUNT_CONFIG); + setActive(cfg, account.isActive()); + set(cfg, KEY_FULL_NAME, account.getFullName()); + set(cfg, KEY_PREFERRED_EMAIL, account.getPreferredEmail()); + set(cfg, KEY_STATUS, account.getStatus()); + saveConfig(ACCOUNT_CONFIG, cfg); + return true; + } + + /** + * Sets/Unsets {@code account.active} in the given config. + * + * <p>{@code account.active} is set to {@code false} if the account is inactive. + * + * <p>If the account is active {@code account.active} is unset since {@code true} is the default + * if this field is missing. + * + * @param cfg the config + * @param value whether the account is active + */ + private static void setActive(Config cfg, boolean value) { + if (!value) { + cfg.setBoolean(ACCOUNT, null, KEY_ACTIVE, false); + } else { + cfg.unset(ACCOUNT, null, KEY_ACTIVE); + } + } + + /** + * Sets/Unsets the given key in the given config. + * + * <p>The key unset if the value is {@code null}. + * + * @param cfg the config + * @param key the key + * @param value the value + */ + private static void set(Config cfg, String key, String value) { + if (!Strings.isNullOrEmpty(value)) { + cfg.setString(ACCOUNT, null, key, value); + } else { + cfg.unset(ACCOUNT, null, key); + } + } + + /** + * Gets the given key from the given config. + * + * <p>Empty values are returned as {@code null} + * + * @param cfg the config + * @param key the key + * @return the value, {@code null} if key was not set or key was set to empty string + */ + private static String get(Config cfg, String key) { + return Strings.emptyToNull(cfg.getString(ACCOUNT, null, key)); + } + + private void checkLoaded() { + checkState(isLoaded, "account not loaded yet"); + } + + /** + * Get the validation errors, if any were discovered during load. + * + * @return list of errors; empty list if there are no errors. + */ + public List<ValidationError> getValidationErrors() { + if (validationErrors != null) { + return ImmutableList.copyOf(validationErrors); + } + return ImmutableList.of(); + } + + @Override + public void error(ValidationError error) { + if (validationErrors == null) { + validationErrors = new ArrayList<>(4); + } + validationErrors.add(error); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java index 88a2411..bb118a3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
@@ -18,12 +18,16 @@ import com.google.gerrit.common.data.PermissionRule; import com.google.gerrit.common.errors.NoSuchGroupException; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.AccountsSection; import com.google.gerrit.server.group.SystemGroupBackend; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ProjectCache; import com.google.inject.Inject; import com.google.inject.Provider; @@ -32,6 +36,7 @@ /** Access control management for one account's access to other accounts. */ public class AccountControl { public static class Factory { + private final PermissionBackend permissionBackend; private final ProjectCache projectCache; private final GroupControl.Factory groupControlFactory; private final Provider<CurrentUser> user; @@ -40,11 +45,13 @@ @Inject Factory( - final ProjectCache projectCache, - final GroupControl.Factory groupControlFactory, - final Provider<CurrentUser> user, - final IdentifiedUser.GenericFactory userFactory, - final AccountVisibility accountVisibility) { + PermissionBackend permissionBackend, + ProjectCache projectCache, + GroupControl.Factory groupControlFactory, + Provider<CurrentUser> user, + IdentifiedUser.GenericFactory userFactory, + AccountVisibility accountVisibility) { + this.permissionBackend = permissionBackend; this.projectCache = projectCache; this.groupControlFactory = groupControlFactory; this.user = user; @@ -54,24 +61,34 @@ public AccountControl get() { return new AccountControl( - projectCache, groupControlFactory, user.get(), userFactory, accountVisibility); + permissionBackend, + projectCache, + groupControlFactory, + user.get(), + userFactory, + accountVisibility); } } private final AccountsSection accountsSection; private final GroupControl.Factory groupControlFactory; + private final PermissionBackend.WithUser perm; private final CurrentUser user; private final IdentifiedUser.GenericFactory userFactory; private final AccountVisibility accountVisibility; + private Boolean viewAll; + AccountControl( - final ProjectCache projectCache, - final GroupControl.Factory groupControlFactory, - final CurrentUser user, - final IdentifiedUser.GenericFactory userFactory, - final AccountVisibility accountVisibility) { + PermissionBackend permissionBackend, + ProjectCache projectCache, + GroupControl.Factory groupControlFactory, + CurrentUser user, + IdentifiedUser.GenericFactory userFactory, + AccountVisibility accountVisibility) { this.accountsSection = projectCache.getAllProjects().getConfig().getAccountsSection(); this.groupControlFactory = groupControlFactory; + this.perm = permissionBackend.user(user); this.user = user; this.userFactory = userFactory; this.accountVisibility = accountVisibility; @@ -99,7 +116,7 @@ * because {@link GroupMembership#getKnownGroups()} may only return a subset of the effective * groups. */ - public boolean canSee(final Account.Id otherUser) { + public boolean canSee(Account.Id otherUser) { return canSee( new OtherUser() { @Override @@ -121,7 +138,7 @@ * because {@link GroupMembership#getKnownGroups()} may only return a subset of the effective * groups. */ - public boolean canSee(final AccountState otherUser) { + public boolean canSee(AccountState otherUser) { return canSee( new OtherUser() { @Override @@ -137,17 +154,16 @@ } private boolean canSee(OtherUser otherUser) { - // Special case: I can always see myself. - if (user.isIdentifiedUser() && user.getAccountId().equals(otherUser.getId())) { + if (accountVisibility == AccountVisibility.ALL) { return true; - } - if (user.getCapabilities().canViewAllAccounts()) { + } else if (user.isIdentifiedUser() && user.getAccountId().equals(otherUser.getId())) { + // I can always see myself. + return true; + } else if (viewAll()) { return true; } switch (accountVisibility) { - case ALL: - return true; case SAME_GROUP: { Set<AccountGroup.UUID> usersGroups = groupsOf(otherUser.getUser()); @@ -178,12 +194,25 @@ } case NONE: break; + case ALL: default: throw new IllegalStateException("Bad AccountVisibility " + accountVisibility); } return false; } + private boolean viewAll() { + if (viewAll == null) { + try { + perm.check(GlobalPermission.VIEW_ALL_ACCOUNTS); + viewAll = true; + } catch (AuthException | PermissionBackendException e) { + viewAll = false; + } + } + return viewAll; + } + private Set<AccountGroup.UUID> groupsOf(IdentifiedUser user) { return user.getEffectiveGroups() .getKnownGroups()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountException.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountException.java index a536c1a..b8b4a9e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountException.java
@@ -18,11 +18,11 @@ public class AccountException extends Exception { private static final long serialVersionUID = 1L; - public AccountException(final String message) { + public AccountException(String message) { super(message); } - public AccountException(final String message, final Throwable why) { + public AccountException(String message, Throwable why) { super(message, why); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java index b5d0463..a6f1e2b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -14,11 +14,8 @@ package com.google.gerrit.server.account; -import static java.util.stream.Collectors.toSet; - import com.google.common.base.Strings; import com.google.gerrit.audit.AuditService; -import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.AccessSection; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.Permission; @@ -29,6 +26,10 @@ import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; +import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gwtorm.server.OrmException; @@ -37,11 +38,15 @@ import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -51,6 +56,8 @@ private static final Logger log = LoggerFactory.getLogger(AccountManager.class); private final SchemaFactory<ReviewDb> schema; + private final Accounts accounts; + private final AccountsUpdate.Server accountsUpdateFactory; private final AccountCache byIdCache; private final AccountByEmailCache byEmailCache; private final Realm realm; @@ -60,11 +67,15 @@ private final AtomicBoolean awaitsFirstAccountCheck; private final AuditService auditService; private final Provider<InternalAccountQuery> accountQueryProvider; + private final ExternalIds externalIds; private final ExternalIdsUpdate.Server externalIdsUpdateFactory; @Inject AccountManager( SchemaFactory<ReviewDb> schema, + @GerritServerConfig Config cfg, + Accounts accounts, + AccountsUpdate.Server accountsUpdateFactory, AccountCache byIdCache, AccountByEmailCache byEmailCache, Realm accountMapper, @@ -73,17 +84,22 @@ ProjectCache projectCache, AuditService auditService, Provider<InternalAccountQuery> accountQueryProvider, + ExternalIds externalIds, ExternalIdsUpdate.Server externalIdsUpdateFactory) { this.schema = schema; + this.accounts = accounts; + this.accountsUpdateFactory = accountsUpdateFactory; this.byIdCache = byIdCache; this.byEmailCache = byEmailCache; this.realm = accountMapper; this.userFactory = userFactory; this.changeUserNameFactory = changeUserNameFactory; this.projectCache = projectCache; - this.awaitsFirstAccountCheck = new AtomicBoolean(true); + this.awaitsFirstAccountCheck = + new AtomicBoolean(cfg.getBoolean("capability", "makeFirstUserAdmin", true)); this.auditService = auditService; this.accountQueryProvider = accountQueryProvider; + this.externalIds = externalIds; this.externalIdsUpdateFactory = externalIdsUpdateFactory; } @@ -111,7 +127,7 @@ who = realm.authenticate(who); try { try (ReviewDb db = schema.open()) { - ExternalId id = findExternalId(db, who.getExternalIdKey()); + ExternalId id = externalIds.get(who.getExternalIdKey()); if (id == null) { // New account, automatically create and return. // @@ -133,14 +149,10 @@ } } - private ExternalId findExternalId(ReviewDb db, ExternalId.Key key) throws OrmException { - return ExternalId.from(db.accountExternalIds().get(key.asAccountExternalIdKey())); - } - private void update(ReviewDb db, AuthRequest who, ExternalId extId) throws OrmException, IOException, ConfigInvalidException { IdentifiedUser user = userFactory.create(extId.accountId()); - Account toUpdate = null; + List<Consumer<Account>> accountUpdates = new ArrayList<>(); // If the email address was modified by the authentication provider, // update our records to match the changed email. @@ -149,23 +161,19 @@ String oldEmail = extId.email(); if (newEmail != null && !newEmail.equals(oldEmail)) { if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) { - toUpdate = load(toUpdate, user.getAccountId(), db); - toUpdate.setPreferredEmail(newEmail); + accountUpdates.add(a -> a.setPreferredEmail(newEmail)); } externalIdsUpdateFactory .create() .replace( - db, - extId, - ExternalId.create(extId.key(), extId.accountId(), newEmail, extId.password())); + extId, ExternalId.create(extId.key(), extId.accountId(), newEmail, extId.password())); } if (!realm.allowsEdit(AccountFieldName.FULL_NAME) && !Strings.isNullOrEmpty(who.getDisplayName()) && !eq(user.getAccount().getFullName(), who.getDisplayName())) { - toUpdate = load(toUpdate, user.getAccountId(), db); - toUpdate.setFullName(who.getDisplayName()); + accountUpdates.add(a -> a.setFullName(who.getDisplayName())); } if (!realm.allowsEdit(AccountFieldName.USER_NAME) @@ -176,27 +184,18 @@ "Not changing already set username %s to %s", user.getUserName(), who.getUserName())); } - if (toUpdate != null) { - db.accounts().update(Collections.singleton(toUpdate)); + if (!accountUpdates.isEmpty()) { + Account account = + accountsUpdateFactory.create().update(db, user.getAccountId(), accountUpdates); + if (account == null) { + throw new OrmException("Account " + user.getAccountId() + " has been deleted"); + } } if (newEmail != null && !newEmail.equals(oldEmail)) { byEmailCache.evict(oldEmail); byEmailCache.evict(newEmail); } - if (toUpdate != null) { - byIdCache.evict(toUpdate.getId()); - } - } - - private Account load(Account toUpdate, Account.Id accountId, ReviewDb db) throws OrmException { - if (toUpdate == null) { - toUpdate = db.accounts().get(accountId); - if (toUpdate == null) { - throw new OrmException("Account " + accountId + " has been deleted"); - } - } - return toUpdate; } private static boolean eq(String a, String b) { @@ -206,24 +205,28 @@ private AuthResult create(ReviewDb db, AuthRequest who) throws OrmException, AccountException, IOException, ConfigInvalidException { Account.Id newId = new Account.Id(db.nextAccountId()); - Account account = new Account(newId, TimeUtil.nowTs()); ExternalId extId = ExternalId.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress()); - account.setFullName(who.getDisplayName()); - account.setPreferredEmail(extId.email()); - boolean isFirstAccount = - awaitsFirstAccountCheck.getAndSet(false) && db.accounts().anyAccounts().toList().isEmpty(); + boolean isFirstAccount = awaitsFirstAccountCheck.getAndSet(false) && !accounts.hasAnyAccount(); + Account account; try { - db.accounts().upsert(Collections.singleton(account)); + AccountsUpdate accountsUpdate = accountsUpdateFactory.create(); + account = + accountsUpdate.insert( + db, + newId, + a -> { + a.setFullName(who.getDisplayName()); + a.setPreferredEmail(extId.email()); + }); - ExternalId existingExtId = - ExternalId.from(db.accountExternalIds().get(extId.key().asAccountExternalIdKey())); + ExternalId existingExtId = externalIds.get(extId.key()); if (existingExtId != null && !existingExtId.accountId().equals(extId.accountId())) { // external ID is assigned to another account, do not overwrite - db.accounts().delete(Collections.singleton(account)); + accountsUpdate.delete(db, account); throw new AccountException( "Cannot assign external ID \"" + extId.key().get() @@ -231,7 +234,7 @@ + newId + "; external ID already in use."); } - externalIdsUpdateFactory.create().upsert(db, extId); + externalIdsUpdateFactory.create().upsert(extId); } 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 @@ -264,7 +267,7 @@ // IdentifiedUser user = userFactory.create(newId); try { - changeUserNameFactory.create(db, user, who.getUserName()).call(); + changeUserNameFactory.create(user, who.getUserName()).call(); } catch (NameAlreadyUsedException e) { String message = "Cannot assign user name \"" @@ -288,7 +291,6 @@ } byEmailCache.evict(account.getPreferredEmail()); - byIdCache.evict(account.getId()); realm.onCreateAccount(who, account); return new AuthResult(newId, extId.key(), true); } @@ -331,8 +333,8 @@ // such an account cannot be used for uploading changes, // this is why the best we can do here is to fail early and cleanup // the database - db.accounts().delete(Collections.singleton(account)); - externalIdsUpdateFactory.create().delete(db, extId); + accountsUpdateFactory.create().delete(db, account); + externalIdsUpdateFactory.create().delete(extId); throw new AccountUserNameException(errorMessage, e); } } @@ -349,7 +351,7 @@ public AuthResult link(Account.Id to, AuthRequest who) throws AccountException, OrmException, IOException, ConfigInvalidException { try (ReviewDb db = schema.open()) { - ExternalId extId = findExternalId(db, who.getExternalIdKey()); + ExternalId extId = externalIds.get(who.getExternalIdKey()); if (extId != null) { if (!extId.accountId().equals(to)) { throw new AccountException("Identity in use by another account"); @@ -358,16 +360,19 @@ } else { externalIdsUpdateFactory .create() - .insert( - db, ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress())); + .insert(ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress())); if (who.getEmailAddress() != null) { - Account a = db.accounts().get(to); - if (a.getPreferredEmail() == null) { - a.setPreferredEmail(who.getEmailAddress()); - db.accounts().update(Collections.singleton(a)); - byIdCache.evict(to); - } + accountsUpdateFactory + .create() + .update( + db, + to, + a -> { + if (a.getPreferredEmail() == null) { + a.setPreferredEmail(who.getEmailAddress()); + } + }); byEmailCache.evict(who.getEmailAddress()); } } @@ -391,25 +396,19 @@ */ public AuthResult updateLink(Account.Id to, AuthRequest who) throws OrmException, AccountException, IOException, ConfigInvalidException { - try (ReviewDb db = schema.open()) { - Collection<ExternalId> filteredExtIdsByScheme = - ExternalId.from(db.accountExternalIds().byAccount(to).toList()) - .stream() - .filter(e -> e.isScheme(who.getExternalIdKey().scheme())) - .collect(toSet()); + Collection<ExternalId> filteredExtIdsByScheme = + externalIds.byAccount(to, who.getExternalIdKey().scheme()); - if (!filteredExtIdsByScheme.isEmpty() - && (filteredExtIdsByScheme.size() > 1 - || !filteredExtIdsByScheme - .stream() - .filter(e -> e.key().equals(who.getExternalIdKey())) - .findAny() - .isPresent())) { - externalIdsUpdateFactory.create().delete(db, filteredExtIdsByScheme); - } - byIdCache.evict(to); - return link(to, who); + if (!filteredExtIdsByScheme.isEmpty() + && (filteredExtIdsByScheme.size() > 1 + || !filteredExtIdsByScheme + .stream() + .filter(e -> e.key().equals(who.getExternalIdKey())) + .findAny() + .isPresent())) { + externalIdsUpdateFactory.create().delete(filteredExtIdsByScheme); } + return link(to, who); } /** @@ -424,22 +423,26 @@ public AuthResult unlink(Account.Id from, AuthRequest who) throws AccountException, OrmException, IOException, ConfigInvalidException { try (ReviewDb db = schema.open()) { - ExternalId extId = findExternalId(db, who.getExternalIdKey()); + ExternalId extId = externalIds.get(who.getExternalIdKey()); if (extId != null) { if (!extId.accountId().equals(from)) { throw new AccountException( "Identity '" + who.getExternalIdKey().get() + "' in use by another account"); } - externalIdsUpdateFactory.create().delete(db, extId); + externalIdsUpdateFactory.create().delete(extId); if (who.getEmailAddress() != null) { - Account a = db.accounts().get(from); - if (a.getPreferredEmail() != null - && a.getPreferredEmail().equals(who.getEmailAddress())) { - a.setPreferredEmail(null); - db.accounts().update(Collections.singleton(a)); - byIdCache.evict(from); - } + accountsUpdateFactory + .create() + .update( + db, + from, + a -> { + if (a.getPreferredEmail() != null + && a.getPreferredEmail().equals(who.getEmailAddress())) { + a.setPreferredEmail(null); + } + }); byEmailCache.evict(who.getEmailAddress()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java index 9803143..7f66b9c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -23,16 +23,19 @@ import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; +import java.io.IOException; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton public class AccountResolver { private final Realm realm; + private final Accounts accounts; private final AccountByEmailCache byEmail; private final AccountCache byId; private final Provider<InternalAccountQuery> accountQueryProvider; @@ -40,10 +43,12 @@ @Inject AccountResolver( Realm realm, + Accounts accounts, AccountByEmailCache byEmail, AccountCache byId, Provider<InternalAccountQuery> accountQueryProvider) { this.realm = realm; + this.accounts = accounts; this.byEmail = byEmail; this.byId = byId; this.accountQueryProvider = accountQueryProvider; @@ -58,7 +63,8 @@ * @return the single account that matches; null if no account matches or there are multiple * candidates. */ - public Account find(ReviewDb db, String nameOrEmail) throws OrmException { + public Account find(ReviewDb db, String nameOrEmail) + throws OrmException, IOException, ConfigInvalidException { Set<Account.Id> r = findAll(db, nameOrEmail); if (r.size() == 1) { return byId.get(r.iterator().next()).getAccount(); @@ -87,7 +93,8 @@ * name ("username"). * @return the accounts that match, empty collection if none. Never null. */ - public Set<Account.Id> findAll(ReviewDb db, String nameOrEmail) throws OrmException { + public Set<Account.Id> findAll(ReviewDb db, String nameOrEmail) + throws OrmException, IOException, ConfigInvalidException { Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(nameOrEmail); if (m.matches()) { Account.Id id = Account.Id.parse(m.group(1)); @@ -115,8 +122,9 @@ return findAllByNameOrEmail(db, nameOrEmail); } - private boolean exists(ReviewDb db, Account.Id id) throws OrmException { - return db.accounts().get(id) != null; + private boolean exists(ReviewDb db, Account.Id id) + throws OrmException, IOException, ConfigInvalidException { + return accounts.get(db, id) != null; } /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java index 4b9b0fb..1eaf34f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
@@ -14,8 +14,8 @@ package com.google.gerrit.server.account; -import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO; -import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; import com.google.common.base.Function; import com.google.common.base.Strings; @@ -28,6 +28,7 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; +import com.google.gerrit.server.account.externalids.ExternalId; import java.util.Collection; import java.util.HashSet; import java.util.Map;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountUserNameException.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountUserNameException.java index 19fd34d..f1a2555 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountUserNameException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountUserNameException.java
@@ -21,7 +21,7 @@ public class AccountUserNameException extends AccountException { private static final long serialVersionUID = 1L; - public AccountUserNameException(final String message, final Throwable why) { + public AccountUserNameException(String message, Throwable why) { super(message, why); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java new file mode 100644 index 0000000..28ed422 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Accounts.java
@@ -0,0 +1,171 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account; + +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.mail.send.OutgoingEmailValidator; +import com.google.gwtorm.server.OrmException; +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.Objects; +import java.util.Set; +import java.util.stream.Stream; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.Repository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Class to access accounts. */ +@Singleton +public class Accounts { + private static final Logger log = LoggerFactory.getLogger(Accounts.class); + + private final boolean readFromGit; + private final GitRepositoryManager repoManager; + private final AllUsersName allUsersName; + private final OutgoingEmailValidator emailValidator; + + @Inject + Accounts( + @GerritServerConfig Config cfg, + GitRepositoryManager repoManager, + AllUsersName allUsersName, + OutgoingEmailValidator emailValidator) { + this.readFromGit = cfg.getBoolean("user", null, "readAccountsFromGit", false); + this.repoManager = repoManager; + this.allUsersName = allUsersName; + this.emailValidator = emailValidator; + } + + public Account get(ReviewDb db, Account.Id accountId) + throws OrmException, IOException, ConfigInvalidException { + if (readFromGit) { + try (Repository repo = repoManager.openRepository(allUsersName)) { + return read(repo, accountId); + } + } + + return db.accounts().get(accountId); + } + + public List<Account> get(ReviewDb db, Collection<Account.Id> accountIds) + throws OrmException, IOException, ConfigInvalidException { + if (readFromGit) { + List<Account> accounts = new ArrayList<>(accountIds.size()); + try (Repository repo = repoManager.openRepository(allUsersName)) { + for (Account.Id accountId : accountIds) { + accounts.add(read(repo, accountId)); + } + } + return accounts; + } + + return db.accounts().get(accountIds).toList(); + } + + /** + * Returns all accounts. + * + * @return all accounts + */ + public List<Account> all(ReviewDb db) throws OrmException, IOException { + if (readFromGit) { + Set<Account.Id> accountIds = allIds(); + List<Account> accounts = new ArrayList<>(accountIds.size()); + try (Repository repo = repoManager.openRepository(allUsersName)) { + for (Account.Id accountId : accountIds) { + try { + accounts.add(read(repo, accountId)); + } catch (Exception e) { + log.error(String.format("Ignoring invalid account %s", accountId.get()), e); + } + } + } + return accounts; + } + + return db.accounts().all().toList(); + } + + /** + * Returns all account IDs. + * + * @return all account IDs + */ + public Set<Account.Id> allIds() throws IOException { + return readUserRefs().collect(toSet()); + } + + /** + * Returns the first n account IDs. + * + * @param n the number of account IDs that should be returned + * @return first n account IDs + */ + public List<Account.Id> firstNIds(int n) throws IOException { + return readUserRefs().sorted(comparing(id -> id.get())).limit(n).collect(toList()); + } + + /** + * Checks if any account exists. + * + * @return {@code true} if at least one account exists, otherwise {@code false} + */ + public boolean hasAnyAccount() throws IOException { + try (Repository repo = repoManager.openRepository(allUsersName)) { + return hasAnyAccount(repo); + } + } + + public static boolean hasAnyAccount(Repository repo) throws IOException { + return readUserRefs(repo).findAny().isPresent(); + } + + private Stream<Account.Id> readUserRefs() throws IOException { + try (Repository repo = repoManager.openRepository(allUsersName)) { + return readUserRefs(repo); + } + } + + private Account read(Repository allUsersRepository, Account.Id accountId) + throws IOException, ConfigInvalidException { + AccountConfig accountConfig = new AccountConfig(emailValidator, accountId); + accountConfig.load(allUsersRepository); + return accountConfig.getAccount(); + } + + private static Stream<Account.Id> readUserRefs(Repository repo) throws IOException { + return repo.getRefDatabase() + .getRefs(RefNames.REFS_USERS) + .values() + .stream() + .map(r -> Account.Id.fromRef(r.getName())) + .filter(Objects::nonNull); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java index 081ea26..1669c4d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
@@ -33,6 +33,8 @@ 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; @Singleton public class AccountsCollection @@ -68,7 +70,8 @@ @Override public AccountResource parse(TopLevelResource root, IdString id) - throws ResourceNotFoundException, AuthException, OrmException { + throws ResourceNotFoundException, AuthException, OrmException, IOException, + ConfigInvalidException { IdentifiedUser user = parseId(id.get()); if (user == null) { throw new ResourceNotFoundException(id); @@ -89,7 +92,8 @@ * account is not visible to the calling user */ public IdentifiedUser parse(String id) - throws AuthException, UnprocessableEntityException, OrmException { + throws AuthException, UnprocessableEntityException, OrmException, IOException, + ConfigInvalidException { return parseOnBehalfOf(null, id); } @@ -104,8 +108,11 @@ * @throws AuthException thrown if 'self' is used as account ID and the current user is not * authenticated * @throws OrmException + * @throws ConfigInvalidException + * @throws IOException */ - public IdentifiedUser parseId(String id) throws AuthException, OrmException { + public IdentifiedUser parseId(String id) + throws AuthException, OrmException, IOException, ConfigInvalidException { return parseIdOnBehalfOf(null, id); } @@ -113,7 +120,8 @@ * Like {@link #parse(String)}, but also sets the {@link CurrentUser#getRealUser()} on the result. */ public IdentifiedUser parseOnBehalfOf(@Nullable CurrentUser caller, String id) - throws AuthException, UnprocessableEntityException, OrmException { + throws AuthException, UnprocessableEntityException, OrmException, IOException, + ConfigInvalidException { IdentifiedUser user = parseIdOnBehalfOf(caller, id); if (user == null) { throw new UnprocessableEntityException(String.format("Account Not Found: %s", id)); @@ -124,7 +132,7 @@ } private IdentifiedUser parseIdOnBehalfOf(@Nullable CurrentUser caller, String id) - throws AuthException, OrmException { + throws AuthException, OrmException, IOException, ConfigInvalidException { if (id.equals("self")) { CurrentUser user = self.get(); if (user.isIdentifiedUser()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java new file mode 100644 index 0000000..2f3f657 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsConsistencyChecker.java
@@ -0,0 +1,67 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account; + +import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +@Singleton +public class AccountsConsistencyChecker { + private final Provider<ReviewDb> dbProvider; + private final Accounts accounts; + private final ExternalIds externalIds; + + @Inject + AccountsConsistencyChecker( + Provider<ReviewDb> dbProvider, Accounts accounts, ExternalIds externalIds) { + this.dbProvider = dbProvider; + this.accounts = accounts; + this.externalIds = externalIds; + } + + public List<ConsistencyProblemInfo> check() throws OrmException, IOException { + List<ConsistencyProblemInfo> problems = new ArrayList<>(); + + for (Account account : accounts.all(dbProvider.get())) { + if (account.getPreferredEmail() != null) { + if (!externalIds + .byAccount(account.getId()) + .stream() + .anyMatch(e -> account.getPreferredEmail().equals(e.email()))) { + addError( + String.format( + "Account '%s' has no external ID for its preferred email '%s'", + account.getId().get(), account.getPreferredEmail()), + problems); + } + } + } + + return problems; + } + + private static void addError(String error, List<ConsistencyProblemInfo> problems) { + problems.add(new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, error)); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java new file mode 100644 index 0000000..ef501ea --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -0,0 +1,446 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.MetaDataUpdate; +import com.google.gerrit.server.index.change.ReindexAfterRefUpdate; +import com.google.gerrit.server.mail.send.OutgoingEmailValidator; +import com.google.gwtorm.server.OrmDuplicateKeyException; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; +import java.sql.Timestamp; +import java.util.List; +import java.util.function.Consumer; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Constants; +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.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.Repository; + +/** + * Updates accounts. + * + * <p>The account updates are written to both ReviewDb and NoteDb. + * + * <p>In NoteDb accounts are represented as user branches in the All-Users repository. Optionally a + * user branch can contain a 'account.config' file that stores account properties, such as full + * name, preferred email, status and the active flag. The timestamp of the first commit on a user + * branch denotes the registration date. The initial commit on the user branch may be empty (since + * having an 'account.config' is optional). See {@link AccountConfig} for details of the + * 'account.config' file format. + * + * <p>On updating accounts the accounts are evicted from the account cache and thus reindexed. The + * eviction from the account cache is done by the {@link ReindexAfterRefUpdate} class which receives + * the event about updating the user branch that is triggered by this class. + */ +@Singleton +public class AccountsUpdate { + /** + * Factory to create an AccountsUpdate instance for updating accounts by the Gerrit server. + * + * <p>The Gerrit server identity will be used as author and committer for all commits that update + * the accounts. + */ + @Singleton + public static class Server { + private final GitRepositoryManager repoManager; + private final GitReferenceUpdated gitRefUpdated; + private final AllUsersName allUsersName; + private final OutgoingEmailValidator emailValidator; + private final Provider<PersonIdent> serverIdent; + private final Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory; + + @Inject + public Server( + GitRepositoryManager repoManager, + GitReferenceUpdated gitRefUpdated, + AllUsersName allUsersName, + OutgoingEmailValidator emailValidator, + @GerritPersonIdent Provider<PersonIdent> serverIdent, + Provider<MetaDataUpdate.Server> metaDataUpdateServerFactory) { + this.repoManager = repoManager; + this.gitRefUpdated = gitRefUpdated; + this.allUsersName = allUsersName; + this.emailValidator = emailValidator; + this.serverIdent = serverIdent; + this.metaDataUpdateServerFactory = metaDataUpdateServerFactory; + } + + public AccountsUpdate create() { + PersonIdent i = serverIdent.get(); + return new AccountsUpdate( + repoManager, + gitRefUpdated, + null, + allUsersName, + emailValidator, + i, + () -> metaDataUpdateServerFactory.get().create(allUsersName)); + } + } + + /** + * Factory to create an AccountsUpdate instance for updating accounts by the current user. + * + * <p>The identity of the current user will be used as author for all commits that update the + * accounts. The Gerrit server identity will be used as committer. + */ + @Singleton + public static class User { + private final GitRepositoryManager repoManager; + private final GitReferenceUpdated gitRefUpdated; + private final AllUsersName allUsersName; + private final OutgoingEmailValidator emailValidator; + private final Provider<PersonIdent> serverIdent; + private final Provider<IdentifiedUser> identifiedUser; + private final Provider<MetaDataUpdate.User> metaDataUpdateUserFactory; + + @Inject + public User( + GitRepositoryManager repoManager, + GitReferenceUpdated gitRefUpdated, + AllUsersName allUsersName, + OutgoingEmailValidator emailValidator, + @GerritPersonIdent Provider<PersonIdent> serverIdent, + Provider<IdentifiedUser> identifiedUser, + Provider<MetaDataUpdate.User> metaDataUpdateUserFactory) { + this.repoManager = repoManager; + this.gitRefUpdated = gitRefUpdated; + this.allUsersName = allUsersName; + this.serverIdent = serverIdent; + this.emailValidator = emailValidator; + this.identifiedUser = identifiedUser; + this.metaDataUpdateUserFactory = metaDataUpdateUserFactory; + } + + public AccountsUpdate create() { + IdentifiedUser user = identifiedUser.get(); + PersonIdent i = serverIdent.get(); + return new AccountsUpdate( + repoManager, + gitRefUpdated, + user, + allUsersName, + emailValidator, + createPersonIdent(i, user), + () -> metaDataUpdateUserFactory.get().create(allUsersName)); + } + + private PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) { + return user.newCommitterIdent(ident.getWhen(), ident.getTimeZone()); + } + } + + private final GitRepositoryManager repoManager; + private final GitReferenceUpdated gitRefUpdated; + @Nullable private final IdentifiedUser currentUser; + private final AllUsersName allUsersName; + private final OutgoingEmailValidator emailValidator; + private final PersonIdent committerIdent; + private final MetaDataUpdateFactory metaDataUpdateFactory; + + private AccountsUpdate( + GitRepositoryManager repoManager, + GitReferenceUpdated gitRefUpdated, + @Nullable IdentifiedUser currentUser, + AllUsersName allUsersName, + OutgoingEmailValidator emailValidator, + PersonIdent committerIdent, + MetaDataUpdateFactory metaDataUpdateFactory) { + this.repoManager = checkNotNull(repoManager, "repoManager"); + this.gitRefUpdated = checkNotNull(gitRefUpdated, "gitRefUpdated"); + this.currentUser = currentUser; + this.allUsersName = checkNotNull(allUsersName, "allUsersName"); + this.emailValidator = checkNotNull(emailValidator, "emailValidator"); + this.committerIdent = checkNotNull(committerIdent, "committerIdent"); + this.metaDataUpdateFactory = checkNotNull(metaDataUpdateFactory, "metaDataUpdateFactory"); + } + + /** + * Inserts a new account. + * + * @param db ReviewDb + * @param accountId ID of the new account + * @param init consumer to populate the new account + * @return the newly created account + * @throws OrmException if updating the database fails + * @throws OrmDuplicateKeyException if the account already exists + * @throws IOException if updating the user branch fails + * @throws ConfigInvalidException if any of the account fields has an invalid value + */ + public Account insert(ReviewDb db, Account.Id accountId, Consumer<Account> init) + throws OrmException, IOException, ConfigInvalidException { + AccountConfig accountConfig = read(accountId); + Account account = accountConfig.getNewAccount(); + init.accept(account); + + // Create in ReviewDb + db.accounts().insert(ImmutableSet.of(account)); + + // Create in NoteDb + commitNew(accountConfig); + return account; + } + + /** + * Gets the account and updates it atomically. + * + * <p>Changing the registration date of an account is not supported. + * + * @param db ReviewDb + * @param accountId ID of the account + * @param consumer consumer to update the account, only invoked if the account exists + * @return the updated account, {@code null} if the account doesn't exist + * @throws OrmException if updating the database fails + * @throws IOException if updating the user branch fails + * @throws ConfigInvalidException if any of the account fields has an invalid value + */ + public Account update(ReviewDb db, Account.Id accountId, Consumer<Account> consumer) + throws OrmException, IOException, ConfigInvalidException { + return update(db, accountId, ImmutableList.of(consumer)); + } + + /** + * Gets the account and updates it atomically. + * + * <p>Changing the registration date of an account is not supported. + * + * @param db ReviewDb + * @param accountId ID of the account + * @param consumers consumers to update the account, only invoked if the account exists + * @return the updated account, {@code null} if the account doesn't exist + * @throws OrmException if updating the database fails + * @throws IOException if updating the user branch fails + * @throws ConfigInvalidException if any of the account fields has an invalid value + */ + public Account update(ReviewDb db, Account.Id accountId, List<Consumer<Account>> consumers) + throws OrmException, IOException, ConfigInvalidException { + if (consumers.isEmpty()) { + return null; + } + + // Update in ReviewDb + db.accounts() + .atomicUpdate( + accountId, + a -> { + consumers.stream().forEach(c -> c.accept(a)); + return a; + }); + + // Update in NoteDb + AccountConfig accountConfig = read(accountId); + Account account = accountConfig.getAccount(); + consumers.stream().forEach(c -> c.accept(account)); + commit(accountConfig); + + return account; + } + + /** + * Replaces the account. + * + * <p>The existing account with the same account ID is overwritten by the given account. Choosing + * to overwrite an account means that any updates that were done to the account by a racing + * request after the account was read are lost. Updates are also lost if the account was read from + * a stale account index. This is why using {@link #update(ReviewDb, + * com.google.gerrit.reviewdb.client.Account.Id, Consumer)} to do an atomic update is always + * preferred. + * + * <p>Changing the registration date of an account is not supported. + * + * @param db ReviewDb + * @param account the new account + * @throws OrmException if updating the database fails + * @throws IOException if updating the user branch fails + * @throws ConfigInvalidException if any of the account fields has an invalid value + * @see #update(ReviewDb, com.google.gerrit.reviewdb.client.Account.Id, Consumer) + */ + public void replace(ReviewDb db, Account account) + throws OrmException, IOException, ConfigInvalidException { + // Update in ReviewDb + db.accounts().update(ImmutableSet.of(account)); + + // Update in NoteDb + AccountConfig accountConfig = read(account.getId()); + accountConfig.setAccount(account); + commit(accountConfig); + } + + /** + * Deletes the account. + * + * @param db ReviewDb + * @param account the account that should be deleted + * @throws OrmException if updating the database fails + * @throws IOException if updating the user branch fails + */ + public void delete(ReviewDb db, Account account) throws OrmException, IOException { + // Delete in ReviewDb + db.accounts().delete(ImmutableSet.of(account)); + + // Delete in NoteDb + deleteUserBranch(account.getId()); + } + + /** + * Deletes the account. + * + * @param db ReviewDb + * @param accountId the ID of the account that should be deleted + * @throws OrmException if updating the database fails + * @throws IOException if updating the user branch fails + */ + public void deleteByKey(ReviewDb db, Account.Id accountId) throws OrmException, IOException { + // Delete in ReviewDb + db.accounts().deleteKeys(ImmutableSet.of(accountId)); + + // Delete in NoteDb + deleteUserBranch(accountId); + } + + public static void createUserBranch( + Repository repo, + Project.NameKey project, + GitReferenceUpdated gitRefUpdated, + @Nullable IdentifiedUser user, + ObjectInserter oi, + PersonIdent committerIdent, + PersonIdent authorIdent, + Account.Id accountId, + Timestamp registeredOn) + throws IOException { + ObjectId id = createInitialEmptyCommit(oi, committerIdent, authorIdent, registeredOn); + + String refName = RefNames.refsUsers(accountId); + RefUpdate ru = repo.updateRef(refName); + ru.setExpectedOldObjectId(ObjectId.zeroId()); + ru.setNewObjectId(id); + ru.setForceUpdate(true); + ru.setRefLogIdent(committerIdent); + ru.setRefLogMessage("Create Account", true); + Result result = ru.update(); + if (result != Result.NEW) { + throw new IOException(String.format("Failed to update ref %s: %s", refName, result.name())); + } + gitRefUpdated.fire(project, ru, user != null ? user.getAccount() : null); + } + + private static ObjectId createInitialEmptyCommit( + ObjectInserter oi, + PersonIdent committerIdent, + PersonIdent authorIdent, + Timestamp registrationDate) + throws IOException { + CommitBuilder cb = new CommitBuilder(); + cb.setTreeId(emptyTree(oi)); + cb.setCommitter(new PersonIdent(committerIdent, registrationDate)); + cb.setAuthor(new PersonIdent(authorIdent, registrationDate)); + cb.setMessage("Create Account"); + ObjectId id = oi.insert(cb); + oi.flush(); + return id; + } + + private static ObjectId emptyTree(ObjectInserter oi) throws IOException { + return oi.insert(Constants.OBJ_TREE, new byte[] {}); + } + + private void deleteUserBranch(Account.Id accountId) throws IOException { + try (Repository repo = repoManager.openRepository(allUsersName)) { + deleteUserBranch(repo, allUsersName, gitRefUpdated, currentUser, committerIdent, accountId); + } + } + + public static void deleteUserBranch( + Repository repo, + Project.NameKey project, + GitReferenceUpdated gitRefUpdated, + @Nullable IdentifiedUser user, + PersonIdent refLogIdent, + Account.Id accountId) + throws IOException { + String refName = RefNames.refsUsers(accountId); + Ref ref = repo.exactRef(refName); + if (ref == null) { + return; + } + + RefUpdate ru = repo.updateRef(refName); + ru.setExpectedOldObjectId(ref.getObjectId()); + ru.setNewObjectId(ObjectId.zeroId()); + ru.setForceUpdate(true); + ru.setRefLogIdent(refLogIdent); + ru.setRefLogMessage("Delete Account", true); + Result result = ru.delete(); + if (result != Result.FORCED) { + throw new IOException(String.format("Failed to delete ref %s: %s", refName, result.name())); + } + gitRefUpdated.fire(project, ru, user != null ? user.getAccount() : null); + } + + private AccountConfig read(Account.Id accountId) throws IOException, ConfigInvalidException { + try (Repository repo = repoManager.openRepository(allUsersName)) { + AccountConfig accountConfig = new AccountConfig(emailValidator, accountId); + accountConfig.load(repo); + return accountConfig; + } + } + + private void commitNew(AccountConfig accountConfig) throws IOException { + // When creating a new account we must allow empty commits so that the user branch gets created + // with an empty commit when no account properties are set and hence no 'account.config' file + // will be created. + commit(accountConfig, true); + } + + private void commit(AccountConfig accountConfig) throws IOException { + commit(accountConfig, false); + } + + private void commit(AccountConfig accountConfig, boolean allowEmptyCommit) throws IOException { + try (MetaDataUpdate md = metaDataUpdateFactory.create()) { + md.setAllowEmpty(allowEmptyCommit); + accountConfig.commit(md); + } + } + + @FunctionalInterface + private static interface MetaDataUpdateFactory { + MetaDataUpdate create() throws IOException; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java index 8c10c73..1c5495f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AddSshKey.java
@@ -30,6 +30,9 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AddSshKey.Input; import com.google.gerrit.server.mail.send.AddKeySender; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.ssh.SshKeyCache; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -50,6 +53,7 @@ } private final Provider<CurrentUser> self; + private final PermissionBackend permissionBackend; private final VersionedAuthorizedKeys.Accessor authorizedKeys; private final SshKeyCache sshKeyCache; private final AddKeySender.Factory addKeyFactory; @@ -57,10 +61,12 @@ @Inject AddSshKey( Provider<CurrentUser> self, + PermissionBackend permissionBackend, VersionedAuthorizedKeys.Accessor authorizedKeys, SshKeyCache sshKeyCache, AddKeySender.Factory addKeyFactory) { this.self = self; + this.permissionBackend = permissionBackend; this.authorizedKeys = authorizedKeys; this.sshKeyCache = sshKeyCache; this.addKeyFactory = addKeyFactory; @@ -68,9 +74,10 @@ @Override public Response<SshKeyInfo> apply(AccountResource rsrc, Input input) - throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException("not allowed to add SSH keys"); + throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException, + PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER); } return apply(rsrc.getUser(), input); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java index d1dd4b0..e654b8d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
@@ -14,9 +14,11 @@ package com.google.gerrit.server.account; -import static com.google.gerrit.server.account.ExternalId.SCHEME_EXTERNAL; -import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT; -import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_EXTERNAL; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO; + +import com.google.gerrit.server.account.externalids.ExternalId; /** * Information for {@link AccountManager#authenticate(AuthRequest)}. @@ -87,7 +89,7 @@ return password; } - public void setPassword(final String pass) { + public void setPassword(String pass) { password = pass; } @@ -95,7 +97,7 @@ return displayName; } - public void setDisplayName(final String name) { + public void setDisplayName(String name) { displayName = name != null && name.length() > 0 ? name : null; } @@ -103,7 +105,7 @@ return emailAddress; } - public void setEmailAddress(final String email) { + public void setEmailAddress(String email) { emailAddress = email != null && email.length() > 0 ? email : null; } @@ -111,7 +113,7 @@ return userName; } - public void setUserName(final String user) { + public void setUserName(String user) { userName = user; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthResult.java index 4aced52..2b1bc96 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthResult.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthResult.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.account; import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.server.account.externalids.ExternalId; /** Result from {@link AccountManager#authenticate(AuthRequest)}. */ public class AuthResult {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java index d35656c..08eecd7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java
@@ -14,7 +14,8 @@ package com.google.gerrit.server.account; -import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission; +import com.google.gerrit.extensions.api.access.PluginPermission; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ChildCollection; @@ -22,7 +23,11 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountResource.Capability; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -30,15 +35,18 @@ @Singleton class Capabilities implements ChildCollection<AccountResource, AccountResource.Capability> { private final Provider<CurrentUser> self; + private final PermissionBackend permissionBackend; private final DynamicMap<RestView<AccountResource.Capability>> views; private final Provider<GetCapabilities> get; @Inject Capabilities( Provider<CurrentUser> self, + PermissionBackend permissionBackend, DynamicMap<RestView<AccountResource.Capability>> views, Provider<GetCapabilities> get) { this.self = self; + this.permissionBackend = permissionBackend; this.views = views; this.get = get; } @@ -50,20 +58,39 @@ @Override public Capability parse(AccountResource parent, IdString id) - throws ResourceNotFoundException, AuthException { - if (self.get() != parent.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException("restricted to administrator"); + throws ResourceNotFoundException, AuthException, PermissionBackendException { + IdentifiedUser target = parent.getUser(); + if (self.get() != target) { + permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER); } - String name = id.get(); - CapabilityControl cap = parent.getUser().getCapabilities(); - if (cap.canPerform(name) - || (cap.canAdministrateServer() && GlobalCapability.isCapability(name))) { - return new AccountResource.Capability(parent.getUser(), name); + GlobalOrPluginPermission perm = parse(id); + if (permissionBackend.user(target).test(perm)) { + return new AccountResource.Capability(target, perm.permissionName()); } throw new ResourceNotFoundException(id); } + private GlobalOrPluginPermission parse(IdString id) throws ResourceNotFoundException { + String name = id.get(); + GlobalOrPluginPermission perm = GlobalPermission.byName(name); + if (perm != null) { + return perm; + } + + int dash = name.lastIndexOf('-'); + if (dash < 0) { + throw new ResourceNotFoundException(id); + } + + String pluginName = name.substring(0, dash); + String capability = name.substring(dash + 1); + if (pluginName.isEmpty() || capability.isEmpty()) { + throw new ResourceNotFoundException(id); + } + return new PluginPermission(pluginName, capability); + } + @Override public DynamicMap<RestView<Capability>> views() { return views;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java index 66d0bf9..496ea1b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -14,155 +14,40 @@ package com.google.gerrit.server.account; -import static com.google.common.base.Predicates.not; - -import com.google.common.base.Predicate; -import com.google.common.collect.FluentIterable; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.PermissionRange; import com.google.gerrit.common.data.PermissionRule; -import com.google.gerrit.common.data.PermissionRule.Action; import com.google.gerrit.server.CurrentUser; -import com.google.gerrit.server.PeerDaemonUser; import com.google.gerrit.server.git.QueueProvider; import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.project.ProjectCache; import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; +import com.google.inject.Singleton; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** Access control management for server-wide capabilities. */ public class CapabilityControl { - public interface Factory { - CapabilityControl create(CurrentUser user); + @Singleton + public static class Factory { + private final ProjectCache projectCache; + + @Inject + Factory(ProjectCache projectCache) { + this.projectCache = projectCache; + } + + public CapabilityControl create(CurrentUser user) { + return new CapabilityControl(projectCache, user); + } } private final CapabilityCollection capabilities; private final CurrentUser user; - private final Map<String, List<PermissionRule>> effective; - private Boolean canAdministrateServer; - private Boolean canEmailReviewers; - - @Inject - CapabilityControl(ProjectCache projectCache, @Assisted CurrentUser currentUser) { + private CapabilityControl(ProjectCache projectCache, CurrentUser currentUser) { capabilities = projectCache.getAllProjects().getCapabilityCollection(); user = currentUser; - effective = new HashMap<>(); - } - - /** Identity of the user the control will compute for. */ - public CurrentUser getUser() { - return user; - } - - /** @return true if the user can administer this server. */ - public boolean canAdministrateServer() { - if (canAdministrateServer == null) { - if (user.getRealUser() != user) { - canAdministrateServer = false; - } else { - canAdministrateServer = - user instanceof PeerDaemonUser - || matchAny(capabilities.administrateServer, ALLOWED_RULE); - } - } - return canAdministrateServer; - } - - /** @return true if the user can create an account for another user. */ - public boolean canCreateAccount() { - return canPerform(GlobalCapability.CREATE_ACCOUNT) || canAdministrateServer(); - } - - /** @return true if the user can create a group. */ - public boolean canCreateGroup() { - return canPerform(GlobalCapability.CREATE_GROUP) || canAdministrateServer(); - } - - /** @return true if the user can create a project. */ - public boolean canCreateProject() { - return canPerform(GlobalCapability.CREATE_PROJECT) || canAdministrateServer(); - } - - /** @return true if the user can email reviewers. */ - public boolean canEmailReviewers() { - if (canEmailReviewers == null) { - canEmailReviewers = - matchAny(capabilities.emailReviewers, ALLOWED_RULE) - || !matchAny(capabilities.emailReviewers, not(ALLOWED_RULE)); - } - return canEmailReviewers; - } - - /** @return true if the user can kill any running task. */ - public boolean canKillTask() { - return canPerform(GlobalCapability.KILL_TASK) || canMaintainServer(); - } - - /** @return true if the user can modify an account for another user. */ - public boolean canModifyAccount() { - return canPerform(GlobalCapability.MODIFY_ACCOUNT) || canAdministrateServer(); - } - - /** @return true if the user can view all accounts. */ - public boolean canViewAllAccounts() { - return canPerform(GlobalCapability.VIEW_ALL_ACCOUNTS) || canAdministrateServer(); - } - - /** @return true if the user can view the server caches. */ - public boolean canViewCaches() { - return canPerform(GlobalCapability.VIEW_CACHES) || canMaintainServer(); - } - - /** @return true if the user can flush the server's caches. */ - public boolean canFlushCaches() { - return canPerform(GlobalCapability.FLUSH_CACHES) || canMaintainServer(); - } - - /** @return true if the user can perform basic server maintenance. */ - public boolean canMaintainServer() { - return canPerform(GlobalCapability.MAINTAIN_SERVER) || canAdministrateServer(); - } - - /** @return true if the user can view open connections. */ - public boolean canViewConnections() { - return canPerform(GlobalCapability.VIEW_CONNECTIONS) || canAdministrateServer(); - } - - /** @return true if the user can view the installed plugins. */ - public boolean canViewPlugins() { - return canPerform(GlobalCapability.VIEW_PLUGINS) || canAdministrateServer(); - } - - /** @return true if the user can view the entire queue. */ - public boolean canViewQueue() { - return canPerform(GlobalCapability.VIEW_QUEUE) || canMaintainServer(); - } - - /** @return true if the user can access the database (with gsql). */ - public boolean canAccessDatabase() { - return canPerform(GlobalCapability.ACCESS_DATABASE); - } - - /** @return true if the user can stream Gerrit events. */ - public boolean canStreamEvents() { - return canPerform(GlobalCapability.STREAM_EVENTS) || canAdministrateServer(); - } - - /** @return true if the user can run the Git garbage collection. */ - public boolean canRunGC() { - return canPerform(GlobalCapability.RUN_GC) || canMaintainServer(); - } - - /** @return true if the user can impersonate another user. */ - public boolean canRunAs() { - return canPerform(GlobalCapability.RUN_AS); } /** @return which priority queue the user's tasks should be submitted to. */ @@ -204,18 +89,15 @@ return QueueProvider.QueueType.INTERACTIVE; } - /** True if the user has this permission. Works only for non labels. */ - public boolean canPerform(String permissionName) { - if (GlobalCapability.ADMINISTRATE_SERVER.equals(permissionName)) { - return canAdministrateServer(); - } - return !access(permissionName).isEmpty(); + /** @return true if the user has a permission rule specifying the range. */ + public boolean hasExplicitRange(String permission) { + return GlobalCapability.hasRange(permission) && !getRules(permission).isEmpty(); } /** The range of permitted values associated with a label permission. */ public PermissionRange getRange(String permission) { if (GlobalCapability.hasRange(permission)) { - return toRange(permission, access(permission)); + return toRange(permission, getRules(permission)); } return null; } @@ -238,14 +120,8 @@ return new PermissionRange(permissionName, min, max); } - /** Rules for the given permission, or the empty list. */ - private List<PermissionRule> access(String permissionName) { - List<PermissionRule> rules = effective.get(permissionName); - if (rules != null) { - return rules; - } - - rules = capabilities.getPermission(permissionName); + private List<PermissionRule> getRules(String permissionName) { + List<PermissionRule> rules = capabilities.getPermission(permissionName); GroupMembership groups = user.getEffectiveGroups(); List<PermissionRule> mine = new ArrayList<>(rules.size()); @@ -254,22 +130,9 @@ mine.add(rule); } } - - if (mine.isEmpty()) { - mine = Collections.emptyList(); - } - effective.put(permissionName, mine); return mine; } - private static final Predicate<PermissionRule> ALLOWED_RULE = r -> r.getAction() == Action.ALLOW; - - private boolean matchAny(Collection<PermissionRule> rules, Predicate<PermissionRule> predicate) { - return user.getEffectiveGroups() - .containsAnyOf( - FluentIterable.from(rules).filter(predicate).transform(r -> r.getGroup().getUUID())); - } - private static boolean match(GroupMembership groups, PermissionRule rule) { return groups.contains(rule.getGroup().getUUID()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java deleted file mode 100644 index 21399f4..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityUtils.java +++ /dev/null
@@ -1,132 +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.server.account; - -import com.google.gerrit.extensions.annotations.CapabilityScope; -import com.google.gerrit.extensions.annotations.RequiresAnyCapability; -import com.google.gerrit.extensions.annotations.RequiresCapability; -import com.google.gerrit.extensions.restapi.AuthException; -import com.google.gerrit.server.CurrentUser; -import com.google.inject.Provider; -import java.lang.annotation.Annotation; -import java.util.Arrays; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class CapabilityUtils { - private static final Logger log = LoggerFactory.getLogger(CapabilityUtils.class); - - public static void checkRequiresCapability( - Provider<CurrentUser> userProvider, String pluginName, Class<?> clazz) throws AuthException { - checkRequiresCapability(userProvider.get(), pluginName, clazz); - } - - public static void checkRequiresCapability(CurrentUser user, String pluginName, Class<?> clazz) - throws AuthException { - RequiresCapability rc = getClassAnnotation(clazz, RequiresCapability.class); - RequiresAnyCapability rac = getClassAnnotation(clazz, RequiresAnyCapability.class); - if (rc != null && rac != null) { - log.error( - String.format( - "Class %s uses both @%s and @%s", - clazz.getName(), - RequiresCapability.class.getSimpleName(), - RequiresAnyCapability.class.getSimpleName())); - throw new AuthException("cannot check capability"); - } - CapabilityControl ctl = user.getCapabilities(); - if (ctl.canAdministrateServer()) { - return; - } - checkRequiresCapability(ctl, pluginName, clazz, rc); - checkRequiresAnyCapability(ctl, pluginName, clazz, rac); - } - - private static void checkRequiresCapability( - CapabilityControl ctl, String pluginName, Class<?> clazz, RequiresCapability rc) - throws AuthException { - if (rc == null) { - return; - } - String capability = resolveCapability(pluginName, rc.value(), rc.scope(), clazz); - if (!ctl.canPerform(capability)) { - throw new AuthException( - String.format("Capability %s is required to access this resource", capability)); - } - } - - private static void checkRequiresAnyCapability( - CapabilityControl ctl, String pluginName, Class<?> clazz, RequiresAnyCapability rac) - throws AuthException { - if (rac == null) { - return; - } - if (rac.value().length == 0) { - log.error( - String.format( - "Class %s uses @%s with no capabilities listed", - clazz.getName(), RequiresAnyCapability.class.getSimpleName())); - throw new AuthException("cannot check capability"); - } - for (String capability : rac.value()) { - capability = resolveCapability(pluginName, capability, rac.scope(), clazz); - if (ctl.canPerform(capability)) { - return; - } - } - throw new AuthException( - "One of the following capabilities is required to access this" - + " resource: " - + Arrays.asList(rac.value())); - } - - private static String resolveCapability( - String pluginName, String capability, CapabilityScope scope, Class<?> clazz) - throws AuthException { - if (pluginName != null - && !"gerrit".equals(pluginName) - && (scope == CapabilityScope.PLUGIN || scope == CapabilityScope.CONTEXT)) { - capability = String.format("%s-%s", pluginName, capability); - } else if (scope == CapabilityScope.PLUGIN) { - log.error( - String.format( - "Class %s uses @%s(scope=%s), but is not within a plugin", - clazz.getName(), - RequiresCapability.class.getSimpleName(), - CapabilityScope.PLUGIN.name())); - throw new AuthException("cannot check capability"); - } - return capability; - } - - /** - * Find an instance of the specified annotation, walking up the inheritance tree if necessary. - * - * @param <T> Annotation type to search for - * @param clazz root class to search, may be null - * @param annotationClass class object of Annotation subclass to search for - * @return the requested annotation or null if none - */ - private static <T extends Annotation> T getClassAnnotation( - Class<?> clazz, Class<T> annotationClass) { - for (; clazz != null; clazz = clazz.getSuperclass()) { - T t = clazz.getAnnotation(annotationClass); - if (t != null) { - return t; - } - } - return null; - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java index 978331e..230e516 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
@@ -14,14 +14,15 @@ package com.google.gerrit.server.account; -import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME; -import static java.util.stream.Collectors.toSet; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.errors.NameAlreadyUsedException; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; import com.google.gerrit.server.ssh.SshKeyCache; import com.google.gwtjsonrpc.common.VoidResult; import com.google.gwtorm.server.OrmDuplicateKeyException; @@ -42,14 +43,14 @@ /** Generic factory to change any user's username. */ public interface Factory { - ChangeUserName create(ReviewDb db, IdentifiedUser user, String newUsername); + ChangeUserName create(IdentifiedUser user, String newUsername); } private final AccountCache accountCache; private final SshKeyCache sshKeyCache; + private final ExternalIds externalIds; private final ExternalIdsUpdate.Server externalIdsUpdateFactory; - private final ReviewDb db; private final IdentifiedUser user; private final String newUsername; @@ -57,14 +58,14 @@ ChangeUserName( AccountCache accountCache, SshKeyCache sshKeyCache, + ExternalIds externalIds, ExternalIdsUpdate.Server externalIdsUpdateFactory, - @Assisted ReviewDb db, @Assisted IdentifiedUser user, @Nullable @Assisted String newUsername) { this.accountCache = accountCache; this.sshKeyCache = sshKeyCache; + this.externalIds = externalIds; this.externalIdsUpdateFactory = externalIdsUpdateFactory; - this.db = db; this.user = user; this.newUsername = newUsername; } @@ -73,11 +74,7 @@ public VoidResult call() throws OrmException, NameAlreadyUsedException, InvalidUserNameException, IOException, ConfigInvalidException { - Collection<ExternalId> old = - ExternalId.from(db.accountExternalIds().byAccount(user.getAccountId()).toList()) - .stream() - .filter(e -> e.isScheme(SCHEME_USERNAME)) - .collect(toSet()); + Collection<ExternalId> old = externalIds.byAccount(user.getAccountId(), SCHEME_USERNAME); if (!old.isEmpty()) { throw new IllegalStateException(USERNAME_CANNOT_BE_CHANGED); } @@ -96,12 +93,11 @@ password = i.password(); } } - externalIdsUpdate.insert(db, ExternalId.create(key, user.getAccountId(), null, password)); + externalIdsUpdate.insert(ExternalId.create(key, user.getAccountId(), null, password)); } catch (OrmDuplicateKeyException dupeErr) { // If we are using this identity, don't report the exception. // - ExternalId other = - ExternalId.from(db.accountExternalIds().get(key.asAccountExternalIdKey())); + ExternalId other = externalIds.get(key); if (other != null && other.accountId().equals(user.getAccountId())) { return VoidResult.INSTANCE; } @@ -114,7 +110,7 @@ // If we have any older user names, remove them. // - externalIdsUpdate.delete(db, old); + externalIdsUpdate.delete(old); for (ExternalId extId : old) { sshKeyCache.evict(extId.key().id()); accountCache.evictByUsername(extId.key().id());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java index 9e7e9a4d..ff367e9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -14,10 +14,10 @@ package com.google.gerrit.server.account; -import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO; import com.google.gerrit.audit.AuditService; -import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.GroupDescriptions; import com.google.gerrit.common.errors.InvalidSshKeyException; @@ -36,9 +36,11 @@ import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; import com.google.gerrit.server.api.accounts.AccountExternalIdCreator; import com.google.gerrit.server.group.GroupsCollection; -import com.google.gerrit.server.index.account.AccountIndexer; import com.google.gerrit.server.mail.send.OutgoingEmailValidator; import com.google.gerrit.server.ssh.SshKeyCache; import com.google.gwtorm.server.OrmDuplicateKeyException; @@ -66,12 +68,14 @@ private final VersionedAuthorizedKeys.Accessor authorizedKeys; private final SshKeyCache sshKeyCache; private final AccountCache accountCache; - private final AccountIndexer indexer; + private final AccountsUpdate.User accountsUpdate; private final AccountByEmailCache byEmailCache; private final AccountLoader.Factory infoLoader; private final DynamicSet<AccountExternalIdCreator> externalIdCreators; private final AuditService auditService; + private final ExternalIds externalIds; private final ExternalIdsUpdate.User externalIdsUpdateFactory; + private final OutgoingEmailValidator validator; private final String username; @Inject @@ -82,12 +86,14 @@ VersionedAuthorizedKeys.Accessor authorizedKeys, SshKeyCache sshKeyCache, AccountCache accountCache, - AccountIndexer indexer, + AccountsUpdate.User accountsUpdate, AccountByEmailCache byEmailCache, AccountLoader.Factory infoLoader, DynamicSet<AccountExternalIdCreator> externalIdCreators, AuditService auditService, + ExternalIds externalIds, ExternalIdsUpdate.User externalIdsUpdateFactory, + OutgoingEmailValidator validator, @Assisted String username) { this.db = db; this.currentUser = currentUser; @@ -95,22 +101,27 @@ this.authorizedKeys = authorizedKeys; this.sshKeyCache = sshKeyCache; this.accountCache = accountCache; - this.indexer = indexer; + this.accountsUpdate = accountsUpdate; this.byEmailCache = byEmailCache; this.infoLoader = infoLoader; this.externalIdCreators = externalIdCreators; this.auditService = auditService; + this.externalIds = externalIds; this.externalIdsUpdateFactory = externalIdsUpdateFactory; + this.validator = validator; this.username = username; } @Override - public Response<AccountInfo> apply(TopLevelResource rsrc, AccountInput input) + public Response<AccountInfo> apply(TopLevelResource rsrc, @Nullable AccountInput input) throws BadRequestException, ResourceConflictException, UnprocessableEntityException, OrmException, IOException, ConfigInvalidException { - if (input == null) { - input = new AccountInput(); - } + return apply(input != null ? input : new AccountInput()); + } + + public Response<AccountInfo> apply(AccountInput input) + throws BadRequestException, ResourceConflictException, UnprocessableEntityException, + OrmException, IOException, ConfigInvalidException { if (input.username != null && !username.equals(input.username)) { throw new BadRequestException("username must match URL"); } @@ -125,16 +136,14 @@ Account.Id id = new Account.Id(db.nextAccountId()); ExternalId extUser = ExternalId.createUsername(username, id, input.httpPassword); - if (db.accountExternalIds().get(extUser.key().asAccountExternalIdKey()) != null) { + if (externalIds.get(extUser.key()) != null) { throw new ResourceConflictException("username '" + username + "' already exists"); } if (input.email != null) { - if (db.accountExternalIds() - .get(ExternalId.Key.create(SCHEME_MAILTO, input.email).asAccountExternalIdKey()) - != null) { + if (externalIds.get(ExternalId.Key.create(SCHEME_MAILTO, input.email)) != null) { throw new UnprocessableEntityException("email '" + input.email + "' already exists"); } - if (!OutgoingEmailValidator.isValid(input.email)) { + if (!validator.isValid(input.email)) { throw new BadRequestException("invalid email address"); } } @@ -147,28 +156,33 @@ ExternalIdsUpdate externalIdsUpdate = externalIdsUpdateFactory.create(); try { - externalIdsUpdate.insert(db, extIds); + externalIdsUpdate.insert(extIds); } catch (OrmDuplicateKeyException duplicateKey) { throw new ResourceConflictException("username '" + username + "' already exists"); } if (input.email != null) { try { - externalIdsUpdate.insert(db, ExternalId.createEmail(id, input.email)); + externalIdsUpdate.insert(ExternalId.createEmail(id, input.email)); } catch (OrmDuplicateKeyException duplicateKey) { try { - externalIdsUpdate.delete(db, extUser); - } catch (IOException | ConfigInvalidException | OrmException cleanupError) { + externalIdsUpdate.delete(extUser); + } catch (IOException | ConfigInvalidException cleanupError) { // Ignored } throw new UnprocessableEntityException("email '" + input.email + "' already exists"); } } - Account a = new Account(id, TimeUtil.nowTs()); - a.setFullName(input.name); - a.setPreferredEmail(input.email); - db.accounts().insert(Collections.singleton(a)); + accountsUpdate + .create() + .insert( + db, + id, + a -> { + a.setFullName(input.name); + a.setPreferredEmail(input.email); + }); for (AccountGroup.Id groupId : groups) { AccountGroupMember m = new AccountGroupMember(new AccountGroupMember.Key(id, groupId)); @@ -188,7 +202,6 @@ accountCache.evictByUsername(username); byEmailCache.evict(input.email); - indexer.index(id); AccountLoader loader = infoLoader.create(true); AccountInfo info = loader.get(id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java index b1a5d3b..56e0c60 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateEmail.java
@@ -32,6 +32,9 @@ import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.mail.send.OutgoingEmailValidator; import com.google.gerrit.server.mail.send.RegisterNewEmailSender; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -50,9 +53,11 @@ private final Provider<CurrentUser> self; private final Realm realm; + private final PermissionBackend permissionBackend; private final AccountManager accountManager; private final RegisterNewEmailSender.Factory registerNewEmailFactory; private final PutPreferred putPreferred; + private final OutgoingEmailValidator validator; private final String email; private final boolean isDevMode; @@ -60,16 +65,20 @@ CreateEmail( Provider<CurrentUser> self, Realm realm, + PermissionBackend permissionBackend, AuthConfig authConfig, AccountManager accountManager, RegisterNewEmailSender.Factory registerNewEmailFactory, PutPreferred putPreferred, + OutgoingEmailValidator validator, @Assisted String email) { this.self = self; this.realm = realm; + this.permissionBackend = permissionBackend; this.accountManager = accountManager; this.registerNewEmailFactory = registerNewEmailFactory; this.putPreferred = putPreferred; + this.validator = validator; this.email = email; this.isDevMode = authConfig.getAuthType() == DEVELOPMENT_BECOME_ANY_ACCOUNT; } @@ -78,23 +87,19 @@ public Response<EmailInfo> apply(AccountResource rsrc, EmailInput input) throws AuthException, BadRequestException, ResourceConflictException, ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException, - IOException, ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("not allowed to add email address"); + IOException, ConfigInvalidException, PermissionBackendException { + if (self.get() != rsrc.getUser() || input.noConfirmation) { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); } if (input == null) { input = new EmailInput(); } - if (!OutgoingEmailValidator.isValid(email)) { + if (!validator.isValid(email)) { throw new BadRequestException("invalid email address"); } - if (input.noConfirmation && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("not allowed to use no_confirmation"); - } - if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) { throw new MethodNotAllowedException("realm does not allow adding emails"); } @@ -105,7 +110,7 @@ public Response<EmailInfo> apply(IdentifiedUser user, EmailInput input) throws AuthException, BadRequestException, ResourceConflictException, ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException, - IOException, ConfigInvalidException { + IOException, ConfigInvalidException, PermissionBackendException { if (input.email != null && !email.equals(input.email)) { throw new BadRequestException("email address must match URL"); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java index 795f1c5..d8e46f4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -37,7 +37,7 @@ } @Override - public boolean allowsEdit(final AccountFieldName field) { + public boolean allowsEdit(AccountFieldName field) { if (authConfig.getAuthType() == AuthType.HTTP) { switch (field) { case USER_NAME: @@ -62,7 +62,7 @@ } @Override - public AuthRequest authenticate(final AuthRequest who) { + public AuthRequest authenticate(AuthRequest who) { if (who.getEmailAddress() == null && who.getLocalUser() != null && emailExpander.canExpand(who.getLocalUser())) { @@ -72,10 +72,10 @@ } @Override - public void onCreateAccount(final AuthRequest who, final Account account) {} + public void onCreateAccount(AuthRequest who, Account account) {} @Override - public Account.Id lookup(final String accountName) { + public Account.Id lookup(String accountName) { if (emailExpander.canExpand(accountName)) { final Set<Account.Id> c = byEmail.get(emailExpander.expand(accountName)); if (1 == c.size()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java index d013120..1fa8ed3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteActive.java
@@ -25,13 +25,13 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.DeleteActive.Input; -import com.google.gwtorm.server.AtomicUpdate; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; +import org.eclipse.jgit.errors.ConfigInvalidException; @RequiresCapability(GlobalCapability.MODIFY_ACCOUNT) @Singleton @@ -39,49 +39,46 @@ public static class Input {} private final Provider<ReviewDb> dbProvider; - private final AccountCache byIdCache; + private final AccountsUpdate.Server accountsUpdate; private final Provider<IdentifiedUser> self; @Inject DeleteActive( - Provider<ReviewDb> dbProvider, AccountCache byIdCache, Provider<IdentifiedUser> self) { + Provider<ReviewDb> dbProvider, + AccountsUpdate.Server accountsUpdate, + Provider<IdentifiedUser> self) { this.dbProvider = dbProvider; - this.byIdCache = byIdCache; + this.accountsUpdate = accountsUpdate; this.self = self; } @Override public Response<?> apply(AccountResource rsrc, Input input) - throws RestApiException, OrmException, IOException { + throws RestApiException, OrmException, IOException, ConfigInvalidException { if (self.get() == rsrc.getUser()) { throw new ResourceConflictException("cannot deactivate own account"); } AtomicBoolean alreadyInactive = new AtomicBoolean(false); - Account a = - dbProvider - .get() - .accounts() - .atomicUpdate( + Account account = + accountsUpdate + .create() + .update( + dbProvider.get(), rsrc.getUser().getAccountId(), - new AtomicUpdate<Account>() { - @Override - public Account update(Account a) { - if (!a.isActive()) { - alreadyInactive.set(true); - } else { - a.setActive(false); - } - return a; + a -> { + if (!a.isActive()) { + alreadyInactive.set(true); + } else { + a.setActive(false); } }); - if (a == null) { + if (account == null) { throw new ResourceNotFoundException("account not found"); } if (alreadyInactive.get()) { throw new ResourceConflictException("account not active"); } - byIdCache.evict(a.getId()); return Response.none(); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java index 794a2c1..ca56eb1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -23,10 +23,14 @@ 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.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.DeleteEmail.Input; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -41,27 +45,31 @@ private final Provider<CurrentUser> self; private final Realm realm; - private final Provider<ReviewDb> dbProvider; + private final PermissionBackend permissionBackend; private final AccountManager accountManager; + private final ExternalIds externalIds; @Inject DeleteEmail( Provider<CurrentUser> self, Realm realm, - Provider<ReviewDb> dbProvider, - AccountManager accountManager) { + PermissionBackend permissionBackend, + AccountManager accountManager, + ExternalIds externalIds) { this.self = self; this.realm = realm; - this.dbProvider = dbProvider; + this.permissionBackend = permissionBackend; this.accountManager = accountManager; + this.externalIds = externalIds; } @Override public Response<?> apply(AccountResource.Email rsrc, Input input) throws AuthException, ResourceNotFoundException, ResourceConflictException, - MethodNotAllowedException, OrmException, IOException, ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("not allowed to delete email address"); + MethodNotAllowedException, OrmException, IOException, ConfigInvalidException, + PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); } return apply(rsrc.getUser(), rsrc.getEmail()); } @@ -74,13 +82,9 @@ } Set<ExternalId> extIds = - dbProvider - .get() - .accountExternalIds() + externalIds .byAccount(user.getAccountId()) - .toList() .stream() - .map(ExternalId::from) .filter(e -> email.equals(e.email())) .collect(toSet()); if (extIds.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java index 42726dc..31679a6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java
@@ -14,74 +14,71 @@ package com.google.gerrit.server.account; -import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; import static java.util.stream.Collectors.toMap; -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.RestApiException; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; +import com.google.inject.Singleton; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.eclipse.jgit.errors.ConfigInvalidException; +@Singleton public class DeleteExternalIds implements RestModifyView<AccountResource, List<String>> { - private final AccountByEmailCache accountByEmailCache; - private final AccountCache accountCache; - private final ExternalIdsUpdate.User externalIdsUpdateFactory; + private final PermissionBackend permissionBackend; + private final AccountManager accountManager; + private final ExternalIds externalIds; private final Provider<CurrentUser> self; - private final Provider<ReviewDb> dbProvider; @Inject DeleteExternalIds( - AccountByEmailCache accountByEmailCache, - AccountCache accountCache, - ExternalIdsUpdate.User externalIdsUpdateFactory, - Provider<CurrentUser> self, - Provider<ReviewDb> dbProvider) { - this.accountByEmailCache = accountByEmailCache; - this.accountCache = accountCache; - this.externalIdsUpdateFactory = externalIdsUpdateFactory; + PermissionBackend permissionBackend, + AccountManager accountManager, + ExternalIds externalIds, + Provider<CurrentUser> self) { + this.permissionBackend = permissionBackend; + this.accountManager = accountManager; + this.externalIds = externalIds; this.self = self; - this.dbProvider = dbProvider; } @Override - public Response<?> apply(AccountResource resource, List<String> externalIds) - throws RestApiException, IOException, OrmException, ConfigInvalidException { + public Response<?> apply(AccountResource resource, List<String> extIds) + throws RestApiException, IOException, OrmException, ConfigInvalidException, + PermissionBackendException { if (self.get() != resource.getUser()) { - throw new AuthException("not allowed to delete external IDs"); + permissionBackend.user(self).check(GlobalPermission.ACCESS_DATABASE); } - if (externalIds == null || externalIds.size() == 0) { + if (extIds == null || extIds.size() == 0) { throw new BadRequestException("external IDs are required"); } - Account.Id accountId = resource.getUser().getAccountId(); Map<ExternalId.Key, ExternalId> externalIdMap = - dbProvider - .get() - .accountExternalIds() + externalIds .byAccount(resource.getUser().getAccountId()) - .toList() .stream() - .map(ExternalId::from) .collect(toMap(i -> i.key(), i -> i)); List<ExternalId> toDelete = new ArrayList<>(); ExternalId.Key last = resource.getUser().getLastLoginExternalIdKey(); - for (String externalIdStr : externalIds) { + for (String externalIdStr : extIds) { ExternalId id = externalIdMap.get(ExternalId.Key.parse(externalIdStr)); if (id == null) { @@ -98,12 +95,14 @@ } } - if (!toDelete.isEmpty()) { - externalIdsUpdateFactory.create().delete(dbProvider.get(), toDelete); - accountCache.evict(accountId); - for (ExternalId e : toDelete) { - accountByEmailCache.evict(e.email()); + try { + for (ExternalId extId : toDelete) { + AuthRequest authRequest = new AuthRequest(extId.key()); + authRequest.setEmailAddress(extId.email()); + accountManager.unlink(extId.accountId(), authRequest); } + } catch (AccountException e) { + throw new ResourceConflictException(e.getMessage()); } return Response.none();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java index 3d5d38e..f1ecd29 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteSshKey.java
@@ -19,6 +19,9 @@ import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.DeleteSshKey.Input; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.ssh.SshKeyCache; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -33,15 +36,18 @@ public static class Input {} private final Provider<CurrentUser> self; + private final PermissionBackend permissionBackend; private final VersionedAuthorizedKeys.Accessor authorizedKeys; private final SshKeyCache sshKeyCache; @Inject DeleteSshKey( Provider<CurrentUser> self, + PermissionBackend permissionBackend, VersionedAuthorizedKeys.Accessor authorizedKeys, SshKeyCache sshKeyCache) { this.self = self; + this.permissionBackend = permissionBackend; this.authorizedKeys = authorizedKeys; this.sshKeyCache = sshKeyCache; } @@ -49,9 +55,9 @@ @Override public Response<?> apply(AccountResource.SshKey rsrc, Input input) throws AuthException, OrmException, RepositoryNotFoundException, IOException, - ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException("not allowed to delete SSH keys"); + ConfigInvalidException, PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER); } authorizedKeys.deleteKey(rsrc.getUser().getAccountId(), rsrc.getSshKey().getKey().get());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java index 97102a2..1666eb1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
@@ -25,6 +25,9 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -37,13 +40,18 @@ public class DeleteWatchedProjects implements RestModifyView<AccountResource, List<ProjectWatchInfo>> { private final Provider<IdentifiedUser> self; + private final PermissionBackend permissionBackend; private final AccountCache accountCache; private final WatchConfig.Accessor watchConfig; @Inject DeleteWatchedProjects( - Provider<IdentifiedUser> self, AccountCache accountCache, WatchConfig.Accessor watchConfig) { + Provider<IdentifiedUser> self, + PermissionBackend permissionBackend, + AccountCache accountCache, + WatchConfig.Accessor watchConfig) { this.self = self; + this.permissionBackend = permissionBackend; this.accountCache = accountCache; this.watchConfig = watchConfig; } @@ -51,9 +59,9 @@ @Override public Response<?> apply(AccountResource rsrc, List<ProjectWatchInfo> input) throws AuthException, UnprocessableEntityException, OrmException, IOException, - ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException("It is not allowed to edit project watches of other users"); + ConfigInvalidException, PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER); } if (input == null) { return Response.none();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java index 3c501e9..af2ab19 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/EmailExpander.java
@@ -23,7 +23,7 @@ class None implements EmailExpander { public static final None INSTANCE = new None(); - public static boolean canHandle(final String fmt) { + public static boolean canHandle(String fmt) { return fmt == null || fmt.isEmpty(); } @@ -43,26 +43,26 @@ class Simple implements EmailExpander { private static final String PLACEHOLDER = "{0}"; - public static boolean canHandle(final String fmt) { + public static boolean canHandle(String fmt) { return fmt != null && fmt.contains(PLACEHOLDER); } private final String lhs; private final String rhs; - public Simple(final String fmt) { + public Simple(String fmt) { final int p = fmt.indexOf(PLACEHOLDER); lhs = fmt.substring(0, p); rhs = fmt.substring(p + PLACEHOLDER.length()); } @Override - public boolean canExpand(final String user) { + public boolean canExpand(String user) { return !user.contains(" "); } @Override - public String expand(final String user) { + public String expand(String user) { return lhs + user + rhs; } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java index b894f56..e31f481 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Emails.java
@@ -17,12 +17,16 @@ import com.google.common.base.Strings; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.AcceptsCreate; +import com.google.gerrit.extensions.restapi.AuthException; 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.RestView; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.AccountResource.Email; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -34,6 +38,7 @@ private final DynamicMap<RestView<AccountResource.Email>> views; private final GetEmails list; private final Provider<CurrentUser> self; + private final PermissionBackend permissionBackend; private final CreateEmail.Factory createEmailFactory; @Inject @@ -41,10 +46,12 @@ DynamicMap<RestView<AccountResource.Email>> views, GetEmails list, Provider<CurrentUser> self, + PermissionBackend permissionBackend, CreateEmail.Factory createEmailFactory) { this.views = views; this.list = list; this.self = self; + this.permissionBackend = permissionBackend; this.createEmailFactory = createEmailFactory; } @@ -55,21 +62,21 @@ @Override public AccountResource.Email parse(AccountResource rsrc, IdString id) - throws ResourceNotFoundException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new ResourceNotFoundException(); + throws ResourceNotFoundException, PermissionBackendException, AuthException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER); } if ("preferred".equals(id.get())) { String email = rsrc.getUser().getAccount().getPreferredEmail(); if (Strings.isNullOrEmpty(email)) { - throw new ResourceNotFoundException(); + throw new ResourceNotFoundException(id); } return new AccountResource.Email(rsrc.getUser(), email); } else if (rsrc.getUser().hasEmailAddress(id.get())) { return new AccountResource.Email(rsrc.getUser(), id.get()); } else { - throw new ResourceNotFoundException(); + throw new ResourceNotFoundException(id); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIds.java deleted file mode 100644 index c937935..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIds.java +++ /dev/null
@@ -1,105 +0,0 @@ -// Copyright (C) 2016 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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.account; - -import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; - -import com.google.gerrit.common.Nullable; -import com.google.gerrit.reviewdb.client.RefNames; -import com.google.gerrit.server.config.AllUsersName; -import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.inject.Inject; -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.Ref; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.notes.NoteMap; -import org.eclipse.jgit.revwalk.RevWalk; - -/** - * Class to read external IDs from NoteDb. - * - * <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called - * refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content - * is a git config file that contains an external ID. It has exactly one externalId subsection with - * an accountId and optionally email and password: - * - * <pre> - * [externalId "username:jdoe"] - * accountId = 1003407 - * email = jdoe@example.com - * password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7 - * </pre> - */ -@Singleton -public class ExternalIds { - public static final int MAX_NOTE_SZ = 1 << 19; - - public static ObjectId readRevision(Repository repo) throws IOException { - Ref ref = repo.exactRef(RefNames.REFS_EXTERNAL_IDS); - return ref != null ? ref.getObjectId() : ObjectId.zeroId(); - } - - public static NoteMap readNoteMap(RevWalk rw, ObjectId rev) throws IOException { - if (!rev.equals(ObjectId.zeroId())) { - return NoteMap.read(rw.getObjectReader(), rw.parseCommit(rev)); - } - return NoteMap.newEmptyMap(); - } - - private final GitRepositoryManager repoManager; - private final AllUsersName allUsersName; - - @Inject - public ExternalIds(GitRepositoryManager repoManager, AllUsersName allUsersName) { - this.repoManager = repoManager; - this.allUsersName = allUsersName; - } - - public ObjectId readRevision() throws IOException { - try (Repository repo = repoManager.openRepository(allUsersName)) { - return readRevision(repo); - } - } - - /** Reads and returns the specified external ID. */ - @Nullable - public ExternalId get(ExternalId.Key key) throws IOException, ConfigInvalidException { - try (Repository repo = repoManager.openRepository(allUsersName); - RevWalk rw = new RevWalk(repo)) { - ObjectId rev = readRevision(repo); - if (rev.equals(ObjectId.zeroId())) { - return null; - } - - return parse(key, rw, rev); - } - } - - private ExternalId parse(ExternalId.Key key, RevWalk rw, ObjectId rev) - throws IOException, ConfigInvalidException { - NoteMap noteMap = readNoteMap(rw, rev); - ObjectId noteId = key.sha1(); - if (!noteMap.contains(noteId)) { - return null; - } - - byte[] raw = - rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ); - return ExternalId.parse(noteId.name(), raw); - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdsUpdate.java deleted file mode 100644 index 7746709..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdsUpdate.java +++ /dev/null
@@ -1,667 +0,0 @@ -// Copyright (C) 2016 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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.account; - -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import static com.google.gerrit.server.account.ExternalId.Key.toAccountExternalIdKeys; -import static com.google.gerrit.server.account.ExternalId.toAccountExternalIds; -import static com.google.gerrit.server.account.ExternalIds.MAX_NOTE_SZ; -import static com.google.gerrit.server.account.ExternalIds.readNoteMap; -import static com.google.gerrit.server.account.ExternalIds.readRevision; -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.stream.Collectors.toSet; -import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; -import static org.eclipse.jgit.lib.Constants.OBJ_TREE; - -import com.github.rholder.retry.RetryException; -import com.github.rholder.retry.Retryer; -import com.github.rholder.retry.RetryerBuilder; -import com.github.rholder.retry.StopStrategies; -import com.github.rholder.retry.WaitStrategies; -import com.google.auto.value.AutoValue; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Throwables; -import com.google.common.collect.Iterables; -import com.google.common.util.concurrent.Runnables; -import com.google.gerrit.common.Nullable; -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.RefNames; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.GerritPersonIdent; -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.git.LockFailureException; -import com.google.gwtorm.server.OrmDuplicateKeyException; -import com.google.gwtorm.server.OrmException; -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.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.lib.CommitBuilder; -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.RefUpdate; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.notes.NoteMap; -import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.revwalk.RevWalk; - -/** - * Updates externalIds in ReviewDb and NoteDb. - * - * <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called - * refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content - * is a git config file that contains an external ID. It has exactly one externalId subsection with - * an accountId and optionally email and password: - * - * <pre> - * [externalId "username:jdoe"] - * accountId = 1003407 - * email = jdoe@example.com - * password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7 - * </pre> - * - * For NoteDb each method call results in one commit on refs/meta/external-ids branch. - * - * <p>On updating external IDs this class takes care to evict affected accounts from the account - * cache and thus triggers reindex for them. - */ -public class ExternalIdsUpdate { - private static final String COMMIT_MSG = "Update external IDs"; - - /** - * Factory to create an ExternalIdsUpdate instance for updating external IDs by the Gerrit server. - * - * <p>The Gerrit server identity will be used as author and committer for all commits that update - * the external IDs. - */ - @Singleton - public static class Server { - private final GitRepositoryManager repoManager; - private final AccountCache accountCache; - private final AllUsersName allUsersName; - private final Provider<PersonIdent> serverIdent; - - @Inject - public Server( - GitRepositoryManager repoManager, - AccountCache accountCache, - AllUsersName allUsersName, - @GerritPersonIdent Provider<PersonIdent> serverIdent) { - this.repoManager = repoManager; - this.accountCache = accountCache; - this.allUsersName = allUsersName; - this.serverIdent = serverIdent; - } - - public ExternalIdsUpdate create() { - PersonIdent i = serverIdent.get(); - return new ExternalIdsUpdate(repoManager, accountCache, allUsersName, i, i); - } - } - - /** - * Factory to create an ExternalIdsUpdate instance for updating external IDs by the current user. - * - * <p>The identity of the current user will be used as author for all commits that update the - * external IDs. The Gerrit server identity will be used as committer. - */ - @Singleton - public static class User { - private final GitRepositoryManager repoManager; - private final AccountCache accountCache; - private final AllUsersName allUsersName; - private final Provider<PersonIdent> serverIdent; - private final Provider<IdentifiedUser> identifiedUser; - - @Inject - public User( - GitRepositoryManager repoManager, - AccountCache accountCache, - AllUsersName allUsersName, - @GerritPersonIdent Provider<PersonIdent> serverIdent, - Provider<IdentifiedUser> identifiedUser) { - this.repoManager = repoManager; - this.accountCache = accountCache; - this.allUsersName = allUsersName; - this.serverIdent = serverIdent; - this.identifiedUser = identifiedUser; - } - - public ExternalIdsUpdate create() { - PersonIdent i = serverIdent.get(); - return new ExternalIdsUpdate( - repoManager, accountCache, allUsersName, createPersonIdent(i, identifiedUser.get()), i); - } - - private PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) { - return user.newCommitterIdent(ident.getWhen(), ident.getTimeZone()); - } - } - - @VisibleForTesting - public static RetryerBuilder<Void> retryerBuilder() { - return RetryerBuilder.<Void>newBuilder() - .retryIfException(e -> e instanceof LockFailureException) - .withWaitStrategy( - WaitStrategies.join( - WaitStrategies.exponentialWait(2, TimeUnit.SECONDS), - WaitStrategies.randomWait(50, TimeUnit.MILLISECONDS))) - .withStopStrategy(StopStrategies.stopAfterDelay(10, TimeUnit.SECONDS)); - } - - private static final Retryer<Void> RETRYER = retryerBuilder().build(); - - private final GitRepositoryManager repoManager; - private final AccountCache accountCache; - private final AllUsersName allUsersName; - private final PersonIdent committerIdent; - private final PersonIdent authorIdent; - private final Runnable afterReadRevision; - private final Retryer<Void> retryer; - - private ExternalIdsUpdate( - GitRepositoryManager repoManager, - AccountCache accountCache, - AllUsersName allUsersName, - PersonIdent committerIdent, - PersonIdent authorIdent) { - this( - repoManager, - accountCache, - allUsersName, - committerIdent, - authorIdent, - Runnables.doNothing(), - RETRYER); - } - - @VisibleForTesting - public ExternalIdsUpdate( - GitRepositoryManager repoManager, - AccountCache accountCache, - AllUsersName allUsersName, - PersonIdent committerIdent, - PersonIdent authorIdent, - Runnable afterReadRevision, - Retryer<Void> retryer) { - this.repoManager = checkNotNull(repoManager, "repoManager"); - this.accountCache = accountCache; - this.allUsersName = checkNotNull(allUsersName, "allUsersName"); - this.committerIdent = checkNotNull(committerIdent, "committerIdent"); - this.authorIdent = checkNotNull(authorIdent, "authorIdent"); - this.afterReadRevision = checkNotNull(afterReadRevision, "afterReadRevision"); - this.retryer = checkNotNull(retryer, "retryer"); - } - - /** - * Inserts a new external ID. - * - * <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}. - */ - public void insert(ReviewDb db, ExternalId extId) - throws IOException, ConfigInvalidException, OrmException { - insert(db, Collections.singleton(extId)); - } - - /** - * Inserts new external IDs. - * - * <p>If any of the external ID already exists, the insert fails with {@link - * OrmDuplicateKeyException}. - */ - public void insert(ReviewDb db, Collection<ExternalId> extIds) - throws IOException, ConfigInvalidException, OrmException { - db.accountExternalIds().insert(toAccountExternalIds(extIds)); - - updateNoteMap( - o -> { - for (ExternalId extId : extIds) { - insert(o.rw(), o.ins(), o.noteMap(), extId); - } - }); - evictAccounts(extIds); - } - - /** - * Inserts or updates an external ID. - * - * <p>If the external ID already exists, it is overwritten, otherwise it is inserted. - */ - public void upsert(ReviewDb db, ExternalId extId) - throws IOException, ConfigInvalidException, OrmException { - upsert(db, Collections.singleton(extId)); - } - - /** - * Inserts or updates external IDs. - * - * <p>If any of the external IDs already exists, it is overwritten. New external IDs are inserted. - */ - public void upsert(ReviewDb db, Collection<ExternalId> extIds) - throws IOException, ConfigInvalidException, OrmException { - db.accountExternalIds().upsert(toAccountExternalIds(extIds)); - - updateNoteMap( - o -> { - for (ExternalId extId : extIds) { - upsert(o.rw(), o.ins(), o.noteMap(), extId); - } - }); - evictAccounts(extIds); - } - - /** - * Deletes an external ID. - * - * <p>The deletion fails with {@link IllegalStateException} if there is an existing external ID - * that has the same key, but otherwise doesn't match the specified external ID. - */ - public void delete(ReviewDb db, ExternalId extId) - throws IOException, ConfigInvalidException, OrmException { - delete(db, Collections.singleton(extId)); - } - - /** - * Deletes external IDs. - * - * <p>The deletion fails with {@link IllegalStateException} if there is an existing external ID - * that has the same key as any of the external IDs that should be deleted, but otherwise doesn't - * match the that external ID. - */ - public void delete(ReviewDb db, Collection<ExternalId> extIds) - throws IOException, ConfigInvalidException, OrmException { - db.accountExternalIds().delete(toAccountExternalIds(extIds)); - - updateNoteMap( - o -> { - for (ExternalId extId : extIds) { - remove(o.rw(), o.noteMap(), extId); - } - }); - evictAccounts(extIds); - } - - /** - * Delete an external ID by key. - * - * <p>The external ID is only deleted if it belongs to the specified account. If it belongs to - * another account the deletion fails with {@link IllegalStateException}. - */ - public void delete(ReviewDb db, Account.Id accountId, ExternalId.Key extIdKey) - throws IOException, ConfigInvalidException, OrmException { - delete(db, accountId, Collections.singleton(extIdKey)); - } - - /** - * Delete external IDs by external ID key. - * - * <p>The external IDs are only deleted if they belongs to the specified account. If any of the - * external IDs belongs to another account the deletion fails with {@link IllegalStateException}. - */ - public void delete(ReviewDb db, Account.Id accountId, Collection<ExternalId.Key> extIdKeys) - throws IOException, ConfigInvalidException, OrmException { - db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(extIdKeys)); - - updateNoteMap( - o -> { - for (ExternalId.Key extIdKey : extIdKeys) { - remove(o.rw(), o.noteMap(), accountId, extIdKey); - } - }); - accountCache.evict(accountId); - } - - /** Deletes all external IDs of the specified account. */ - public void deleteAll(ReviewDb db, Account.Id accountId) - throws IOException, ConfigInvalidException, OrmException { - delete(db, ExternalId.from(db.accountExternalIds().byAccount(accountId).toList())); - } - - /** - * Replaces external IDs for an account by external ID keys. - * - * <p>Deletion of external IDs is done before adding the new external IDs. This means if an - * external ID key is specified for deletion and an external ID with the same key is specified to - * be added, the old external ID with that key is deleted first and then the new external ID is - * added (so the external ID for that key is replaced). - * - * <p>If any of the specified external IDs belongs to another account the replacement fails with - * {@link IllegalStateException}. - */ - public void replace( - ReviewDb db, - Account.Id accountId, - Collection<ExternalId.Key> toDelete, - Collection<ExternalId> toAdd) - throws IOException, ConfigInvalidException, OrmException { - checkSameAccount(toAdd, accountId); - - db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(toDelete)); - db.accountExternalIds().insert(toAccountExternalIds(toAdd)); - - updateNoteMap( - o -> { - for (ExternalId.Key extIdKey : toDelete) { - remove(o.rw(), o.noteMap(), accountId, extIdKey); - } - - for (ExternalId extId : toAdd) { - insert(o.rw(), o.ins(), o.noteMap(), extId); - } - }); - accountCache.evict(accountId); - } - - /** - * Replaces an external ID. - * - * <p>If the specified external IDs belongs to different accounts the replacement fails with - * {@link IllegalStateException}. - */ - public void replace(ReviewDb db, ExternalId toDelete, ExternalId toAdd) - throws IOException, ConfigInvalidException, OrmException { - replace(db, Collections.singleton(toDelete), Collections.singleton(toAdd)); - } - - /** - * Replaces external IDs. - * - * <p>Deletion of external IDs is done before adding the new external IDs. This means if an - * external ID is specified for deletion and an external ID with the same key is specified to be - * added, the old external ID with that key is deleted first and then the new external ID is added - * (so the external ID for that key is replaced). - * - * <p>If the specified external IDs belong to different accounts the replacement fails with {@link - * IllegalStateException}. - */ - public void replace(ReviewDb db, Collection<ExternalId> toDelete, Collection<ExternalId> toAdd) - throws IOException, ConfigInvalidException, OrmException { - Account.Id accountId = checkSameAccount(Iterables.concat(toDelete, toAdd)); - if (accountId == null) { - // toDelete and toAdd are empty -> nothing to do - return; - } - - replace(db, accountId, toDelete.stream().map(e -> e.key()).collect(toSet()), toAdd); - } - - /** - * Checks that all specified external IDs belong to the same account. - * - * @return the ID of the account to which all specified external IDs belong. - */ - public static Account.Id checkSameAccount(Iterable<ExternalId> extIds) { - return checkSameAccount(extIds, null); - } - - /** - * Checks that all specified external IDs belong to specified account. If no account is specified - * it is checked that all specified external IDs belong to the same account. - * - * @return the ID of the account to which all specified external IDs belong. - */ - public static Account.Id checkSameAccount( - Iterable<ExternalId> extIds, @Nullable Account.Id accountId) { - for (ExternalId extId : extIds) { - if (accountId == null) { - accountId = extId.accountId(); - continue; - } - checkState( - accountId.equals(extId.accountId()), - "external id %s belongs to account %s, expected account %s", - extId.key().get(), - extId.accountId().get(), - accountId.get()); - } - return accountId; - } - - /** - * Inserts a new external ID and sets it in the note map. - * - * <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}. - */ - public static void insert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId) - throws OrmDuplicateKeyException, ConfigInvalidException, IOException { - if (noteMap.contains(extId.key().sha1())) { - throw new OrmDuplicateKeyException( - String.format("external id %s already exists", extId.key().get())); - } - upsert(rw, ins, noteMap, extId); - } - - /** - * Insert or updates an new external ID and sets it in the note map. - * - * <p>If the external ID already exists it is overwritten. - */ - public static void upsert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId) - throws IOException, ConfigInvalidException { - ObjectId noteId = extId.key().sha1(); - Config c = new Config(); - if (noteMap.contains(extId.key().sha1())) { - byte[] raw = - rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ); - try { - c.fromText(new String(raw, UTF_8)); - } catch (ConfigInvalidException e) { - throw new ConfigInvalidException( - String.format("Invalid external id config for note %s: %s", noteId, e.getMessage())); - } - } - extId.writeToConfig(c); - byte[] raw = c.toText().getBytes(UTF_8); - ObjectId dataBlob = ins.insert(OBJ_BLOB, raw); - noteMap.set(noteId, dataBlob); - } - - /** - * Removes an external ID from the note map. - * - * <p>The removal fails with {@link IllegalStateException} if there is an existing external ID - * that has the same key, but otherwise doesn't match the specified external ID. - */ - public static void remove(RevWalk rw, NoteMap noteMap, ExternalId extId) - throws IOException, ConfigInvalidException { - ObjectId noteId = extId.key().sha1(); - if (!noteMap.contains(noteId)) { - return; - } - - byte[] raw = - rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ); - ExternalId actualExtId = ExternalId.parse(noteId.name(), raw); - checkState( - extId.equals(actualExtId), - "external id %s should be removed, but it's not matching the actual external id %s", - extId.toString(), - actualExtId.toString()); - noteMap.remove(noteId); - } - - /** - * Removes an external ID from the note map by external ID key. - * - * <p>The external ID is only deleted if it belongs to the specified account. If the external IDs - * belongs to another account the deletion fails with {@link IllegalStateException}. - */ - private static void remove( - RevWalk rw, NoteMap noteMap, Account.Id accountId, ExternalId.Key extIdKey) - throws IOException, ConfigInvalidException { - ObjectId noteId = extIdKey.sha1(); - if (!noteMap.contains(noteId)) { - return; - } - - byte[] raw = - rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ); - ExternalId extId = ExternalId.parse(noteId.name(), raw); - checkState( - accountId.equals(extId.accountId()), - "external id %s should be removed for account %s," - + " but external id belongs to account %s", - extIdKey.get(), - accountId.get(), - extId.accountId().get()); - noteMap.remove(noteId); - } - - private void updateNoteMap(MyConsumer<OpenRepo> update) - throws IOException, ConfigInvalidException, OrmException { - try (Repository repo = repoManager.openRepository(allUsersName); - RevWalk rw = new RevWalk(repo); - ObjectInserter ins = repo.newObjectInserter()) { - retryer.call(new TryNoteMapUpdate(repo, rw, ins, update)); - } catch (ExecutionException | RetryException e) { - if (e.getCause() != null) { - Throwables.throwIfInstanceOf(e.getCause(), IOException.class); - Throwables.throwIfInstanceOf(e.getCause(), ConfigInvalidException.class); - Throwables.throwIfInstanceOf(e.getCause(), OrmException.class); - } - throw new OrmException(e); - } - } - - private void commit( - Repository repo, RevWalk rw, ObjectInserter ins, ObjectId rev, NoteMap noteMap) - throws IOException { - commit(repo, rw, ins, rev, noteMap, COMMIT_MSG, committerIdent, authorIdent); - } - - /** Commits updates to the external IDs. */ - public static void commit( - Repository repo, - RevWalk rw, - ObjectInserter ins, - ObjectId rev, - NoteMap noteMap, - String commitMessage, - PersonIdent committerIdent, - PersonIdent authorIdent) - throws IOException { - CommitBuilder cb = new CommitBuilder(); - cb.setMessage(commitMessage); - cb.setTreeId(noteMap.writeTree(ins)); - cb.setAuthor(authorIdent); - cb.setCommitter(committerIdent); - if (!rev.equals(ObjectId.zeroId())) { - cb.setParentId(rev); - } else { - cb.setParentIds(); // Ref is currently nonexistent, commit has no parents. - } - if (cb.getTreeId() == null) { - if (rev.equals(ObjectId.zeroId())) { - cb.setTreeId(emptyTree(ins)); // No parent, assume empty tree. - } else { - RevCommit p = rw.parseCommit(rev); - cb.setTreeId(p.getTree()); // Copy tree from parent. - } - } - ObjectId commitId = ins.insert(cb); - ins.flush(); - - RefUpdate u = repo.updateRef(RefNames.REFS_EXTERNAL_IDS); - u.setRefLogIdent(committerIdent); - u.setRefLogMessage("Update external IDs", false); - u.setExpectedOldObjectId(rev); - u.setNewObjectId(commitId); - RefUpdate.Result res = u.update(); - switch (res) { - case NEW: - case FAST_FORWARD: - case NO_CHANGE: - case RENAMED: - case FORCED: - break; - case LOCK_FAILURE: - throw new LockFailureException("Updating external IDs failed with " + res); - case IO_FAILURE: - case NOT_ATTEMPTED: - case REJECTED: - case REJECTED_CURRENT_BRANCH: - default: - throw new IOException("Updating external IDs failed with " + res); - } - } - - private static ObjectId emptyTree(ObjectInserter ins) throws IOException { - return ins.insert(OBJ_TREE, new byte[] {}); - } - - private void evictAccounts(Collection<ExternalId> extIds) throws IOException { - for (Account.Id id : extIds.stream().map(ExternalId::accountId).collect(toSet())) { - accountCache.evict(id); - } - } - - private static interface MyConsumer<T> { - void accept(T t) throws IOException, ConfigInvalidException, OrmException; - } - - @AutoValue - abstract static class OpenRepo { - static OpenRepo create(Repository repo, RevWalk rw, ObjectInserter ins, NoteMap noteMap) { - return new AutoValue_ExternalIdsUpdate_OpenRepo(repo, rw, ins, noteMap); - } - - abstract Repository repo(); - - abstract RevWalk rw(); - - abstract ObjectInserter ins(); - - abstract NoteMap noteMap(); - } - - private class TryNoteMapUpdate implements Callable<Void> { - private final Repository repo; - private final RevWalk rw; - private final ObjectInserter ins; - private final MyConsumer<OpenRepo> update; - - private TryNoteMapUpdate( - Repository repo, RevWalk rw, ObjectInserter ins, MyConsumer<OpenRepo> update) { - this.repo = repo; - this.rw = rw; - this.ins = ins; - this.update = update; - } - - @Override - public Void call() throws Exception { - ObjectId rev = readRevision(repo); - - afterReadRevision.run(); - - NoteMap noteMap = readNoteMap(rw, rev); - update.accept(OpenRepo.create(repo, rw, ins, noteMap)); - - commit(repo, rw, ins, rev, noteMap); - return null; - } - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java index cd3c0c8..fe01b32 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -14,27 +14,13 @@ package com.google.gerrit.server.account; -import static com.google.gerrit.common.data.GlobalCapability.ACCESS_DATABASE; -import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT; -import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP; -import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT; -import static com.google.gerrit.common.data.GlobalCapability.EMAIL_REVIEWERS; -import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES; -import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK; -import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER; -import static com.google.gerrit.common.data.GlobalCapability.MODIFY_ACCOUNT; import static com.google.gerrit.common.data.GlobalCapability.PRIORITY; -import static com.google.gerrit.common.data.GlobalCapability.RUN_GC; -import static com.google.gerrit.common.data.GlobalCapability.STREAM_EVENTS; -import static com.google.gerrit.common.data.GlobalCapability.VIEW_ALL_ACCOUNTS; -import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES; -import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS; -import static com.google.gerrit.common.data.GlobalCapability.VIEW_PLUGINS; -import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE; import com.google.common.collect.Iterables; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.PermissionRange; +import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission; +import com.google.gerrit.extensions.api.access.PluginPermission; import com.google.gerrit.extensions.config.CapabilityDefinition; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.AuthException; @@ -45,12 +31,14 @@ import com.google.gerrit.server.OutputFormat; import com.google.gerrit.server.account.AccountResource.Capability; import com.google.gerrit.server.git.QueueProvider; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gson.reflect.TypeToken; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -67,83 +55,84 @@ private Set<String> query; + private final PermissionBackend permissionBackend; + private final CapabilityControl.Factory capabilityFactory; private final Provider<CurrentUser> self; private final DynamicMap<CapabilityDefinition> pluginCapabilities; @Inject - GetCapabilities(Provider<CurrentUser> self, DynamicMap<CapabilityDefinition> pluginCapabilities) { + GetCapabilities( + PermissionBackend permissionBackend, + CapabilityControl.Factory capabilityFactory, + Provider<CurrentUser> self, + DynamicMap<CapabilityDefinition> pluginCapabilities) { + this.permissionBackend = permissionBackend; + this.capabilityFactory = capabilityFactory; this.self = self; this.pluginCapabilities = pluginCapabilities; } @Override - public Object apply(AccountResource resource) throws AuthException { - if (self.get() != resource.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException("restricted to administrator"); + public Object apply(AccountResource rsrc) throws AuthException, PermissionBackendException { + PermissionBackend.WithUser perm = permissionBackend.user(self); + if (self.get() != rsrc.getUser()) { + perm.check(GlobalPermission.ADMINISTRATE_SERVER); + perm = permissionBackend.user(rsrc.getUser()); } - CapabilityControl cc = resource.getUser().getCapabilities(); Map<String, Object> have = new LinkedHashMap<>(); - for (String name : GlobalCapability.getAllNames()) { - if (!name.equals(PRIORITY) && want(name) && cc.canPerform(name)) { - if (GlobalCapability.hasRange(name)) { - have.put(name, new Range(cc.getRange(name))); - } else { - have.put(name, true); - } - } - } - for (String pluginName : pluginCapabilities.plugins()) { - for (String capability : pluginCapabilities.byPlugin(pluginName).keySet()) { - String name = String.format("%s-%s", pluginName, capability); - if (want(name) && cc.canPerform(name)) { - have.put(name, true); - } - } + for (GlobalOrPluginPermission p : perm.test(permissionsToTest())) { + have.put(p.permissionName(), true); } - have.put(ACCESS_DATABASE, cc.canAccessDatabase()); - have.put(CREATE_ACCOUNT, cc.canCreateAccount()); - have.put(CREATE_GROUP, cc.canCreateGroup()); - have.put(CREATE_PROJECT, cc.canCreateProject()); - have.put(EMAIL_REVIEWERS, cc.canEmailReviewers()); - have.put(FLUSH_CACHES, cc.canFlushCaches()); - have.put(KILL_TASK, cc.canKillTask()); - have.put(MAINTAIN_SERVER, cc.canMaintainServer()); - have.put(MODIFY_ACCOUNT, cc.canModifyAccount()); - have.put(RUN_GC, cc.canRunGC()); - have.put(STREAM_EVENTS, cc.canStreamEvents()); - have.put(VIEW_ALL_ACCOUNTS, cc.canViewAllAccounts()); - have.put(VIEW_CACHES, cc.canViewCaches()); - have.put(VIEW_CONNECTIONS, cc.canViewConnections()); - have.put(VIEW_PLUGINS, cc.canViewPlugins()); - have.put(VIEW_QUEUE, cc.canViewQueue()); - - QueueProvider.QueueType queue = cc.getQueueType(); - if (queue != QueueProvider.QueueType.INTERACTIVE - || (query != null && query.contains(PRIORITY))) { - have.put(PRIORITY, queue); - } - - Iterator<Map.Entry<String, Object>> itr = have.entrySet().iterator(); - while (itr.hasNext()) { - Map.Entry<String, Object> e = itr.next(); - if (!want(e.getKey())) { - itr.remove(); - } else if (e.getValue() instanceof Boolean && !((Boolean) e.getValue())) { - itr.remove(); - } - } + CapabilityControl cc = capabilityFactory.create(rsrc.getUser()); + addRanges(have, cc); + addPriority(have, cc); return OutputFormat.JSON .newGson() .toJsonTree(have, new TypeToken<Map<String, Object>>() {}.getType()); } + private Set<GlobalOrPluginPermission> permissionsToTest() { + Set<GlobalOrPluginPermission> toTest = new HashSet<>(); + for (GlobalPermission p : GlobalPermission.values()) { + if (want(p.permissionName())) { + toTest.add(p); + } + } + + for (String pluginName : pluginCapabilities.plugins()) { + for (String capability : pluginCapabilities.byPlugin(pluginName).keySet()) { + PluginPermission p = new PluginPermission(pluginName, capability); + if (want(p.permissionName())) { + toTest.add(p); + } + } + } + return toTest; + } + private boolean want(String name) { return query == null || query.contains(name.toLowerCase()); } + private void addRanges(Map<String, Object> have, CapabilityControl cc) { + for (String name : GlobalCapability.getRangeNames()) { + if (want(name) && cc.hasExplicitRange(name)) { + have.put(name, new Range(cc.getRange(name))); + } + } + } + + private void addPriority(Map<String, Object> have, CapabilityControl cc) { + QueueProvider.QueueType queue = cc.getQueueType(); + if (queue != QueueProvider.QueueType.INTERACTIVE + || (query != null && query.contains(PRIORITY))) { + have.put(PRIORITY, queue); + } + } + private static class Range { private transient PermissionRange range;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java index 0edff4f..8215c6b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
@@ -25,6 +25,9 @@ import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.UserConfigSections; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -42,23 +45,26 @@ private final Provider<CurrentUser> self; private final Provider<AllUsersName> allUsersName; + private final PermissionBackend permissionBackend; private final GitRepositoryManager gitMgr; @Inject GetDiffPreferences( Provider<CurrentUser> self, Provider<AllUsersName> allUsersName, + PermissionBackend permissionBackend, GitRepositoryManager gitMgr) { this.self = self; this.allUsersName = allUsersName; + this.permissionBackend = permissionBackend; this.gitMgr = gitMgr; } @Override public DiffPreferencesInfo apply(AccountResource rsrc) - throws AuthException, ConfigInvalidException, IOException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException("restricted to administrator"); + throws AuthException, ConfigInvalidException, IOException, PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER); } Account.Id id = rsrc.getUser().getAccountId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java index e385020..bb207f0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
@@ -24,6 +24,9 @@ import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.UserConfigSections; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -35,22 +38,27 @@ @Singleton public class GetEditPreferences implements RestReadView<AccountResource> { private final Provider<CurrentUser> self; + private final PermissionBackend permissionBackend; private final AllUsersName allUsersName; private final GitRepositoryManager gitMgr; @Inject GetEditPreferences( - Provider<CurrentUser> self, AllUsersName allUsersName, GitRepositoryManager gitMgr) { + Provider<CurrentUser> self, + PermissionBackend permissionBackend, + AllUsersName allUsersName, + GitRepositoryManager gitMgr) { this.self = self; + this.permissionBackend = permissionBackend; this.allUsersName = allUsersName; this.gitMgr = gitMgr; } @Override public EditPreferencesInfo apply(AccountResource rsrc) - throws AuthException, IOException, ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("requires Modify Account capability"); + throws AuthException, IOException, ConfigInvalidException, PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); } return readFromGit(rsrc.getUser().getAccountId(), gitMgr, allUsersName, null);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java index 6ea911f..709bfc3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java
@@ -14,48 +14,56 @@ package com.google.gerrit.server.account; -import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.gerrit.extensions.common.AccountExternalIdInfo; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestReadView; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; import com.google.gerrit.server.config.AuthConfig; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; 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; @Singleton public class GetExternalIds implements RestReadView<AccountResource> { - private final Provider<ReviewDb> db; + private final PermissionBackend permissionBackend; + private final ExternalIds externalIds; private final Provider<CurrentUser> self; private final AuthConfig authConfig; @Inject - GetExternalIds(Provider<ReviewDb> db, Provider<CurrentUser> self, AuthConfig authConfig) { - this.db = db; + GetExternalIds( + PermissionBackend permissionBackend, + ExternalIds externalIds, + Provider<CurrentUser> self, + AuthConfig authConfig) { + this.permissionBackend = permissionBackend; + this.externalIds = externalIds; this.self = self; this.authConfig = authConfig; } @Override public List<AccountExternalIdInfo> apply(AccountResource resource) - throws RestApiException, OrmException { + throws RestApiException, IOException, OrmException, PermissionBackendException { if (self.get() != resource.getUser()) { - throw new AuthException("not allowed to get external IDs"); + permissionBackend.user(self).check(GlobalPermission.ACCESS_DATABASE); } - Collection<ExternalId> ids = - ExternalId.from( - db.get().accountExternalIds().byAccount(resource.getUser().getAccountId()).toList()); + Collection<ExternalId> ids = externalIds.byAccount(resource.getUser().getAccountId()); if (ids.isEmpty()) { return ImmutableList.of(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java index 77cdbd4..3ebf864 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetPreferences.java
@@ -19,6 +19,9 @@ import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.CurrentUser; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -26,18 +29,22 @@ @Singleton public class GetPreferences implements RestReadView<AccountResource> { private final Provider<CurrentUser> self; + private final PermissionBackend permissionBackend; private final AccountCache accountCache; @Inject - GetPreferences(Provider<CurrentUser> self, AccountCache accountCache) { + GetPreferences( + Provider<CurrentUser> self, PermissionBackend permissionBackend, AccountCache accountCache) { this.self = self; + this.permissionBackend = permissionBackend; this.accountCache = accountCache; } @Override - public GeneralPreferencesInfo apply(AccountResource rsrc) throws AuthException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("requires Modify Account capability"); + public GeneralPreferencesInfo apply(AccountResource rsrc) + throws AuthException, PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); } Account.Id id = rsrc.getUser().getAccountId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java index 980d880..9f5b9d5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetSshKeys.java
@@ -22,6 +22,9 @@ import com.google.gerrit.reviewdb.client.AccountSshKey; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -35,20 +38,25 @@ public class GetSshKeys implements RestReadView<AccountResource> { private final Provider<CurrentUser> self; + private final PermissionBackend permissionBackend; private final VersionedAuthorizedKeys.Accessor authorizedKeys; @Inject - GetSshKeys(Provider<CurrentUser> self, VersionedAuthorizedKeys.Accessor authorizedKeys) { + GetSshKeys( + Provider<CurrentUser> self, + PermissionBackend permissionBackend, + VersionedAuthorizedKeys.Accessor authorizedKeys) { this.self = self; + this.permissionBackend = permissionBackend; this.authorizedKeys = authorizedKeys; } @Override public List<SshKeyInfo> apply(AccountResource rsrc) throws AuthException, OrmException, RepositoryNotFoundException, IOException, - ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("not allowed to get SSH keys"); + ConfigInvalidException, PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); } return apply(rsrc.getUser()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java index e0aeee0..c2c0547 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
@@ -23,6 +23,9 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -38,22 +41,28 @@ @Singleton public class GetWatchedProjects implements RestReadView<AccountResource> { - + private final PermissionBackend permissionBackend; private final Provider<IdentifiedUser> self; private final WatchConfig.Accessor watchConfig; @Inject - public GetWatchedProjects(Provider<IdentifiedUser> self, WatchConfig.Accessor watchConfig) { + public GetWatchedProjects( + PermissionBackend permissionBackend, + Provider<IdentifiedUser> self, + WatchConfig.Accessor watchConfig) { + this.permissionBackend = permissionBackend; this.self = self; this.watchConfig = watchConfig; } @Override public List<ProjectWatchInfo> apply(AccountResource rsrc) - throws OrmException, AuthException, IOException, ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException("It is not allowed to list project watches of other users"); + throws OrmException, AuthException, IOException, ConfigInvalidException, + PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER); } + Account.Id accountId = rsrc.getUser().getAccountId(); List<ProjectWatchInfo> projectWatchInfos = new ArrayList<>(); for (Map.Entry<ProjectWatchKey, Set<NotifyType>> e :
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java index 5c8e3e9..1706880 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -17,6 +17,7 @@ import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; +import com.google.gerrit.common.TimeUtil; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroupName; import com.google.gerrit.reviewdb.server.ReviewDb; @@ -87,7 +88,7 @@ } @Override - public AccountGroup get(final AccountGroup.Id groupId) { + public AccountGroup get(AccountGroup.Id groupId) { try { Optional<AccountGroup> g = byId.get(groupId); return g.isPresent() ? g.get() : missing(groupId); @@ -98,7 +99,7 @@ } @Override - public void evict(final AccountGroup group) throws IOException { + public void evict(AccountGroup group) throws IOException { if (group.getId() != null) { byId.invalidate(group.getId()); } @@ -112,8 +113,8 @@ } @Override - public void evictAfterRename( - final AccountGroup.NameKey oldName, final AccountGroup.NameKey newName) throws IOException { + public void evictAfterRename(final AccountGroup.NameKey oldName, AccountGroup.NameKey newName) + throws IOException { if (oldName != null) { byName.invalidate(oldName.get()); } @@ -167,19 +168,19 @@ private static AccountGroup missing(AccountGroup.Id key) { AccountGroup.NameKey name = new AccountGroup.NameKey("Deleted Group" + key); - return new AccountGroup(name, key, null); + return new AccountGroup(name, key, null, TimeUtil.nowTs()); } static class ByIdLoader extends CacheLoader<AccountGroup.Id, Optional<AccountGroup>> { private final SchemaFactory<ReviewDb> schema; @Inject - ByIdLoader(final SchemaFactory<ReviewDb> sf) { + ByIdLoader(SchemaFactory<ReviewDb> sf) { schema = sf; } @Override - public Optional<AccountGroup> load(final AccountGroup.Id key) throws Exception { + public Optional<AccountGroup> load(AccountGroup.Id key) throws Exception { try (ReviewDb db = schema.open()) { return Optional.ofNullable(db.accountGroups().get(key)); } @@ -190,7 +191,7 @@ private final SchemaFactory<ReviewDb> schema; @Inject - ByNameLoader(final SchemaFactory<ReviewDb> sf) { + ByNameLoader(SchemaFactory<ReviewDb> sf) { schema = sf; } @@ -211,7 +212,7 @@ private final SchemaFactory<ReviewDb> schema; @Inject - ByUUIDLoader(final SchemaFactory<ReviewDb> sf) { + ByUUIDLoader(SchemaFactory<ReviewDb> sf) { schema = sf; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupComparator.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupComparator.java index 4bab3a7..6ba2e5e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupComparator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupComparator.java
@@ -20,7 +20,7 @@ public class GroupComparator implements Comparator<AccountGroup> { @Override - public int compare(final AccountGroup group1, final AccountGroup group2) { + public int compare(AccountGroup group1, AccountGroup group2) { return group1.getName().compareTo(group2.getName()); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java index ee788ec..6721e11 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -17,9 +17,13 @@ import com.google.gerrit.common.data.GroupDescription; import com.google.gerrit.common.data.GroupDescriptions; import com.google.gerrit.common.errors.NoSuchGroupException; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.CurrentUser; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -29,36 +33,44 @@ @Singleton public static class GenericFactory { + private final PermissionBackend permissionBackend; private final GroupBackend groupBackend; @Inject - GenericFactory(final GroupBackend gb) { + GenericFactory(PermissionBackend permissionBackend, GroupBackend gb) { + this.permissionBackend = permissionBackend; groupBackend = gb; } - public GroupControl controlFor(final CurrentUser who, final AccountGroup.UUID groupId) + public GroupControl controlFor(CurrentUser who, AccountGroup.UUID groupId) throws NoSuchGroupException { - final GroupDescription.Basic group = groupBackend.get(groupId); + GroupDescription.Basic group = groupBackend.get(groupId); if (group == null) { throw new NoSuchGroupException(groupId); } - return new GroupControl(who, group, groupBackend); + return new GroupControl(who, group, permissionBackend, groupBackend); } } public static class Factory { + private final PermissionBackend permissionBackend; private final GroupCache groupCache; private final Provider<CurrentUser> user; private final GroupBackend groupBackend; @Inject - Factory(final GroupCache gc, final Provider<CurrentUser> cu, final GroupBackend gb) { + Factory( + PermissionBackend permissionBackend, + GroupCache gc, + Provider<CurrentUser> cu, + GroupBackend gb) { + this.permissionBackend = permissionBackend; groupCache = gc; user = cu; groupBackend = gb; } - public GroupControl controlFor(final AccountGroup.Id groupId) throws NoSuchGroupException { + public GroupControl controlFor(AccountGroup.Id groupId) throws NoSuchGroupException { final AccountGroup group = groupCache.get(groupId); if (group == null) { throw new NoSuchGroupException(groupId); @@ -66,7 +78,7 @@ return controlFor(GroupDescriptions.forAccountGroup(group)); } - public GroupControl controlFor(final AccountGroup.UUID groupId) throws NoSuchGroupException { + public GroupControl controlFor(AccountGroup.UUID groupId) throws NoSuchGroupException { final GroupDescription.Basic group = groupBackend.get(groupId); if (group == null) { throw new NoSuchGroupException(groupId); @@ -79,10 +91,10 @@ } public GroupControl controlFor(GroupDescription.Basic group) { - return new GroupControl(user.get(), group, groupBackend); + return new GroupControl(user.get(), group, permissionBackend, groupBackend); } - public GroupControl validateFor(final AccountGroup.Id groupId) throws NoSuchGroupException { + public GroupControl validateFor(AccountGroup.Id groupId) throws NoSuchGroupException { final GroupControl c = controlFor(groupId); if (!c.isVisible()) { throw new NoSuchGroupException(groupId); @@ -90,7 +102,7 @@ return c; } - public GroupControl validateFor(final AccountGroup.UUID groupUUID) throws NoSuchGroupException { + public GroupControl validateFor(AccountGroup.UUID groupUUID) throws NoSuchGroupException { final GroupControl c = controlFor(groupUUID); if (!c.isVisible()) { throw new NoSuchGroupException(groupUUID); @@ -102,11 +114,17 @@ private final CurrentUser user; private final GroupDescription.Basic group; private Boolean isOwner; + private final PermissionBackend.WithUser perm; private final GroupBackend groupBackend; - GroupControl(CurrentUser who, GroupDescription.Basic gd, GroupBackend gb) { + GroupControl( + CurrentUser who, + GroupDescription.Basic gd, + PermissionBackend permissionBackend, + GroupBackend gb) { user = who; group = gd; + this.perm = permissionBackend.user(user); groupBackend = gb; } @@ -127,8 +145,8 @@ return user.isInternalUser() || groupBackend.isVisibleToAll(group.getGroupUUID()) || user.getEffectiveGroups().contains(group.getGroupUUID()) - || user.getCapabilities().canAdministrateServer() - || isOwner(); + || isOwner() + || canAdministrateServer(); } public boolean isOwner() { @@ -137,13 +155,20 @@ isOwner = false; } else if (isOwner == null) { AccountGroup.UUID ownerUUID = accountGroup.getOwnerGroupUUID(); - isOwner = - getUser().getEffectiveGroups().contains(ownerUUID) - || getUser().getCapabilities().canAdministrateServer(); + isOwner = getUser().getEffectiveGroups().contains(ownerUUID) || canAdministrateServer(); } return isOwner; } + private boolean canAdministrateServer() { + try { + perm.check(GlobalPermission.ADMINISTRATE_SERVER); + return true; + } catch (AuthException | PermissionBackendException denied) { + return false; + } + } + public boolean canAddMember() { return isOwner(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java index 1c9baf8..e1fc101 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -139,7 +139,7 @@ private final SchemaFactory<ReviewDb> schema; @Inject - SubgroupsLoader(final SchemaFactory<ReviewDb> sf) { + SubgroupsLoader(SchemaFactory<ReviewDb> sf) { schema = sf; } @@ -165,7 +165,7 @@ private final SchemaFactory<ReviewDb> schema; @Inject - ParentGroupsLoader(final SchemaFactory<ReviewDb> sf) { + ParentGroupsLoader(SchemaFactory<ReviewDb> sf) { schema = sf; } @@ -190,7 +190,7 @@ private final SchemaFactory<ReviewDb> schema; @Inject - AllExternalLoader(final SchemaFactory<ReviewDb> sf) { + AllExternalLoader(SchemaFactory<ReviewDb> sf) { schema = sf; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java index ea99b9b..d84d051 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
@@ -58,7 +58,7 @@ this.currentUser = currentUser; } - public Set<Account> listAccounts(final AccountGroup.UUID groupUUID, final Project.NameKey project) + public Set<Account> listAccounts(AccountGroup.UUID groupUUID, Project.NameKey project) throws NoSuchGroupException, NoSuchProjectException, OrmException, IOException { return listAccounts(groupUUID, project, new HashSet<AccountGroup.UUID>()); } @@ -78,8 +78,7 @@ return Collections.emptySet(); } - private Set<Account> getProjectOwners( - final Project.NameKey project, final Set<AccountGroup.UUID> seen) + private Set<Account> getProjectOwners(final Project.NameKey project, Set<AccountGroup.UUID> seen) throws NoSuchProjectException, NoSuchGroupException, OrmException, IOException { seen.add(SystemGroupBackend.PROJECT_OWNERS); if (project == null) { @@ -90,7 +89,7 @@ projectControl.controlFor(project, currentUser).getProjectState().getAllOwners(); final HashSet<Account> projectOwners = new HashSet<>(); - for (final AccountGroup.UUID ownerGroup : ownerGroups) { + for (AccountGroup.UUID ownerGroup : ownerGroups) { if (!seen.contains(ownerGroup)) { projectOwners.addAll(listAccounts(ownerGroup, project, seen)); } @@ -99,19 +98,19 @@ } private Set<Account> getGroupMembers( - final AccountGroup group, final Project.NameKey project, final Set<AccountGroup.UUID> seen) + final AccountGroup group, Project.NameKey project, Set<AccountGroup.UUID> seen) throws NoSuchGroupException, OrmException, NoSuchProjectException, IOException { seen.add(group.getGroupUUID()); final GroupDetail groupDetail = groupDetailFactory.create(group.getId()).call(); final Set<Account> members = new HashSet<>(); if (groupDetail.members != null) { - for (final AccountGroupMember member : groupDetail.members) { + for (AccountGroupMember member : groupDetail.members) { members.add(accountCache.get(member.getAccountId()).getAccount()); } } if (groupDetail.includes != null) { - for (final AccountGroupById groupInclude : groupDetail.includes) { + for (AccountGroupById groupInclude : groupDetail.includes) { final AccountGroup includedGroup = groupCache.get(groupInclude.getIncludeUUID()); if (includedGroup != null && !seen.contains(includedGroup.getGroupUUID())) { members.addAll(listAccounts(includedGroup.getGroupUUID(), project, seen));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Index.java index 6943dca..ecc6b8c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Index.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Index.java
@@ -19,6 +19,9 @@ import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.Index.Input; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -29,18 +32,22 @@ public static class Input {} private final AccountCache accountCache; + private final PermissionBackend permissionBackend; private final Provider<CurrentUser> self; @Inject - Index(AccountCache accountCache, Provider<CurrentUser> self) { + Index( + AccountCache accountCache, PermissionBackend permissionBackend, Provider<CurrentUser> self) { this.accountCache = accountCache; + this.permissionBackend = permissionBackend; this.self = self; } @Override - public Response<?> apply(AccountResource rsrc, Input input) throws IOException, AuthException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("not allowed to index account"); + public Response<?> apply(AccountResource rsrc, Input input) + throws IOException, AuthException, PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); } // evicting the account from the cache, reindexes the account
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java index 7791a2e..e7ff314 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalAccountDirectory.java
@@ -21,6 +21,7 @@ import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.avatar.AvatarProvider; import com.google.inject.AbstractModule; import com.google.inject.Inject;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java index b0ada0d..a42362c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -64,7 +64,7 @@ } @Override - public Collection<GroupReference> suggest(final String name, final ProjectControl project) { + public Collection<GroupReference> suggest(String name, ProjectControl project) { return groupCache .all() .stream()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java index 55ba912..38887f6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PostWatchedProjects.java
@@ -15,7 +15,6 @@ package com.google.gerrit.server.account; import com.google.gerrit.extensions.client.ProjectWatchInfo; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestModifyView; @@ -24,6 +23,9 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ProjectsCollection; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -41,6 +43,7 @@ public class PostWatchedProjects implements RestModifyView<AccountResource, List<ProjectWatchInfo>> { private final Provider<IdentifiedUser> self; + private final PermissionBackend permissionBackend; private final GetWatchedProjects getWatchedProjects; private final ProjectsCollection projectsCollection; private final AccountCache accountCache; @@ -49,11 +52,13 @@ @Inject public PostWatchedProjects( Provider<IdentifiedUser> self, + PermissionBackend permissionBackend, GetWatchedProjects getWatchedProjects, ProjectsCollection projectsCollection, AccountCache accountCache, WatchConfig.Accessor watchConfig) { this.self = self; + this.permissionBackend = permissionBackend; this.getWatchedProjects = getWatchedProjects; this.projectsCollection = projectsCollection; this.accountCache = accountCache; @@ -62,10 +67,12 @@ @Override public List<ProjectWatchInfo> apply(AccountResource rsrc, List<ProjectWatchInfo> input) - throws OrmException, RestApiException, IOException, ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException("not allowed to edit project watches"); + throws OrmException, RestApiException, IOException, ConfigInvalidException, + PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER); } + Account.Id accountId = rsrc.getUser().getAccountId(); watchConfig.upsertProjectWatches(accountId, asMap(input)); accountCache.evict(accountId); @@ -73,7 +80,8 @@ } private Map<ProjectWatchKey, Set<NotifyType>> asMap(List<ProjectWatchInfo> input) - throws BadRequestException, UnprocessableEntityException, IOException { + throws BadRequestException, UnprocessableEntityException, IOException, + PermissionBackendException { Map<ProjectWatchKey, Set<NotifyType>> m = new HashMap<>(); for (ProjectWatchInfo info : input) { if (info.project == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java index 32c5345..6051a95 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
@@ -22,14 +22,13 @@ import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.PutActive.Input; -import com.google.gwtorm.server.AtomicUpdate; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; -import java.util.Collections; import java.util.concurrent.atomic.AtomicBoolean; +import org.eclipse.jgit.errors.ConfigInvalidException; @RequiresCapability(GlobalCapability.MODIFY_ACCOUNT) @Singleton @@ -37,40 +36,34 @@ public static class Input {} private final Provider<ReviewDb> dbProvider; - private final AccountCache byIdCache; + private final AccountsUpdate.Server accountsUpdate; @Inject - PutActive(Provider<ReviewDb> dbProvider, AccountCache byIdCache) { + PutActive(Provider<ReviewDb> dbProvider, AccountsUpdate.Server accountsUpdate) { this.dbProvider = dbProvider; - this.byIdCache = byIdCache; + this.accountsUpdate = accountsUpdate; } @Override public Response<String> apply(AccountResource rsrc, Input input) - throws ResourceNotFoundException, OrmException, IOException { + throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException { AtomicBoolean alreadyActive = new AtomicBoolean(false); - Account a = - dbProvider - .get() - .accounts() - .atomicUpdate( + Account account = + accountsUpdate + .create() + .update( + dbProvider.get(), rsrc.getUser().getAccountId(), - new AtomicUpdate<Account>() { - @Override - public Account update(Account a) { - if (a.isActive()) { - alreadyActive.set(true); - } else { - a.setActive(true); - } - return a; + a -> { + if (a.isActive()) { + alreadyActive.set(true); + } else { + a.setActive(true); } }); - if (a == null) { + if (account == null) { throw new ResourceNotFoundException("account not found"); } - dbProvider.get().accounts().update(Collections.singleton(a)); - byIdCache.evict(a.getId()); return alreadyActive.get() ? Response.ok("") : Response.created(""); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java index 29aac58..e00f6b3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
@@ -14,7 +14,7 @@ package com.google.gerrit.server.account; -import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; import com.google.common.base.Strings; import com.google.gerrit.extensions.restapi.AuthException; @@ -22,10 +22,15 @@ 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.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.PutHttpPassword.Input; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -53,23 +58,30 @@ } private final Provider<CurrentUser> self; - private final Provider<ReviewDb> dbProvider; + private final PermissionBackend permissionBackend; + private final ExternalIds externalIds; private final ExternalIdsUpdate.User externalIdsUpdate; @Inject PutHttpPassword( Provider<CurrentUser> self, - Provider<ReviewDb> dbProvider, + PermissionBackend permissionBackend, + ExternalIds externalIds, ExternalIdsUpdate.User externalIdsUpdate) { this.self = self; - this.dbProvider = dbProvider; + this.permissionBackend = permissionBackend; + this.externalIds = externalIds; this.externalIdsUpdate = externalIdsUpdate; } @Override public Response<String> apply(AccountResource rsrc, Input input) throws AuthException, ResourceNotFoundException, ResourceConflictException, OrmException, - IOException, ConfigInvalidException { + IOException, ConfigInvalidException, PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER); + } + if (input == null) { input = new Input(); } @@ -77,22 +89,12 @@ String newPassword; if (input.generate) { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException("not allowed to generate HTTP password"); - } newPassword = generate(); - } else if (input.httpPassword == null) { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException("not allowed to clear HTTP password"); - } newPassword = null; } else { - if (!self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException( - "not allowed to set HTTP password directly, " - + "requires the Administrate Server permission"); - } + // Only administrators can explicitly set the password. + permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER); newPassword = input.httpPassword; } return apply(rsrc.getUser(), newPassword); @@ -105,20 +107,13 @@ throw new ResourceConflictException("username must be set"); } - ExternalId extId = - ExternalId.from( - dbProvider - .get() - .accountExternalIds() - .get( - ExternalId.Key.create(SCHEME_USERNAME, user.getUserName()) - .asAccountExternalIdKey())); + ExternalId extId = externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, user.getUserName())); if (extId == null) { throw new ResourceNotFoundException(); } ExternalId newExtId = ExternalId.createWithPassword(extId.key(), extId.accountId(), extId.email(), newPassword); - externalIdsUpdate.create().upsert(dbProvider.get(), newExtId); + externalIdsUpdate.create().upsert(newExtId); return Strings.isNullOrEmpty(newPassword) ? Response.<String>none() : Response.ok(newPassword); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java index 443a549..792e71d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutName.java
@@ -27,12 +27,15 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.PutName.Input; -import com.google.gwtorm.server.AtomicUpdate; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; 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; @Singleton public class PutName implements RestModifyView<AccountResource, Input> { @@ -42,33 +45,37 @@ private final Provider<CurrentUser> self; private final Realm realm; + private final PermissionBackend permissionBackend; private final Provider<ReviewDb> dbProvider; - private final AccountCache byIdCache; + private final AccountsUpdate.Server accountsUpdate; @Inject PutName( Provider<CurrentUser> self, Realm realm, + PermissionBackend permissionBackend, Provider<ReviewDb> dbProvider, - AccountCache byIdCache) { + AccountsUpdate.Server accountsUpdate) { this.self = self; this.realm = realm; + this.permissionBackend = permissionBackend; this.dbProvider = dbProvider; - this.byIdCache = byIdCache; + this.accountsUpdate = accountsUpdate; } @Override public Response<String> apply(AccountResource rsrc, Input input) throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException, - IOException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("not allowed to change name"); + IOException, PermissionBackendException, ConfigInvalidException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); } return apply(rsrc.getUser(), input); } public Response<String> apply(IdentifiedUser user, Input input) - throws MethodNotAllowedException, ResourceNotFoundException, OrmException, IOException { + throws MethodNotAllowedException, ResourceNotFoundException, OrmException, IOException, + ConfigInvalidException { if (input == null) { input = new Input(); } @@ -78,25 +85,15 @@ } String newName = input.name; - Account a = - dbProvider - .get() - .accounts() - .atomicUpdate( - user.getAccountId(), - new AtomicUpdate<Account>() { - @Override - public Account update(Account a) { - a.setFullName(newName); - return a; - } - }); - if (a == null) { + Account account = + accountsUpdate + .create() + .update(dbProvider.get(), user.getAccountId(), a -> a.setFullName(newName)); + if (account == null) { throw new ResourceNotFoundException("account not found"); } - byIdCache.evict(a.getId()); - return Strings.isNullOrEmpty(a.getFullName()) - ? Response.<String>none() - : Response.ok(a.getFullName()); + return Strings.isNullOrEmpty(account.getFullName()) + ? Response.none() + : Response.ok(account.getFullName()); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java index ec60fb3..98d4ac5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
@@ -23,14 +23,16 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.PutPreferred.Input; -import com.google.gwtorm.server.AtomicUpdate; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; -import java.util.Collections; import java.util.concurrent.atomic.AtomicBoolean; +import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton public class PutPreferred implements RestModifyView<AccountResource.Email, Input> { @@ -38,49 +40,50 @@ private final Provider<CurrentUser> self; private final Provider<ReviewDb> dbProvider; - private final AccountCache byIdCache; + private final PermissionBackend permissionBackend; + private final AccountsUpdate.Server accountsUpdate; @Inject - PutPreferred(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider, AccountCache byIdCache) { + PutPreferred( + Provider<CurrentUser> self, + Provider<ReviewDb> dbProvider, + PermissionBackend permissionBackend, + AccountsUpdate.Server accountsUpdate) { this.self = self; this.dbProvider = dbProvider; - this.byIdCache = byIdCache; + this.permissionBackend = permissionBackend; + this.accountsUpdate = accountsUpdate; } @Override public Response<String> apply(AccountResource.Email rsrc, Input input) - throws AuthException, ResourceNotFoundException, OrmException, IOException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("not allowed to set preferred email address"); + throws AuthException, ResourceNotFoundException, OrmException, IOException, + PermissionBackendException, ConfigInvalidException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); } return apply(rsrc.getUser(), rsrc.getEmail()); } public Response<String> apply(IdentifiedUser user, String email) - throws ResourceNotFoundException, OrmException, IOException { + throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException { AtomicBoolean alreadyPreferred = new AtomicBoolean(false); - Account a = - dbProvider - .get() - .accounts() - .atomicUpdate( + Account account = + accountsUpdate + .create() + .update( + dbProvider.get(), user.getAccountId(), - new AtomicUpdate<Account>() { - @Override - public Account update(Account a) { - if (email.equals(a.getPreferredEmail())) { - alreadyPreferred.set(true); - } else { - a.setPreferredEmail(email); - } - return a; + a -> { + if (email.equals(a.getPreferredEmail())) { + alreadyPreferred.set(true); + } else { + a.setPreferredEmail(email); } }); - if (a == null) { + if (account == null) { throw new ResourceNotFoundException("account not found"); } - dbProvider.get().accounts().update(Collections.singleton(a)); - byIdCache.evict(a.getId()); return alreadyPreferred.get() ? Response.ok("") : Response.created(""); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java index ff541fd..136fc68 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutStatus.java
@@ -25,12 +25,15 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.PutStatus.Input; -import com.google.gwtorm.server.AtomicUpdate; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; 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; @Singleton public class PutStatus implements RestModifyView<AccountResource, Input> { @@ -46,48 +49,50 @@ private final Provider<CurrentUser> self; private final Provider<ReviewDb> dbProvider; - private final AccountCache byIdCache; + private final PermissionBackend permissionBackend; + private final AccountsUpdate.Server accountsUpdate; @Inject - PutStatus(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider, AccountCache byIdCache) { + PutStatus( + Provider<CurrentUser> self, + Provider<ReviewDb> dbProvider, + PermissionBackend permissionBackend, + AccountsUpdate.Server accountsUpdate) { this.self = self; this.dbProvider = dbProvider; - this.byIdCache = byIdCache; + this.permissionBackend = permissionBackend; + this.accountsUpdate = accountsUpdate; } @Override public Response<String> apply(AccountResource rsrc, Input input) - throws AuthException, ResourceNotFoundException, OrmException, IOException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("not allowed to set status"); + throws AuthException, ResourceNotFoundException, OrmException, IOException, + PermissionBackendException, ConfigInvalidException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); } return apply(rsrc.getUser(), input); } public Response<String> apply(IdentifiedUser user, Input input) - throws ResourceNotFoundException, OrmException, IOException { + throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException { if (input == null) { input = new Input(); } String newStatus = input.status; - Account a = - dbProvider - .get() - .accounts() - .atomicUpdate( + Account account = + accountsUpdate + .create() + .update( + dbProvider.get(), user.getAccountId(), - new AtomicUpdate<Account>() { - @Override - public Account update(Account a) { - a.setStatus(Strings.nullToEmpty(newStatus)); - return a; - } - }); - if (a == null) { + a -> a.setStatus(Strings.nullToEmpty(newStatus))); + if (account == null) { throw new ResourceNotFoundException("account not found"); } - byIdCache.evict(a.getId()); - return Strings.isNullOrEmpty(a.getStatus()) ? Response.none() : Response.ok(a.getStatus()); + return Strings.isNullOrEmpty(account.getStatus()) + ? Response.none() + : Response.ok(account.getStatus()); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java index e3a3c12..a73bdd9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
@@ -22,9 +22,11 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.PutUsername.Input; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -40,27 +42,28 @@ private final Provider<CurrentUser> self; private final ChangeUserName.Factory changeUserNameFactory; + private final PermissionBackend permissionBackend; private final Realm realm; - private final Provider<ReviewDb> db; @Inject PutUsername( Provider<CurrentUser> self, ChangeUserName.Factory changeUserNameFactory, - Realm realm, - Provider<ReviewDb> db) { + PermissionBackend permissionBackend, + Realm realm) { this.self = self; this.changeUserNameFactory = changeUserNameFactory; + this.permissionBackend = permissionBackend; this.realm = realm; - this.db = db; } @Override public String apply(AccountResource rsrc, Input input) throws AuthException, MethodNotAllowedException, UnprocessableEntityException, - ResourceConflictException, OrmException, IOException, ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) { - throw new AuthException("not allowed to set username"); + ResourceConflictException, OrmException, IOException, ConfigInvalidException, + PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.ADMINISTRATE_SERVER); } if (!realm.allowsEdit(AccountFieldName.USER_NAME)) { @@ -72,7 +75,7 @@ } try { - changeUserNameFactory.create(db.get(), rsrc.getUser(), input.username).call(); + changeUserNameFactory.create(rsrc.getUser(), input.username).call(); } catch (IllegalStateException e) { if (ChangeUserName.USERNAME_CANNOT_BE_CHANGED.equals(e.getMessage())) { throw new MethodNotAllowedException(e.getMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java index ac0cc96..88e9e20 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
@@ -29,6 +29,9 @@ import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.UserConfigSections; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -41,6 +44,7 @@ private final Provider<CurrentUser> self; private final Provider<MetaDataUpdate.User> metaDataUpdateFactory; private final AllUsersName allUsersName; + private final PermissionBackend permissionBackend; private final GitRepositoryManager gitMgr; @Inject @@ -48,19 +52,21 @@ Provider<CurrentUser> self, Provider<MetaDataUpdate.User> metaDataUpdateFactory, AllUsersName allUsersName, + PermissionBackend permissionBackend, GitRepositoryManager gitMgr) { this.self = self; this.metaDataUpdateFactory = metaDataUpdateFactory; this.allUsersName = allUsersName; + this.permissionBackend = permissionBackend; this.gitMgr = gitMgr; } @Override public DiffPreferencesInfo apply(AccountResource rsrc, DiffPreferencesInfo in) throws AuthException, BadRequestException, ConfigInvalidException, - RepositoryNotFoundException, IOException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("requires Modify Account capability"); + RepositoryNotFoundException, IOException, PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); } if (in == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java index ca981b8..53285db 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
@@ -28,6 +28,9 @@ import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.UserConfigSections; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -40,6 +43,7 @@ private final Provider<CurrentUser> self; private final Provider<MetaDataUpdate.User> metaDataUpdateFactory; + private final PermissionBackend permissionBackend; private final GitRepositoryManager gitMgr; private final AllUsersName allUsersName; @@ -47,10 +51,12 @@ SetEditPreferences( Provider<CurrentUser> self, Provider<MetaDataUpdate.User> metaDataUpdateFactory, + PermissionBackend permissionBackend, GitRepositoryManager gitMgr, AllUsersName allUsersName) { this.self = self; this.metaDataUpdateFactory = metaDataUpdateFactory; + this.permissionBackend = permissionBackend; this.gitMgr = gitMgr; this.allUsersName = allUsersName; } @@ -58,9 +64,9 @@ @Override public EditPreferencesInfo apply(AccountResource rsrc, EditPreferencesInfo in) throws AuthException, BadRequestException, RepositoryNotFoundException, IOException, - ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("requires Modify Account capability"); + ConfigInvalidException, PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); } if (in == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java index 91672f7..c033d9d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetPreferences.java
@@ -36,6 +36,9 @@ import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.UserConfigSections; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -51,6 +54,7 @@ public class SetPreferences implements RestModifyView<AccountResource, GeneralPreferencesInfo> { private final Provider<CurrentUser> self; private final AccountCache cache; + private final PermissionBackend permissionBackend; private final GeneralPreferencesLoader loader; private final Provider<MetaDataUpdate.User> metaDataUpdateFactory; private final AllUsersName allUsersName; @@ -60,6 +64,7 @@ SetPreferences( Provider<CurrentUser> self, AccountCache cache, + PermissionBackend permissionBackend, GeneralPreferencesLoader loader, Provider<MetaDataUpdate.User> metaDataUpdateFactory, AllUsersName allUsersName, @@ -67,6 +72,7 @@ this.self = self; this.loader = loader; this.cache = cache; + this.permissionBackend = permissionBackend; this.metaDataUpdateFactory = metaDataUpdateFactory; this.allUsersName = allUsersName; this.downloadSchemes = downloadSchemes; @@ -74,9 +80,10 @@ @Override public GeneralPreferencesInfo apply(AccountResource rsrc, GeneralPreferencesInfo i) - throws AuthException, BadRequestException, IOException, ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new AuthException("requires Modify Account capability"); + throws AuthException, BadRequestException, IOException, ConfigInvalidException, + PermissionBackendException { + if (self.get() != rsrc.getUser()) { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); } checkDownloadScheme(i.downloadScheme);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java index 6336e08..70c02a1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SshKeys.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.account; import com.google.gerrit.extensions.registration.DynamicMap; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ChildCollection; import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; @@ -22,6 +23,9 @@ import com.google.gerrit.reviewdb.client.AccountSshKey; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -34,6 +38,7 @@ private final DynamicMap<RestView<AccountResource.SshKey>> views; private final GetSshKeys list; private final Provider<CurrentUser> self; + private final PermissionBackend permissionBackend; private final VersionedAuthorizedKeys.Accessor authorizedKeys; @Inject @@ -41,10 +46,12 @@ DynamicMap<RestView<AccountResource.SshKey>> views, GetSshKeys list, Provider<CurrentUser> self, + PermissionBackend permissionBackend, VersionedAuthorizedKeys.Accessor authorizedKeys) { this.views = views; this.list = list; this.self = self; + this.permissionBackend = permissionBackend; this.authorizedKeys = authorizedKeys; } @@ -55,9 +62,15 @@ @Override public AccountResource.SshKey parse(AccountResource rsrc, IdString id) - throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException { - if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { - throw new ResourceNotFoundException(); + throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException, + PermissionBackendException { + if (self.get() != rsrc.getUser()) { + try { + permissionBackend.user(self).check(GlobalPermission.MODIFY_ACCOUNT); + } catch (AuthException e) { + // If lacking MODIFY_ACCOUNT claim the resource does not exist. + throw new ResourceNotFoundException(); + } } return parse(rsrc.getUser(), id); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java new file mode 100644 index 0000000..ab212fb --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/DisabledExternalIdCache.java
@@ -0,0 +1,95 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account.externalids; + +import com.google.gerrit.reviewdb.client.Account; +import com.google.inject.AbstractModule; +import com.google.inject.Module; +import java.io.IOException; +import java.util.Collection; +import java.util.Set; +import org.eclipse.jgit.lib.ObjectId; + +public class DisabledExternalIdCache implements ExternalIdCache { + public static Module module() { + return new AbstractModule() { + + @Override + protected void configure() { + bind(ExternalIdCache.class).to(DisabledExternalIdCache.class); + } + }; + } + + @Override + public void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) {} + + @Override + public void onUpdate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) {} + + @Override + public void onReplace( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Account.Id accountId, + Collection<ExternalId> toRemove, + Collection<ExternalId> toAdd) {} + + @Override + public void onReplaceByKeys( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Account.Id accountId, + Collection<ExternalId.Key> toRemove, + Collection<ExternalId> toAdd) {} + + @Override + public void onReplaceByKeys( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Collection<ExternalId.Key> toRemove, + Collection<ExternalId> toAdd) {} + + @Override + public void onReplace( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Collection<ExternalId> toRemove, + Collection<ExternalId> toAdd) {} + + @Override + public void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) {} + + @Override + public void onRemoveByKeys( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Account.Id accountId, + Collection<ExternalId.Key> extIdKeys) {} + + @Override + public void onRemoveByKeys( + ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId.Key> extIdKeys) {} + + @Override + public Set<ExternalId> byAccount(Account.Id accountId) { + throw new UnsupportedOperationException(); + } + + @Override + public Set<ExternalId> byEmail(String email) throws IOException { + throw new UnsupportedOperationException(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalId.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java similarity index 78% rename from gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalId.java rename to gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java index cd10b7b..9c456f5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalId.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -12,24 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.account; +package com.google.gerrit.server.account.externalids; import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.stream.Collectors.toSet; import com.google.auto.value.AutoValue; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.hash.Hashing; -import com.google.common.primitives.Ints; import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AccountExternalId; +import com.google.gerrit.server.account.HashedPassword; import java.io.Serializable; -import java.util.Collection; import java.util.Set; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; @@ -75,10 +71,6 @@ return new AutoValue_ExternalId_Key(Strings.emptyToNull(scheme), id); } - public static ExternalId.Key from(AccountExternalId.Key externalIdKey) { - return parse(externalIdKey.get()); - } - /** * Parses an external ID key from a string in the format "scheme:id" or "id". * @@ -92,11 +84,6 @@ return create(externalId.substring(0, c), externalId.substring(c + 1)); } - public static Set<AccountExternalId.Key> toAccountExternalIdKeys( - Collection<ExternalId.Key> extIdKeys) { - return extIdKeys.stream().map(k -> k.asAccountExternalIdKey()).collect(toSet()); - } - public abstract @Nullable String scheme(); public abstract String id(); @@ -105,23 +92,17 @@ return scheme.equals(scheme()); } - public AccountExternalId.Key asAccountExternalIdKey() { - if (scheme() != null) { - return new AccountExternalId.Key(scheme(), id()); - } - return new AccountExternalId.Key(id()); - } - /** * Returns the SHA1 of the external ID that is used as note ID in the refs/meta/external-ids * notes branch. */ + @SuppressWarnings("deprecation") // Use Hashing.sha1 for compatibility. public ObjectId sha1() { return ObjectId.fromRaw(Hashing.sha1().hashString(get(), UTF_8).asBytes()); } /** - * Exports this external ID key as string with the format "scheme:id", or "id" id scheme is + * Exports this external ID key as string with the format "scheme:id", or "id" if scheme is * null. * * <p>This string representation is used as subsection name in the Git config file that stores @@ -215,35 +196,27 @@ throw invalidConfig( noteId, String.format( - "Expected exactly 1 %s section, found %d", + "Expected exactly 1 '%s' section, found %d", EXTERNAL_ID_SECTION, externalIdKeys.size())); } String externalIdKeyStr = Iterables.getOnlyElement(externalIdKeys); Key externalIdKey = Key.parse(externalIdKeyStr); if (externalIdKey == null) { - throw invalidConfig(noteId, String.format("Invalid external id: %s", externalIdKeyStr)); + throw invalidConfig(noteId, String.format("External ID %s is invalid", externalIdKeyStr)); } - String accountIdStr = - externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY); + if (!externalIdKey.sha1().getName().equals(noteId)) { + throw invalidConfig( + noteId, + String.format( + "SHA1 of external ID '%s' does not match note ID '%s'", externalIdKeyStr, noteId)); + } + String email = externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, EMAIL_KEY); String password = externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, PASSWORD_KEY); - if (accountIdStr == null) { - throw invalidConfig( - noteId, - String.format( - "Missing value for %s.%s.%s", EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY)); - } - Integer accountId = Ints.tryParse(accountIdStr); - if (accountId == null) { - throw invalidConfig( - noteId, - String.format( - "Value %s for %s.%s.%s is invalid, expected account ID", - accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY)); - } + int accountId = readAccountId(noteId, externalIdConfig, externalIdKeyStr); return new AutoValue_ExternalId( externalIdKey, @@ -252,32 +225,41 @@ Strings.emptyToNull(password)); } + private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr) + throws ConfigInvalidException { + String accountIdStr = + externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY); + if (accountIdStr == null) { + throw invalidConfig( + noteId, + String.format( + "Value for '%s.%s.%s' is missing, expected account ID", + EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY)); + } + + try { + int accountId = + externalIdConfig.getInt(EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY, -1); + if (accountId <= 0) { + throw invalidConfig( + noteId, + String.format( + "Value %s for '%s.%s.%s' is invalid, expected account ID", + accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY)); + } + return accountId; + } catch (IllegalArgumentException e) { + throw invalidConfig( + noteId, + String.format( + "Value %s for '%s.%s.%s' is invalid, expected account ID", + accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY)); + } + } + private static ConfigInvalidException invalidConfig(String noteId, String message) { return new ConfigInvalidException( - String.format("Invalid external id config for note %s: %s", noteId, message)); - } - - public static ExternalId from(AccountExternalId externalId) { - if (externalId == null) { - return null; - } - - return new AutoValue_ExternalId( - ExternalId.Key.parse(externalId.getExternalId()), - externalId.getAccountId(), - Strings.emptyToNull(externalId.getEmailAddress()), - Strings.emptyToNull(externalId.getPassword())); - } - - public static Set<ExternalId> from(Collection<AccountExternalId> externalIds) { - if (externalIds == null) { - return ImmutableSet.of(); - } - return externalIds.stream().map(ExternalId::from).collect(toSet()); - } - - public static Set<AccountExternalId> toAccountExternalIds(Collection<ExternalId> extIds) { - return extIds.stream().map(e -> e.asAccountExternalId()).collect(toSet()); + String.format("Invalid external ID config for note '%s': %s", noteId, message)); } public abstract Key key(); @@ -292,13 +274,6 @@ return key().isScheme(scheme); } - public AccountExternalId asAccountExternalId() { - AccountExternalId extId = new AccountExternalId(accountId(), key().asAccountExternalIdKey()); - extId.setEmailAddress(email()); - extId.setPassword(password()); - return extId; - } - /** * Exports this external ID as Git config file text. * @@ -321,7 +296,11 @@ public void writeToConfig(Config c) { String externalIdKey = key().get(); - c.setInt(EXTERNAL_ID_SECTION, externalIdKey, ACCOUNT_ID_KEY, accountId().get()); + // Do not use c.setInt(...) to write the account ID because c.setInt(...) persists integers + // that can be expressed in KiB as a unit strings, e.g. "1024000" is stored as "100k". Using + // c.setString(...) ensures that account IDs are human readable. + c.setString( + EXTERNAL_ID_SECTION, externalIdKey, ACCOUNT_ID_KEY, Integer.toString(accountId().get())); if (email() != null) { c.setString(EXTERNAL_ID_SECTION, externalIdKey, EMAIL_KEY, email()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java new file mode 100644 index 0000000..53a1cea --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCache.java
@@ -0,0 +1,100 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account.externalids; + +import com.google.gerrit.reviewdb.client.Account; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import org.eclipse.jgit.lib.ObjectId; + +/** Caches external IDs of all accounts */ +interface ExternalIdCache { + void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) + throws IOException; + + void onUpdate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) + throws IOException; + + void onReplace( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Account.Id accountId, + Collection<ExternalId> toRemove, + Collection<ExternalId> toAdd) + throws IOException; + + void onReplaceByKeys( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Account.Id accountId, + Collection<ExternalId.Key> toRemove, + Collection<ExternalId> toAdd) + throws IOException; + + void onReplaceByKeys( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Collection<ExternalId.Key> toRemove, + Collection<ExternalId> toAdd) + throws IOException; + + void onReplace( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Collection<ExternalId> toRemove, + Collection<ExternalId> toAdd) + throws IOException; + + void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extId) + throws IOException; + + void onRemoveByKeys( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Account.Id accountId, + Collection<ExternalId.Key> extIdKeys) + throws IOException; + + void onRemoveByKeys( + ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId.Key> extIdKeys) + throws IOException; + + Set<ExternalId> byAccount(Account.Id accountId) throws IOException; + + Set<ExternalId> byEmail(String email) throws IOException; + + default void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, ExternalId extId) + throws IOException { + onCreate(oldNotesRev, newNotesRev, Collections.singleton(extId)); + } + + default void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, ExternalId extId) + throws IOException { + onRemove(oldNotesRev, newNotesRev, Collections.singleton(extId)); + } + + default void onRemoveByKey( + ObjectId oldNotesRev, ObjectId newNotesRev, Account.Id accountId, ExternalId.Key extIdKey) + throws IOException { + onRemoveByKeys(oldNotesRev, newNotesRev, accountId, Collections.singleton(extIdKey)); + } + + default void onUpdate(ObjectId oldNotesRev, ObjectId newNotesRev, ExternalId updatedExtId) + throws IOException { + onUpdate(oldNotesRev, newNotesRev, Collections.singleton(updatedExtId)); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java new file mode 100644 index 0000000..2f68e98 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdCacheImpl.java
@@ -0,0 +1,300 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account.externalids; + +import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap; +import static java.util.stream.Collectors.toSet; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Strings; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableSetMultimap; +import com.google.common.collect.Iterables; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.gerrit.reviewdb.client.Account; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; +import org.eclipse.jgit.lib.ObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Caches external IDs of all accounts. The external IDs are always loaded from NoteDb. */ +@Singleton +class ExternalIdCacheImpl implements ExternalIdCache { + private static final Logger log = LoggerFactory.getLogger(ExternalIdCacheImpl.class); + + private final LoadingCache<ObjectId, AllExternalIds> extIdsByAccount; + private final ExternalIdReader externalIdReader; + private final Lock lock; + + @Inject + ExternalIdCacheImpl(ExternalIdReader externalIdReader) { + this.extIdsByAccount = + CacheBuilder.newBuilder() + // The cached data is potentially pretty large and we are always only interested + // in the latest value, hence the maximum cache size is set to 1. + // This can lead to extra cache loads in case of the following race: + // 1. thread 1 reads the notes ref at revision A + // 2. thread 2 updates the notes ref to revision B and stores the derived value + // for B in the cache + // 3. thread 1 attempts to read the data for revision A from the cache, and misses + // 4. later threads attempt to read at B + // In this race unneeded reloads are done in step 3 (reload from revision A) and + // step 4 (reload from revision B, because the value for revision B was lost when the + // reload from revision A was done, since the cache can hold only one entry). + // These reloads could be avoided by increasing the cache size to 2. However the race + // window between reading the ref and looking it up in the cache is small so that + // it's rare that this race happens. Therefore it's not worth to double the memory + // usage of this cache, just to avoid this. + .maximumSize(1) + .build(new Loader(externalIdReader)); + this.externalIdReader = externalIdReader; + this.lock = new ReentrantLock(true /* fair */); + } + + @Override + public void onCreate(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extIds) + throws IOException { + updateCache( + oldNotesRev, + newNotesRev, + m -> { + for (ExternalId extId : extIds) { + m.put(extId.accountId(), extId); + } + }); + } + + @Override + public void onRemove(ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> extIds) + throws IOException { + updateCache( + oldNotesRev, + newNotesRev, + m -> { + for (ExternalId extId : extIds) { + m.remove(extId.accountId(), extId); + } + }); + } + + @Override + public void onRemoveByKeys( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Account.Id accountId, + Collection<ExternalId.Key> extIdKeys) + throws IOException { + updateCache(oldNotesRev, newNotesRev, m -> removeKeys(m.get(accountId), extIdKeys)); + } + + @Override + public void onRemoveByKeys( + ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId.Key> extIdKeys) + throws IOException { + updateCache(oldNotesRev, newNotesRev, m -> removeKeys(m.values(), extIdKeys)); + } + + @Override + public void onUpdate( + ObjectId oldNotesRev, ObjectId newNotesRev, Collection<ExternalId> updatedExtIds) + throws IOException { + updateCache( + oldNotesRev, + newNotesRev, + m -> { + removeKeys(m.values(), updatedExtIds.stream().map(e -> e.key()).collect(toSet())); + for (ExternalId updatedExtId : updatedExtIds) { + m.put(updatedExtId.accountId(), updatedExtId); + } + }); + } + + @Override + public void onReplace( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Account.Id accountId, + Collection<ExternalId> toRemove, + Collection<ExternalId> toAdd) + throws IOException { + ExternalIdsUpdate.checkSameAccount(Iterables.concat(toRemove, toAdd), accountId); + + updateCache( + oldNotesRev, + newNotesRev, + m -> { + for (ExternalId extId : toRemove) { + m.remove(extId.accountId(), extId); + } + for (ExternalId extId : toAdd) { + m.put(extId.accountId(), extId); + } + }); + } + + @Override + public void onReplaceByKeys( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Account.Id accountId, + Collection<ExternalId.Key> toRemove, + Collection<ExternalId> toAdd) + throws IOException { + ExternalIdsUpdate.checkSameAccount(toAdd, accountId); + + updateCache( + oldNotesRev, + newNotesRev, + m -> { + removeKeys(m.get(accountId), toRemove); + for (ExternalId extId : toAdd) { + m.put(extId.accountId(), extId); + } + }); + } + + @Override + public void onReplaceByKeys( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Collection<ExternalId.Key> toRemove, + Collection<ExternalId> toAdd) + throws IOException { + updateCache( + oldNotesRev, + newNotesRev, + m -> { + removeKeys(m.values(), toRemove); + for (ExternalId extId : toAdd) { + m.put(extId.accountId(), extId); + } + }); + } + + @Override + public void onReplace( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Collection<ExternalId> toRemove, + Collection<ExternalId> toAdd) + throws IOException { + updateCache( + oldNotesRev, + newNotesRev, + m -> { + for (ExternalId extId : toRemove) { + m.remove(extId.accountId(), extId); + } + for (ExternalId extId : toAdd) { + m.put(extId.accountId(), extId); + } + }); + } + + @Override + public Set<ExternalId> byAccount(Account.Id accountId) throws IOException { + try { + return extIdsByAccount.get(externalIdReader.readRevision()).byAccount().get(accountId); + } catch (ExecutionException e) { + throw new IOException("Cannot list external ids by account", e); + } + } + + @Override + public Set<ExternalId> byEmail(String email) throws IOException { + try { + return extIdsByAccount.get(externalIdReader.readRevision()).byEmail().get(email); + } catch (ExecutionException e) { + throw new IOException("Cannot list external ids by email", e); + } + } + + private void updateCache( + ObjectId oldNotesRev, + ObjectId newNotesRev, + Consumer<Multimap<Account.Id, ExternalId>> update) { + lock.lock(); + try { + ListMultimap<Account.Id, ExternalId> m; + if (!ObjectId.zeroId().equals(oldNotesRev)) { + m = + MultimapBuilder.hashKeys() + .arrayListValues() + .build(extIdsByAccount.get(oldNotesRev).byAccount()); + } else { + m = MultimapBuilder.hashKeys().arrayListValues().build(); + } + update.accept(m); + extIdsByAccount.put(newNotesRev, AllExternalIds.create(m)); + } catch (ExecutionException e) { + log.warn("Cannot update external IDs", e); + } finally { + lock.unlock(); + } + } + + private static void removeKeys(Collection<ExternalId> ids, Collection<ExternalId.Key> toRemove) { + Collections2.transform(ids, e -> e.key()).removeAll(toRemove); + } + + private static class Loader extends CacheLoader<ObjectId, AllExternalIds> { + private final ExternalIdReader externalIdReader; + + Loader(ExternalIdReader externalIdReader) { + this.externalIdReader = externalIdReader; + } + + @Override + public AllExternalIds load(ObjectId notesRev) throws Exception { + Multimap<Account.Id, ExternalId> extIdsByAccount = + MultimapBuilder.hashKeys().arrayListValues().build(); + for (ExternalId extId : externalIdReader.all(notesRev)) { + extIdsByAccount.put(extId.accountId(), extId); + } + return AllExternalIds.create(extIdsByAccount); + } + } + + @AutoValue + abstract static class AllExternalIds { + static AllExternalIds create(Multimap<Account.Id, ExternalId> byAccount) { + ImmutableSetMultimap<String, ExternalId> byEmail = + byAccount + .values() + .stream() + .filter(e -> !Strings.isNullOrEmpty(e.email())) + .collect(toImmutableSetMultimap(ExternalId::email, e -> e)); + return new AutoValue_ExternalIdCacheImpl_AllExternalIds( + ImmutableSetMultimap.copyOf(byAccount), byEmail); + } + + public abstract ImmutableSetMultimap<Account.Id, ExternalId> byAccount(); + + public abstract ImmutableSetMultimap<String, ExternalId> byEmail(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java new file mode 100644 index 0000000..8c97144 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdModule.java
@@ -0,0 +1,25 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account.externalids; + +import com.google.inject.AbstractModule; + +public class ExternalIdModule extends AbstractModule { + @Override + protected void configure() { + bind(ExternalIdCacheImpl.class); + bind(ExternalIdCache.class).to(ExternalIdCacheImpl.class); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java new file mode 100644 index 0000000..ead2c1c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
@@ -0,0 +1,199 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account.externalids; + +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.metrics.Description; +import com.google.gerrit.metrics.Description.Units; +import com.google.gerrit.metrics.MetricMaker; +import com.google.gerrit.metrics.Timer0; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.notes.Note; +import org.eclipse.jgit.notes.NoteMap; +import org.eclipse.jgit.revwalk.RevWalk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Class to read external IDs from NoteDb. + * + * <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called + * refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content + * is a git config file that contains an external ID. It has exactly one externalId subsection with + * an accountId and optionally email and password: + * + * <pre> + * [externalId "username:jdoe"] + * accountId = 1003407 + * email = jdoe@example.com + * password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7 + * </pre> + */ +@Singleton +public class ExternalIdReader { + private static final Logger log = LoggerFactory.getLogger(ExternalIdReader.class); + + public static final int MAX_NOTE_SZ = 1 << 19; + + public static ObjectId readRevision(Repository repo) throws IOException { + Ref ref = repo.exactRef(RefNames.REFS_EXTERNAL_IDS); + return ref != null ? ref.getObjectId() : ObjectId.zeroId(); + } + + public static NoteMap readNoteMap(RevWalk rw, ObjectId rev) throws IOException { + if (!rev.equals(ObjectId.zeroId())) { + return NoteMap.read(rw.getObjectReader(), rw.parseCommit(rev)); + } + return NoteMap.newEmptyMap(); + } + + private final GitRepositoryManager repoManager; + private final AllUsersName allUsersName; + private boolean failOnLoad = false; + private final Timer0 readAllLatency; + + @Inject + ExternalIdReader( + GitRepositoryManager repoManager, AllUsersName allUsersName, MetricMaker metricMaker) { + this.repoManager = repoManager; + this.allUsersName = allUsersName; + this.readAllLatency = + metricMaker.newTimer( + "notedb/read_all_external_ids_latency", + new Description("Latency for reading all external IDs from NoteDb.") + .setCumulative() + .setUnit(Units.MILLISECONDS)); + } + + @VisibleForTesting + public void setFailOnLoad(boolean failOnLoad) { + this.failOnLoad = failOnLoad; + } + + ObjectId readRevision() throws IOException { + try (Repository repo = repoManager.openRepository(allUsersName)) { + return readRevision(repo); + } + } + + /** Reads and returns all external IDs. */ + Set<ExternalId> all() throws IOException { + checkReadEnabled(); + + try (Repository repo = repoManager.openRepository(allUsersName)) { + return all(repo, readRevision(repo)); + } + } + + /** + * Reads and returns all external IDs from the specified revision of the refs/meta/external-ids + * branch. + */ + Set<ExternalId> all(ObjectId rev) throws IOException { + checkReadEnabled(); + + try (Repository repo = repoManager.openRepository(allUsersName)) { + return all(repo, rev); + } + } + + /** Reads and returns all external IDs. */ + private Set<ExternalId> all(Repository repo, ObjectId rev) throws IOException { + if (rev.equals(ObjectId.zeroId())) { + return ImmutableSet.of(); + } + + try (Timer0.Context ctx = readAllLatency.start(); + RevWalk rw = new RevWalk(repo)) { + NoteMap noteMap = readNoteMap(rw, rev); + Set<ExternalId> extIds = new HashSet<>(); + for (Note note : noteMap) { + byte[] raw = + rw.getObjectReader().open(note.getData(), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ); + try { + extIds.add(ExternalId.parse(note.getName(), raw)); + } catch (Exception e) { + log.error(String.format("Ignoring invalid external ID note %s", note.getName()), e); + } + } + return extIds; + } + } + + /** Reads and returns the specified external ID. */ + @Nullable + ExternalId get(ExternalId.Key key) throws IOException, ConfigInvalidException { + checkReadEnabled(); + + try (Repository repo = repoManager.openRepository(allUsersName); + RevWalk rw = new RevWalk(repo)) { + ObjectId rev = readRevision(repo); + if (rev.equals(ObjectId.zeroId())) { + return null; + } + + return parse(key, rw, rev); + } + } + + /** Reads and returns the specified external ID from the given revision. */ + @Nullable + ExternalId get(ExternalId.Key key, ObjectId rev) throws IOException, ConfigInvalidException { + checkReadEnabled(); + + if (rev.equals(ObjectId.zeroId())) { + return null; + } + + try (Repository repo = repoManager.openRepository(allUsersName); + RevWalk rw = new RevWalk(repo)) { + return parse(key, rw, rev); + } + } + + private static ExternalId parse(ExternalId.Key key, RevWalk rw, ObjectId rev) + throws IOException, ConfigInvalidException { + NoteMap noteMap = readNoteMap(rw, rev); + ObjectId noteId = key.sha1(); + if (!noteMap.contains(noteId)) { + return null; + } + + byte[] raw = + rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ); + return ExternalId.parse(noteId.name(), raw); + } + + private void checkReadEnabled() throws IOException { + if (failOnLoad) { + throw new IOException("Reading from external IDs is disabled"); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIds.java new file mode 100644 index 0000000..5003a35 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIds.java
@@ -0,0 +1,80 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account.externalids; + +import static java.util.stream.Collectors.toSet; + +import com.google.gerrit.common.Nullable; +import com.google.gerrit.reviewdb.client.Account; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.ObjectId; + +/** + * Class to access external IDs. + * + * <p>The external IDs are either read from NoteDb or retrieved from the cache. + */ +@Singleton +public class ExternalIds { + private final ExternalIdReader externalIdReader; + private final ExternalIdCache externalIdCache; + + @Inject + public ExternalIds(ExternalIdReader externalIdReader, ExternalIdCache externalIdCache) { + this.externalIdReader = externalIdReader; + this.externalIdCache = externalIdCache; + } + + /** Returns all external IDs. */ + public Set<ExternalId> all() throws IOException { + return externalIdReader.all(); + } + + /** Returns all external IDs from the specified revision of the refs/meta/external-ids branch. */ + public Set<ExternalId> all(ObjectId rev) throws IOException { + return externalIdReader.all(rev); + } + + /** Returns the specified external ID. */ + @Nullable + public ExternalId get(ExternalId.Key key) throws IOException, ConfigInvalidException { + return externalIdReader.get(key); + } + + /** Returns the specified external ID from the given revision. */ + @Nullable + public ExternalId get(ExternalId.Key key, ObjectId rev) + throws IOException, ConfigInvalidException { + return externalIdReader.get(key, rev); + } + + /** Returns the external IDs of the specified account. */ + public Set<ExternalId> byAccount(Account.Id accountId) throws IOException { + return externalIdCache.byAccount(accountId); + } + + /** Returns the external IDs of the specified account that have the given scheme. */ + public Set<ExternalId> byAccount(Account.Id accountId, String scheme) throws IOException { + return byAccount(accountId).stream().filter(e -> e.key().isScheme(scheme)).collect(toSet()); + } + + public Set<ExternalId> byEmail(String email) throws IOException { + return externalIdCache.byEmail(email); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdsBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsBatchUpdate.java similarity index 82% rename from gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdsBatchUpdate.java rename to gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsBatchUpdate.java index 531e562..e35b0c3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdsBatchUpdate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsBatchUpdate.java
@@ -12,12 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.server.account; - -import static com.google.gerrit.server.account.ExternalId.toAccountExternalIds; +package com.google.gerrit.server.account.externalids; import com.google.common.collect.ImmutableSet; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.git.GitRepositoryManager; @@ -39,13 +36,14 @@ * * <p>For NoteDb all updates will result in a single commit to the refs/meta/external-ids branch. * This means callers can prepare many updates by invoking {@link #replace(ExternalId, ExternalId)} - * multiple times and when {@link ExternalIdsBatchUpdate#commit(ReviewDb, String)} is invoked a - * single NoteDb commit is created that contains all the prepared updates. + * multiple times and when {@link ExternalIdsBatchUpdate#commit(String)} is invoked a single NoteDb + * commit is created that contains all the prepared updates. */ public class ExternalIdsBatchUpdate { private final GitRepositoryManager repoManager; private final AllUsersName allUsersName; private final PersonIdent serverIdent; + private final ExternalIdCache externalIdCache; private final Set<ExternalId> toAdd = new HashSet<>(); private final Set<ExternalId> toDelete = new HashSet<>(); @@ -53,16 +51,18 @@ public ExternalIdsBatchUpdate( GitRepositoryManager repoManager, AllUsersName allUsersName, - @GerritPersonIdent PersonIdent serverIdent) { + @GerritPersonIdent PersonIdent serverIdent, + ExternalIdCache externalIdCache) { this.repoManager = repoManager; this.allUsersName = allUsersName; this.serverIdent = serverIdent; + this.externalIdCache = externalIdCache; } /** * Adds an external ID replacement to the batch. * - * <p>The actual replacement is only done when {@link #commit(ReviewDb, String)} is invoked. + * <p>The actual replacement is only done when {@link #commit(String)} is invoked. */ public void replace(ExternalId extIdToDelete, ExternalId extIdToAdd) { ExternalIdsUpdate.checkSameAccount(ImmutableSet.of(extIdToDelete, extIdToAdd)); @@ -82,21 +82,18 @@ * * <p>For NoteDb a single commit is created that contains all the external ID updates. */ - public void commit(ReviewDb db, String commitMessage) + public void commit(String commitMessage) throws IOException, OrmException, ConfigInvalidException { if (toDelete.isEmpty() && toAdd.isEmpty()) { return; } - db.accountExternalIds().delete(toAccountExternalIds(toDelete)); - db.accountExternalIds().insert(toAccountExternalIds(toAdd)); - try (Repository repo = repoManager.openRepository(allUsersName); RevWalk rw = new RevWalk(repo); ObjectInserter ins = repo.newObjectInserter()) { - ObjectId rev = ExternalIds.readRevision(repo); + ObjectId rev = ExternalIdReader.readRevision(repo); - NoteMap noteMap = ExternalIds.readNoteMap(rw, rev); + NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev); for (ExternalId extId : toDelete) { ExternalIdsUpdate.remove(rw, noteMap, extId); @@ -106,8 +103,10 @@ ExternalIdsUpdate.insert(rw, ins, noteMap, extId); } - ExternalIdsUpdate.commit( - repo, rw, ins, rev, noteMap, commitMessage, serverIdent, serverIdent); + ObjectId newRev = + ExternalIdsUpdate.commit( + repo, rw, ins, rev, noteMap, commitMessage, serverIdent, serverIdent); + externalIdCache.onReplace(rev, newRev, toDelete, toAdd); } toAdd.clear();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java new file mode 100644 index 0000000..f74210f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsConsistencyChecker.java
@@ -0,0 +1,155 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account.externalids; + +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; +import static java.util.stream.Collectors.joining; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; + +import com.google.common.collect.ListMultimap; +import com.google.common.collect.MultimapBuilder; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.HashedPassword; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.mail.send.OutgoingEmailValidator; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.codec.DecoderException; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.notes.Note; +import org.eclipse.jgit.notes.NoteMap; +import org.eclipse.jgit.revwalk.RevWalk; + +@Singleton +public class ExternalIdsConsistencyChecker { + private final GitRepositoryManager repoManager; + private final AllUsersName allUsers; + private final AccountCache accountCache; + private final OutgoingEmailValidator validator; + + @Inject + ExternalIdsConsistencyChecker( + GitRepositoryManager repoManager, + AllUsersName allUsers, + AccountCache accountCache, + OutgoingEmailValidator validator) { + this.repoManager = repoManager; + this.allUsers = allUsers; + this.accountCache = accountCache; + this.validator = validator; + } + + public List<ConsistencyProblemInfo> check() throws IOException { + try (Repository repo = repoManager.openRepository(allUsers)) { + return check(repo, ExternalIdReader.readRevision(repo)); + } + } + + public List<ConsistencyProblemInfo> check(ObjectId rev) throws IOException { + try (Repository repo = repoManager.openRepository(allUsers)) { + return check(repo, rev); + } + } + + private List<ConsistencyProblemInfo> check(Repository repo, ObjectId commit) throws IOException { + List<ConsistencyProblemInfo> problems = new ArrayList<>(); + + ListMultimap<String, ExternalId.Key> emails = + MultimapBuilder.hashKeys().arrayListValues().build(); + + try (RevWalk rw = new RevWalk(repo)) { + NoteMap noteMap = ExternalIdReader.readNoteMap(rw, commit); + for (Note note : noteMap) { + byte[] raw = + rw.getObjectReader() + .open(note.getData(), OBJ_BLOB) + .getCachedBytes(ExternalIdReader.MAX_NOTE_SZ); + try { + ExternalId extId = ExternalId.parse(note.getName(), raw); + problems.addAll(validateExternalId(extId)); + + if (extId.email() != null) { + emails.put(extId.email(), extId.key()); + } + } catch (ConfigInvalidException e) { + addError(String.format(e.getMessage()), problems); + } + } + } + + emails + .asMap() + .entrySet() + .stream() + .filter(e -> e.getValue().size() > 1) + .forEach( + e -> + addError( + String.format( + "Email '%s' is not unique, it's used by the following external IDs: %s", + e.getKey(), + e.getValue() + .stream() + .map(k -> "'" + k.get() + "'") + .sorted() + .collect(joining(", "))), + problems)); + + return problems; + } + + private List<ConsistencyProblemInfo> validateExternalId(ExternalId extId) { + List<ConsistencyProblemInfo> problems = new ArrayList<>(); + + if (accountCache.getOrNull(extId.accountId()) == null) { + addError( + String.format( + "External ID '%s' belongs to account that doesn't exist: %s", + extId.key().get(), extId.accountId().get()), + problems); + } + + if (extId.email() != null && !validator.isValid(extId.email())) { + addError( + String.format( + "External ID '%s' has an invalid email: %s", extId.key().get(), extId.email()), + problems); + } + + if (extId.password() != null && extId.isScheme(SCHEME_USERNAME)) { + try { + HashedPassword.decode(extId.password()); + } catch (DecoderException e) { + addError( + String.format( + "External ID '%s' has an invalid password: %s", extId.key().get(), e.getMessage()), + problems); + } + } + + return problems; + } + + private static void addError(String error, List<ConsistencyProblemInfo> problems) { + problems.add(new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, error)); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java new file mode 100644 index 0000000..2985504 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
@@ -0,0 +1,817 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.account.externalids; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.gerrit.server.account.externalids.ExternalIdReader.MAX_NOTE_SZ; +import static com.google.gerrit.server.account.externalids.ExternalIdReader.readNoteMap; +import static com.google.gerrit.server.account.externalids.ExternalIdReader.readRevision; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toSet; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.Constants.OBJ_TREE; + +import com.github.rholder.retry.RetryException; +import com.github.rholder.retry.Retryer; +import com.github.rholder.retry.RetryerBuilder; +import com.github.rholder.retry.StopStrategies; +import com.github.rholder.retry.WaitStrategies; +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Throwables; +import com.google.common.collect.Iterables; +import com.google.common.collect.Streams; +import com.google.common.util.concurrent.Runnables; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.metrics.Counter0; +import com.google.gerrit.metrics.Description; +import com.google.gerrit.metrics.MetricMaker; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.LockFailureException; +import com.google.gwtorm.server.OrmDuplicateKeyException; +import com.google.gwtorm.server.OrmException; +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.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.CommitBuilder; +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.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.notes.NoteMap; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Updates externalIds in ReviewDb and NoteDb. + * + * <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called + * refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content + * is a git config file that contains an external ID. It has exactly one externalId subsection with + * an accountId and optionally email and password: + * + * <pre> + * [externalId "username:jdoe"] + * accountId = 1003407 + * email = jdoe@example.com + * password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7 + * </pre> + * + * For NoteDb each method call results in one commit on refs/meta/external-ids branch. + * + * <p>On updating external IDs this class takes care to evict affected accounts from the account + * cache and thus triggers reindex for them. + */ +public class ExternalIdsUpdate { + private static final String COMMIT_MSG = "Update external IDs"; + + /** + * Factory to create an ExternalIdsUpdate instance for updating external IDs by the Gerrit server. + * + * <p>The Gerrit server identity will be used as author and committer for all commits that update + * the external IDs. + */ + @Singleton + public static class Server { + private final GitRepositoryManager repoManager; + private final AccountCache accountCache; + private final AllUsersName allUsersName; + private final MetricMaker metricMaker; + private final ExternalIds externalIds; + private final ExternalIdCache externalIdCache; + private final Provider<PersonIdent> serverIdent; + + @Inject + public Server( + GitRepositoryManager repoManager, + AccountCache accountCache, + AllUsersName allUsersName, + MetricMaker metricMaker, + ExternalIds externalIds, + ExternalIdCache externalIdCache, + @GerritPersonIdent Provider<PersonIdent> serverIdent) { + this.repoManager = repoManager; + this.accountCache = accountCache; + this.allUsersName = allUsersName; + this.metricMaker = metricMaker; + this.externalIds = externalIds; + this.externalIdCache = externalIdCache; + this.serverIdent = serverIdent; + } + + public ExternalIdsUpdate create() { + PersonIdent i = serverIdent.get(); + return new ExternalIdsUpdate( + repoManager, accountCache, allUsersName, metricMaker, externalIds, externalIdCache, i, i); + } + } + + /** + * Factory to create an ExternalIdsUpdate instance for updating external IDs by the Gerrit server. + * + * <p>Using this class no reindex will be performed for the affected accounts and they will also + * not be evicted from the account cache. + * + * <p>The Gerrit server identity will be used as author and committer for all commits that update + * the external IDs. + */ + @Singleton + public static class ServerNoReindex { + private final GitRepositoryManager repoManager; + private final AllUsersName allUsersName; + private final MetricMaker metricMaker; + private final ExternalIds externalIds; + private final ExternalIdCache externalIdCache; + private final Provider<PersonIdent> serverIdent; + + @Inject + public ServerNoReindex( + GitRepositoryManager repoManager, + AllUsersName allUsersName, + MetricMaker metricMaker, + ExternalIds externalIds, + ExternalIdCache externalIdCache, + @GerritPersonIdent Provider<PersonIdent> serverIdent) { + this.repoManager = repoManager; + this.allUsersName = allUsersName; + this.metricMaker = metricMaker; + this.externalIds = externalIds; + this.externalIdCache = externalIdCache; + this.serverIdent = serverIdent; + } + + public ExternalIdsUpdate create() { + PersonIdent i = serverIdent.get(); + return new ExternalIdsUpdate( + repoManager, null, allUsersName, metricMaker, externalIds, externalIdCache, i, i); + } + } + + /** + * Factory to create an ExternalIdsUpdate instance for updating external IDs by the current user. + * + * <p>The identity of the current user will be used as author for all commits that update the + * external IDs. The Gerrit server identity will be used as committer. + */ + @Singleton + public static class User { + private final GitRepositoryManager repoManager; + private final AccountCache accountCache; + private final AllUsersName allUsersName; + private final MetricMaker metricMaker; + private final ExternalIds externalIds; + private final ExternalIdCache externalIdCache; + private final Provider<PersonIdent> serverIdent; + private final Provider<IdentifiedUser> identifiedUser; + + @Inject + public User( + GitRepositoryManager repoManager, + AccountCache accountCache, + AllUsersName allUsersName, + MetricMaker metricMaker, + ExternalIds externalIds, + ExternalIdCache externalIdCache, + @GerritPersonIdent Provider<PersonIdent> serverIdent, + Provider<IdentifiedUser> identifiedUser) { + this.repoManager = repoManager; + this.accountCache = accountCache; + this.allUsersName = allUsersName; + this.metricMaker = metricMaker; + this.externalIds = externalIds; + this.externalIdCache = externalIdCache; + this.serverIdent = serverIdent; + this.identifiedUser = identifiedUser; + } + + public ExternalIdsUpdate create() { + PersonIdent i = serverIdent.get(); + return new ExternalIdsUpdate( + repoManager, + accountCache, + allUsersName, + metricMaker, + externalIds, + externalIdCache, + createPersonIdent(i, identifiedUser.get()), + i); + } + + private PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) { + return user.newCommitterIdent(ident.getWhen(), ident.getTimeZone()); + } + } + + @VisibleForTesting + public static RetryerBuilder<RefsMetaExternalIdsUpdate> retryerBuilder() { + return RetryerBuilder.<RefsMetaExternalIdsUpdate>newBuilder() + .retryIfException(e -> e instanceof LockFailureException) + .withWaitStrategy( + WaitStrategies.join( + WaitStrategies.exponentialWait(2, TimeUnit.SECONDS), + WaitStrategies.randomWait(50, TimeUnit.MILLISECONDS))) + .withStopStrategy(StopStrategies.stopAfterDelay(10, TimeUnit.SECONDS)); + } + + private static final Retryer<RefsMetaExternalIdsUpdate> RETRYER = retryerBuilder().build(); + + private final GitRepositoryManager repoManager; + @Nullable private final AccountCache accountCache; + private final AllUsersName allUsersName; + private final ExternalIds externalIds; + private final ExternalIdCache externalIdCache; + private final PersonIdent committerIdent; + private final PersonIdent authorIdent; + private final Runnable afterReadRevision; + private final Retryer<RefsMetaExternalIdsUpdate> retryer; + private final Counter0 updateCount; + + private ExternalIdsUpdate( + GitRepositoryManager repoManager, + @Nullable AccountCache accountCache, + AllUsersName allUsersName, + MetricMaker metricMaker, + ExternalIds externalIds, + ExternalIdCache externalIdCache, + PersonIdent committerIdent, + PersonIdent authorIdent) { + this( + repoManager, + accountCache, + allUsersName, + metricMaker, + externalIds, + externalIdCache, + committerIdent, + authorIdent, + Runnables.doNothing(), + RETRYER); + } + + @VisibleForTesting + public ExternalIdsUpdate( + GitRepositoryManager repoManager, + @Nullable AccountCache accountCache, + AllUsersName allUsersName, + MetricMaker metricMaker, + ExternalIds externalIds, + ExternalIdCache externalIdCache, + PersonIdent committerIdent, + PersonIdent authorIdent, + Runnable afterReadRevision, + Retryer<RefsMetaExternalIdsUpdate> retryer) { + this.repoManager = checkNotNull(repoManager, "repoManager"); + this.accountCache = accountCache; + this.allUsersName = checkNotNull(allUsersName, "allUsersName"); + this.committerIdent = checkNotNull(committerIdent, "committerIdent"); + this.externalIds = checkNotNull(externalIds, "externalIds"); + this.externalIdCache = checkNotNull(externalIdCache, "externalIdCache"); + this.authorIdent = checkNotNull(authorIdent, "authorIdent"); + this.afterReadRevision = checkNotNull(afterReadRevision, "afterReadRevision"); + this.retryer = checkNotNull(retryer, "retryer"); + this.updateCount = + metricMaker.newCounter( + "notedb/external_id_update_count", + new Description("Total number of external ID updates.").setRate().setUnit("updates")); + } + + /** + * Inserts a new external ID. + * + * <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}. + */ + public void insert(ExternalId extId) throws IOException, ConfigInvalidException, OrmException { + insert(Collections.singleton(extId)); + } + + /** + * Inserts new external IDs. + * + * <p>If any of the external ID already exists, the insert fails with {@link + * OrmDuplicateKeyException}. + */ + public void insert(Collection<ExternalId> extIds) + throws IOException, ConfigInvalidException, OrmException { + RefsMetaExternalIdsUpdate u = + updateNoteMap( + o -> { + for (ExternalId extId : extIds) { + insert(o.rw(), o.ins(), o.noteMap(), extId); + } + }); + externalIdCache.onCreate(u.oldRev(), u.newRev(), extIds); + evictAccounts(extIds); + } + + /** + * Inserts or updates an external ID. + * + * <p>If the external ID already exists, it is overwritten, otherwise it is inserted. + */ + public void upsert(ExternalId extId) throws IOException, ConfigInvalidException, OrmException { + upsert(Collections.singleton(extId)); + } + + /** + * Inserts or updates external IDs. + * + * <p>If any of the external IDs already exists, it is overwritten. New external IDs are inserted. + */ + public void upsert(Collection<ExternalId> extIds) + throws IOException, ConfigInvalidException, OrmException { + RefsMetaExternalIdsUpdate u = + updateNoteMap( + o -> { + for (ExternalId extId : extIds) { + upsert(o.rw(), o.ins(), o.noteMap(), extId); + } + }); + externalIdCache.onUpdate(u.oldRev(), u.newRev(), extIds); + evictAccounts(extIds); + } + + /** + * Deletes an external ID. + * + * @throws IllegalStateException is thrown if there is an existing external ID that has the same + * key, but otherwise doesn't match the specified external ID. + */ + public void delete(ExternalId extId) throws IOException, ConfigInvalidException, OrmException { + delete(Collections.singleton(extId)); + } + + /** + * Deletes external IDs. + * + * @throws IllegalStateException is thrown if there is an existing external ID that has the same + * key as any of the external IDs that should be deleted, but otherwise doesn't match the that + * external ID. + */ + public void delete(Collection<ExternalId> extIds) + throws IOException, ConfigInvalidException, OrmException { + RefsMetaExternalIdsUpdate u = + updateNoteMap( + o -> { + for (ExternalId extId : extIds) { + remove(o.rw(), o.noteMap(), extId); + } + }); + externalIdCache.onRemove(u.oldRev(), u.newRev(), extIds); + evictAccounts(extIds); + } + + /** + * Delete an external ID by key. + * + * @throws IllegalStateException is thrown if the external ID does not belong to the specified + * account. + */ + public void delete(Account.Id accountId, ExternalId.Key extIdKey) + throws IOException, ConfigInvalidException, OrmException { + delete(accountId, Collections.singleton(extIdKey)); + } + + /** + * Delete external IDs by external ID key. + * + * @throws IllegalStateException is thrown if any of the external IDs does not belong to the + * specified account. + */ + public void delete(Account.Id accountId, Collection<ExternalId.Key> extIdKeys) + throws IOException, ConfigInvalidException, OrmException { + RefsMetaExternalIdsUpdate u = + updateNoteMap( + o -> { + for (ExternalId.Key extIdKey : extIdKeys) { + remove(o.rw(), o.noteMap(), extIdKey, accountId); + } + }); + externalIdCache.onRemoveByKeys(u.oldRev(), u.newRev(), accountId, extIdKeys); + evictAccount(accountId); + } + + /** + * Delete external IDs by external ID key. + * + * <p>The external IDs are deleted regardless of which account they belong to. + */ + public void deleteByKeys(Collection<ExternalId.Key> extIdKeys) + throws IOException, ConfigInvalidException, OrmException { + Set<ExternalId> deletedExtIds = new HashSet<>(); + RefsMetaExternalIdsUpdate u = + updateNoteMap( + o -> { + for (ExternalId.Key extIdKey : extIdKeys) { + ExternalId extId = remove(o.rw(), o.noteMap(), extIdKey, null); + if (extId != null) { + deletedExtIds.add(extId); + } + } + }); + externalIdCache.onRemoveByKeys(u.oldRev(), u.newRev(), extIdKeys); + evictAccounts(deletedExtIds); + } + + /** Deletes all external IDs of the specified account. */ + public void deleteAll(Account.Id accountId) + throws IOException, ConfigInvalidException, OrmException { + delete(externalIds.byAccount(accountId)); + } + + /** + * Replaces external IDs for an account by external ID keys. + * + * <p>Deletion of external IDs is done before adding the new external IDs. This means if an + * external ID key is specified for deletion and an external ID with the same key is specified to + * be added, the old external ID with that key is deleted first and then the new external ID is + * added (so the external ID for that key is replaced). + * + * @throws IllegalStateException is thrown if any of the specified external IDs does not belong to + * the specified account. + */ + public void replace( + Account.Id accountId, Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd) + throws IOException, ConfigInvalidException, OrmException { + checkSameAccount(toAdd, accountId); + + RefsMetaExternalIdsUpdate u = + updateNoteMap( + o -> { + for (ExternalId.Key extIdKey : toDelete) { + remove(o.rw(), o.noteMap(), extIdKey, accountId); + } + + for (ExternalId extId : toAdd) { + insert(o.rw(), o.ins(), o.noteMap(), extId); + } + }); + externalIdCache.onReplaceByKeys(u.oldRev(), u.newRev(), accountId, toDelete, toAdd); + evictAccount(accountId); + } + + /** + * Replaces external IDs for an account by external ID keys. + * + * <p>Deletion of external IDs is done before adding the new external IDs. This means if an + * external ID key is specified for deletion and an external ID with the same key is specified to + * be added, the old external ID with that key is deleted first and then the new external ID is + * added (so the external ID for that key is replaced). + * + * <p>The external IDs are replaced regardless of which account they belong to. + */ + public void replaceByKeys(Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd) + throws IOException, ConfigInvalidException, OrmException { + Set<ExternalId> deletedExtIds = new HashSet<>(); + RefsMetaExternalIdsUpdate u = + updateNoteMap( + o -> { + for (ExternalId.Key extIdKey : toDelete) { + ExternalId extId = remove(o.rw(), o.noteMap(), extIdKey, null); + if (extId != null) { + deletedExtIds.add(extId); + } + } + + for (ExternalId extId : toAdd) { + insert(o.rw(), o.ins(), o.noteMap(), extId); + } + }); + externalIdCache.onReplaceByKeys(u.oldRev(), u.newRev(), toDelete, toAdd); + evictAccounts(Streams.concat(deletedExtIds.stream(), toAdd.stream()).collect(toSet())); + } + + /** + * Replaces an external ID. + * + * @throws IllegalStateException is thrown if the specified external IDs belong to different + * accounts. + */ + public void replace(ExternalId toDelete, ExternalId toAdd) + throws IOException, ConfigInvalidException, OrmException { + replace(Collections.singleton(toDelete), Collections.singleton(toAdd)); + } + + /** + * Replaces external IDs. + * + * <p>Deletion of external IDs is done before adding the new external IDs. This means if an + * external ID is specified for deletion and an external ID with the same key is specified to be + * added, the old external ID with that key is deleted first and then the new external ID is added + * (so the external ID for that key is replaced). + * + * @throws IllegalStateException is thrown if the specified external IDs belong to different + * accounts. + */ + public void replace(Collection<ExternalId> toDelete, Collection<ExternalId> toAdd) + throws IOException, ConfigInvalidException, OrmException { + Account.Id accountId = checkSameAccount(Iterables.concat(toDelete, toAdd)); + if (accountId == null) { + // toDelete and toAdd are empty -> nothing to do + return; + } + + replace(accountId, toDelete.stream().map(e -> e.key()).collect(toSet()), toAdd); + } + + /** + * Checks that all specified external IDs belong to the same account. + * + * @return the ID of the account to which all specified external IDs belong. + */ + public static Account.Id checkSameAccount(Iterable<ExternalId> extIds) { + return checkSameAccount(extIds, null); + } + + /** + * Checks that all specified external IDs belong to specified account. If no account is specified + * it is checked that all specified external IDs belong to the same account. + * + * @return the ID of the account to which all specified external IDs belong. + */ + public static Account.Id checkSameAccount( + Iterable<ExternalId> extIds, @Nullable Account.Id accountId) { + for (ExternalId extId : extIds) { + if (accountId == null) { + accountId = extId.accountId(); + continue; + } + checkState( + accountId.equals(extId.accountId()), + "external id %s belongs to account %s, expected account %s", + extId.key().get(), + extId.accountId().get(), + accountId.get()); + } + return accountId; + } + + /** + * Inserts a new external ID and sets it in the note map. + * + * <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}. + */ + public static void insert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId) + throws OrmDuplicateKeyException, ConfigInvalidException, IOException { + if (noteMap.contains(extId.key().sha1())) { + throw new OrmDuplicateKeyException( + String.format("external id %s already exists", extId.key().get())); + } + upsert(rw, ins, noteMap, extId); + } + + /** + * Insert or updates an new external ID and sets it in the note map. + * + * <p>If the external ID already exists it is overwritten. + */ + public static void upsert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId) + throws IOException, ConfigInvalidException { + ObjectId noteId = extId.key().sha1(); + Config c = new Config(); + if (noteMap.contains(extId.key().sha1())) { + byte[] raw = + rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ); + try { + c.fromText(new String(raw, UTF_8)); + } catch (ConfigInvalidException e) { + throw new ConfigInvalidException( + String.format("Invalid external id config for note %s: %s", noteId, e.getMessage())); + } + } + extId.writeToConfig(c); + byte[] raw = c.toText().getBytes(UTF_8); + ObjectId dataBlob = ins.insert(OBJ_BLOB, raw); + noteMap.set(noteId, dataBlob); + } + + /** + * Removes an external ID from the note map. + * + * @throws IllegalStateException is thrown if there is an existing external ID that has the same + * key, but otherwise doesn't match the specified external ID. + */ + public static void remove(RevWalk rw, NoteMap noteMap, ExternalId extId) + throws IOException, ConfigInvalidException { + ObjectId noteId = extId.key().sha1(); + if (!noteMap.contains(noteId)) { + return; + } + + byte[] raw = + rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ); + ExternalId actualExtId = ExternalId.parse(noteId.name(), raw); + checkState( + extId.equals(actualExtId), + "external id %s should be removed, but it's not matching the actual external id %s", + extId.toString(), + actualExtId.toString()); + noteMap.remove(noteId); + } + + /** + * Removes an external ID from the note map by external ID key. + * + * @throws IllegalStateException is thrown if an expected account ID is provided and an external + * ID with the specified key exists, but belongs to another account. + * @return the external ID that was removed, {@code null} if no external ID with the specified key + * exists + */ + private static ExternalId remove( + RevWalk rw, NoteMap noteMap, ExternalId.Key extIdKey, Account.Id expectedAccountId) + throws IOException, ConfigInvalidException { + ObjectId noteId = extIdKey.sha1(); + if (!noteMap.contains(noteId)) { + return null; + } + + byte[] raw = + rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ); + ExternalId extId = ExternalId.parse(noteId.name(), raw); + if (expectedAccountId != null) { + checkState( + expectedAccountId.equals(extId.accountId()), + "external id %s should be removed for account %s," + + " but external id belongs to account %s", + extIdKey.get(), + expectedAccountId.get(), + extId.accountId().get()); + } + noteMap.remove(noteId); + return extId; + } + + private RefsMetaExternalIdsUpdate updateNoteMap(MyConsumer<OpenRepo> update) + throws IOException, ConfigInvalidException, OrmException { + try { + return retryer.call( + () -> { + try (Repository repo = repoManager.openRepository(allUsersName); + ObjectInserter ins = repo.newObjectInserter()) { + ObjectId rev = readRevision(repo); + + afterReadRevision.run(); + + try (RevWalk rw = new RevWalk(repo)) { + NoteMap noteMap = readNoteMap(rw, rev); + update.accept(OpenRepo.create(repo, rw, ins, noteMap)); + + return commit(repo, rw, ins, rev, noteMap); + } + } + }); + } catch (ExecutionException | RetryException e) { + if (e.getCause() != null) { + Throwables.throwIfInstanceOf(e.getCause(), IOException.class); + Throwables.throwIfInstanceOf(e.getCause(), ConfigInvalidException.class); + Throwables.throwIfInstanceOf(e.getCause(), OrmException.class); + } + throw new OrmException(e); + } + } + + private RefsMetaExternalIdsUpdate commit( + Repository repo, RevWalk rw, ObjectInserter ins, ObjectId rev, NoteMap noteMap) + throws IOException { + ObjectId newRev = commit(repo, rw, ins, rev, noteMap, COMMIT_MSG, committerIdent, authorIdent); + updateCount.increment(); + return RefsMetaExternalIdsUpdate.create(rev, newRev); + } + + /** Commits updates to the external IDs. */ + public static ObjectId commit( + Repository repo, + RevWalk rw, + ObjectInserter ins, + ObjectId rev, + NoteMap noteMap, + String commitMessage, + PersonIdent committerIdent, + PersonIdent authorIdent) + throws IOException { + CommitBuilder cb = new CommitBuilder(); + cb.setMessage(commitMessage); + cb.setTreeId(noteMap.writeTree(ins)); + cb.setAuthor(authorIdent); + cb.setCommitter(committerIdent); + if (!rev.equals(ObjectId.zeroId())) { + cb.setParentId(rev); + } else { + cb.setParentIds(); // Ref is currently nonexistent, commit has no parents. + } + if (cb.getTreeId() == null) { + if (rev.equals(ObjectId.zeroId())) { + cb.setTreeId(emptyTree(ins)); // No parent, assume empty tree. + } else { + RevCommit p = rw.parseCommit(rev); + cb.setTreeId(p.getTree()); // Copy tree from parent. + } + } + ObjectId commitId = ins.insert(cb); + ins.flush(); + + RefUpdate u = repo.updateRef(RefNames.REFS_EXTERNAL_IDS); + u.setRefLogIdent(committerIdent); + u.setRefLogMessage("Update external IDs", false); + u.setExpectedOldObjectId(rev); + u.setNewObjectId(commitId); + RefUpdate.Result res = u.update(); + switch (res) { + case NEW: + case FAST_FORWARD: + case NO_CHANGE: + case RENAMED: + case FORCED: + break; + case LOCK_FAILURE: + throw new LockFailureException("Updating external IDs failed with " + res); + case IO_FAILURE: + case NOT_ATTEMPTED: + case REJECTED: + case REJECTED_CURRENT_BRANCH: + default: + throw new IOException("Updating external IDs failed with " + res); + } + return rw.parseCommit(commitId); + } + + private static ObjectId emptyTree(ObjectInserter ins) throws IOException { + return ins.insert(OBJ_TREE, new byte[] {}); + } + + private void evictAccount(Account.Id accountId) throws IOException { + if (accountCache != null) { + accountCache.evict(accountId); + } + } + + private void evictAccounts(Collection<ExternalId> extIds) throws IOException { + if (accountCache == null) { + return; + } + + for (Account.Id id : extIds.stream().map(ExternalId::accountId).collect(toSet())) { + accountCache.evict(id); + } + } + + @FunctionalInterface + private static interface MyConsumer<T> { + void accept(T t) throws IOException, ConfigInvalidException, OrmException; + } + + @AutoValue + abstract static class OpenRepo { + static OpenRepo create(Repository repo, RevWalk rw, ObjectInserter ins, NoteMap noteMap) { + return new AutoValue_ExternalIdsUpdate_OpenRepo(repo, rw, ins, noteMap); + } + + abstract Repository repo(); + + abstract RevWalk rw(); + + abstract ObjectInserter ins(); + + abstract NoteMap noteMap(); + } + + @VisibleForTesting + @AutoValue + public abstract static class RefsMetaExternalIdsUpdate { + static RefsMetaExternalIdsUpdate create(ObjectId oldRev, ObjectId newRev) { + return new AutoValue_ExternalIdsUpdate_RefsMetaExternalIdsUpdate(oldRev, newRev); + } + + abstract ObjectId oldRev(); + + abstract ObjectId newRev(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/ApiUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/ApiUtil.java new file mode 100644 index 0000000..c5b8b12 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/ApiUtil.java
@@ -0,0 +1,39 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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; + +import com.google.common.base.Throwables; +import com.google.gerrit.extensions.restapi.RestApiException; + +/** Static utilities for API implementations. */ +public class ApiUtil { + /** + * Convert an exception encountered during API execution to a {@link RestApiException}. + * + * @param msg message to be used in the case where a new {@code RestApiException} is wrapped + * around {@code e}. + * @param e exception being handled. + * @return {@code e} if it is already a {@code RestApiException}, otherwise a new {@code + * RestApiException} wrapped around {@code e}. + * @throws RuntimeException if {@code e} is a runtime exception, it is rethrown as-is. + */ + public static RestApiException asRestApiException(String msg, Exception e) + throws RuntimeException { + Throwables.throwIfUnchecked(e); + return e instanceof RestApiException ? (RestApiException) e : new RestApiException(msg, e); + } + + private ApiUtil() {} +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java index 430b6b7..64760a65 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -14,10 +14,10 @@ package com.google.gerrit.server.api.accounts; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; import static javax.servlet.http.HttpServletResponse.SC_OK; import com.google.gerrit.common.RawInputUtil; -import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.extensions.api.accounts.AccountApi; import com.google.gerrit.extensions.api.accounts.EmailInput; import com.google.gerrit.extensions.api.accounts.GpgKeyApi; @@ -38,7 +38,6 @@ import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.TopLevelResource; -import com.google.gerrit.server.GpgException; import com.google.gerrit.server.account.AccountLoader; import com.google.gerrit.server.account.AccountResource; import com.google.gerrit.server.account.AddSshKey; @@ -71,14 +70,11 @@ import com.google.gerrit.server.account.Stars; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.ChangesCollection; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import java.io.IOException; import java.util.List; import java.util.Map; import java.util.SortedSet; -import org.eclipse.jgit.errors.ConfigInvalidException; public class AccountApiImpl implements AccountApi { interface Factory { @@ -202,8 +198,8 @@ AccountInfo ai = accountLoader.get(account.getUser().getAccountId()); accountLoader.fill(); return ai; - } catch (OrmException e) { - throw new RestApiException("Cannot parse change", e); + } catch (Exception e) { + throw asRestApiException("Cannot parse change", e); } } @@ -221,8 +217,8 @@ } else { deleteActive.apply(account, new DeleteActive.Input()); } - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot set active", e); + } catch (Exception e) { + throw asRestApiException("Cannot set active", e); } } @@ -234,15 +230,19 @@ @Override public GeneralPreferencesInfo getPreferences() throws RestApiException { - return getPreferences.apply(account); + try { + return getPreferences.apply(account); + } catch (Exception e) { + throw asRestApiException("Cannot get preferences", e); + } } @Override public GeneralPreferencesInfo setPreferences(GeneralPreferencesInfo in) throws RestApiException { try { return setPreferences.apply(account, in); - } catch (IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot set preferences", e); + } catch (Exception e) { + throw asRestApiException("Cannot set preferences", e); } } @@ -250,8 +250,8 @@ public DiffPreferencesInfo getDiffPreferences() throws RestApiException { try { return getDiffPreferences.apply(account); - } catch (IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot query diff preferences", e); + } catch (Exception e) { + throw asRestApiException("Cannot query diff preferences", e); } } @@ -259,8 +259,8 @@ public DiffPreferencesInfo setDiffPreferences(DiffPreferencesInfo in) throws RestApiException { try { return setDiffPreferences.apply(account, in); - } catch (IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot set diff preferences", e); + } catch (Exception e) { + throw asRestApiException("Cannot set diff preferences", e); } } @@ -268,8 +268,8 @@ public EditPreferencesInfo getEditPreferences() throws RestApiException { try { return getEditPreferences.apply(account); - } catch (IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot query edit preferences", e); + } catch (Exception e) { + throw asRestApiException("Cannot query edit preferences", e); } } @@ -277,8 +277,8 @@ public EditPreferencesInfo setEditPreferences(EditPreferencesInfo in) throws RestApiException { try { return setEditPreferences.apply(account, in); - } catch (IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot set edit preferences", e); + } catch (Exception e) { + throw asRestApiException("Cannot set edit preferences", e); } } @@ -286,8 +286,8 @@ public List<ProjectWatchInfo> getWatchedProjects() throws RestApiException { try { return getWatchedProjects.apply(account); - } catch (OrmException | IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot get watched projects", e); + } catch (Exception e) { + throw asRestApiException("Cannot get watched projects", e); } } @@ -296,8 +296,8 @@ throws RestApiException { try { return postWatchedProjects.apply(account, in); - } catch (OrmException | IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot update watched projects", e); + } catch (Exception e) { + throw asRestApiException("Cannot update watched projects", e); } } @@ -305,8 +305,8 @@ public void deleteWatchedProjects(List<ProjectWatchInfo> in) throws RestApiException { try { deleteWatchedProjects.apply(account, in); - } catch (OrmException | IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot delete watched projects", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete watched projects", e); } } @@ -316,8 +316,8 @@ ChangeResource rsrc = changes.parse(TopLevelResource.INSTANCE, IdString.fromUrl(changeId)); starredChangesCreate.setChange(rsrc); starredChangesCreate.apply(account, new StarredChanges.EmptyInput()); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot star change", e); + } catch (Exception e) { + throw asRestApiException("Cannot star change", e); } } @@ -328,8 +328,8 @@ AccountResource.StarredChange starredChange = new AccountResource.StarredChange(account.getUser(), rsrc); starredChangesDelete.apply(starredChange, new StarredChanges.EmptyInput()); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot unstar change", e); + } catch (Exception e) { + throw asRestApiException("Cannot unstar change", e); } } @@ -338,8 +338,8 @@ try { AccountResource.Star rsrc = stars.parse(account, IdString.fromUrl(changeId)); starsPost.apply(rsrc, input); - } catch (OrmException e) { - throw new RestApiException("Cannot post stars", e); + } catch (Exception e) { + throw asRestApiException("Cannot post stars", e); } } @@ -348,8 +348,8 @@ try { AccountResource.Star rsrc = stars.parse(account, IdString.fromUrl(changeId)); return starsGet.apply(rsrc); - } catch (OrmException e) { - throw new RestApiException("Cannot get stars", e); + } catch (Exception e) { + throw asRestApiException("Cannot get stars", e); } } @@ -357,8 +357,8 @@ public List<ChangeInfo> getStarredChanges() throws RestApiException { try { return stars.list().apply(account); - } catch (OrmException e) { - throw new RestApiException("Cannot get starred changes", e); + } catch (Exception e) { + throw asRestApiException("Cannot get starred changes", e); } } @@ -372,8 +372,8 @@ AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), input.email); try { createEmailFactory.create(input.email).apply(rsrc, input); - } catch (EmailException | OrmException | IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot add email", e); + } catch (Exception e) { + throw asRestApiException("Cannot add email", e); } } @@ -382,8 +382,8 @@ AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), email); try { deleteEmail.apply(rsrc, null); - } catch (OrmException | IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot delete email", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete email", e); } } @@ -392,8 +392,8 @@ PutStatus.Input in = new PutStatus.Input(status); try { putStatus.apply(account, in); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot set status", e); + } catch (Exception e) { + throw asRestApiException("Cannot set status", e); } } @@ -401,8 +401,8 @@ public List<SshKeyInfo> listSshKeys() throws RestApiException { try { return getSshKeys.apply(account); - } catch (OrmException | IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot list SSH keys", e); + } catch (Exception e) { + throw asRestApiException("Cannot list SSH keys", e); } } @@ -412,8 +412,8 @@ in.raw = RawInputUtil.create(key); try { return addSshKey.apply(account, in).value(); - } catch (OrmException | IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot add SSH key", e); + } catch (Exception e) { + throw asRestApiException("Cannot add SSH key", e); } } @@ -423,8 +423,8 @@ AccountResource.SshKey sshKeyRes = sshKeys.parse(account, IdString.fromDecoded(Integer.toString(seq))); deleteSshKey.apply(sshKeyRes, null); - } catch (OrmException | IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot delete SSH key", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete SSH key", e); } } @@ -432,8 +432,8 @@ public Map<String, GpgKeyInfo> listGpgKeys() throws RestApiException { try { return gpgApiAdapter.listGpgKeys(account); - } catch (GpgException e) { - throw new RestApiException("Cannot list GPG keys", e); + } catch (Exception e) { + throw asRestApiException("Cannot list GPG keys", e); } } @@ -442,8 +442,8 @@ throws RestApiException { try { return gpgApiAdapter.putGpgKeys(account, add, delete); - } catch (GpgException e) { - throw new RestApiException("Cannot add GPG key", e); + } catch (Exception e) { + throw asRestApiException("Cannot add GPG key", e); } } @@ -451,8 +451,8 @@ public GpgKeyApi gpgKey(String id) throws RestApiException { try { return gpgApiAdapter.gpgKey(account, IdString.fromDecoded(id)); - } catch (GpgException e) { - throw new RestApiException("Cannot get PGP key", e); + } catch (Exception e) { + throw asRestApiException("Cannot get PGP key", e); } } @@ -467,8 +467,8 @@ AgreementInput input = new AgreementInput(); input.name = agreementName; putAgreement.apply(account, input); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot sign agreement", e); + } catch (Exception e) { + throw asRestApiException("Cannot sign agreement", e); } } @@ -476,8 +476,8 @@ public void index() throws RestApiException { try { index.apply(account, new Index.Input()); - } catch (IOException e) { - throw new RestApiException("Cannot index account", e); + } catch (Exception e) { + throw asRestApiException("Cannot index account", e); } } @@ -485,8 +485,8 @@ public List<AccountExternalIdInfo> getExternalIds() throws RestApiException { try { return getExternalIds.apply(account); - } catch (OrmException e) { - throw new RestApiException("Cannot get external IDs", e); + } catch (Exception e) { + throw asRestApiException("Cannot get external IDs", e); } } @@ -494,8 +494,8 @@ public void deleteExternalIds(List<String> externalIds) throws RestApiException { try { deleteExternalIds.apply(account, externalIds); - } catch (IOException | OrmException | ConfigInvalidException e) { - throw new RestApiException("Cannot delete external IDs", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete external IDs", e); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java index 2d90853..2f8dee6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountExternalIdCreator.java
@@ -15,7 +15,7 @@ package com.google.gerrit.server.api.accounts; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import java.util.List; public interface AccountExternalIdCreator {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java index 498b720..5257aec 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/accounts/AccountsImpl.java
@@ -15,7 +15,7 @@ package com.google.gerrit.server.api.accounts; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; import com.google.gerrit.extensions.api.accounts.AccountApi; import com.google.gerrit.extensions.api.accounts.AccountInput; @@ -32,18 +32,18 @@ import com.google.gerrit.server.account.AccountsCollection; import com.google.gerrit.server.account.CreateAccount; import com.google.gerrit.server.account.QueryAccounts; -import com.google.gwtorm.server.OrmException; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import java.io.IOException; import java.util.List; -import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton public class AccountsImpl implements Accounts { private final AccountsCollection accounts; private final AccountApiImpl.Factory api; + private final PermissionBackend permissionBackend; private final Provider<CurrentUser> self; private final CreateAccount.Factory createAccount; private final Provider<QueryAccounts> queryAccountsProvider; @@ -52,11 +52,13 @@ AccountsImpl( AccountsCollection accounts, AccountApiImpl.Factory api, + PermissionBackend permissionBackend, Provider<CurrentUser> self, CreateAccount.Factory createAccount, Provider<QueryAccounts> queryAccountsProvider) { this.accounts = accounts; this.api = api; + this.permissionBackend = permissionBackend; this.self = self; this.createAccount = createAccount; this.queryAccountsProvider = queryAccountsProvider; @@ -66,8 +68,8 @@ public AccountApi id(String id) throws RestApiException { try { return api.create(accounts.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(id))); - } catch (OrmException e) { - throw new RestApiException("Cannot parse change", e); + } catch (Exception e) { + throw asRestApiException("Cannot parse change", e); } } @@ -96,13 +98,13 @@ if (checkNotNull(in, "AccountInput").username == null) { throw new BadRequestException("AccountInput must specify username"); } - checkRequiresCapability(self, null, CreateAccount.class); try { - AccountInfo info = - createAccount.create(in.username).apply(TopLevelResource.INSTANCE, in).value(); + CreateAccount impl = createAccount.create(in.username); + permissionBackend.user(self).checkAny(GlobalPermission.fromAnnotation(impl.getClass())); + AccountInfo info = impl.apply(TopLevelResource.INSTANCE, in).value(); return id(info._accountId); - } catch (OrmException | IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot create account " + in.username, e); + } catch (Exception e) { + throw asRestApiException("Cannot create account " + in.username, e); } } @@ -128,8 +130,8 @@ myQueryAccounts.setQuery(r.getQuery()); myQueryAccounts.setLimit(r.getLimit()); return myQueryAccounts.apply(TopLevelResource.INSTANCE); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve suggested accounts", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve suggested accounts", e); } } @@ -158,8 +160,8 @@ myQueryAccounts.addOption(option); } return myQueryAccounts.apply(TopLevelResource.INSTANCE); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve suggested accounts", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve suggested accounts", e); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java index a0babe1..4f4af54 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -14,9 +14,12 @@ package com.google.gerrit.server.api.changes; -import com.google.gerrit.common.errors.EmailException; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + +import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.api.changes.AbandonInput; import com.google.gerrit.extensions.api.changes.AddReviewerInput; +import com.google.gerrit.extensions.api.changes.AddReviewerResult; import com.google.gerrit.extensions.api.changes.AssigneeInput; import com.google.gerrit.extensions.api.changes.ChangeApi; import com.google.gerrit.extensions.api.changes.ChangeEditApi; @@ -51,34 +54,41 @@ import com.google.gerrit.server.change.CreateMergePatchSet; import com.google.gerrit.server.change.DeleteAssignee; import com.google.gerrit.server.change.DeleteChange; +import com.google.gerrit.server.change.DeletePrivate; import com.google.gerrit.server.change.GetAssignee; import com.google.gerrit.server.change.GetHashtags; import com.google.gerrit.server.change.GetPastAssignees; import com.google.gerrit.server.change.GetTopic; +import com.google.gerrit.server.change.Ignore; import com.google.gerrit.server.change.Index; import com.google.gerrit.server.change.ListChangeComments; import com.google.gerrit.server.change.ListChangeDrafts; import com.google.gerrit.server.change.ListChangeRobotComments; import com.google.gerrit.server.change.Move; +import com.google.gerrit.server.change.Mute; import com.google.gerrit.server.change.PostHashtags; +import com.google.gerrit.server.change.PostPrivate; import com.google.gerrit.server.change.PostReviewers; import com.google.gerrit.server.change.PublishDraftPatchSet; import com.google.gerrit.server.change.PutAssignee; +import com.google.gerrit.server.change.PutMessage; import com.google.gerrit.server.change.PutTopic; import com.google.gerrit.server.change.Rebase; import com.google.gerrit.server.change.Restore; import com.google.gerrit.server.change.Revert; import com.google.gerrit.server.change.Reviewers; import com.google.gerrit.server.change.Revisions; +import com.google.gerrit.server.change.SetPrivateOp; +import com.google.gerrit.server.change.SetReadyForReview; +import com.google.gerrit.server.change.SetWorkInProgress; import com.google.gerrit.server.change.SubmittedTogether; import com.google.gerrit.server.change.SuggestChangeReviewers; -import com.google.gerrit.server.project.InvalidChangeOperationException; -import com.google.gerrit.server.update.UpdateException; -import com.google.gwtorm.server.OrmException; +import com.google.gerrit.server.change.Unignore; +import com.google.gerrit.server.change.Unmute; +import com.google.gerrit.server.change.WorkInProgressOp; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; -import java.io.IOException; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -122,6 +132,15 @@ private final Check check; private final Index index; private final Move move; + private final PostPrivate postPrivate; + private final DeletePrivate deletePrivate; + private final Ignore ignore; + private final Unignore unignore; + private final Mute mute; + private final Unmute unmute; + private final SetWorkInProgress setWip; + private final SetReadyForReview setReady; + private final PutMessage putMessage; @Inject ChangeApiImpl( @@ -157,6 +176,15 @@ Check check, Index index, Move move, + PostPrivate postPrivate, + DeletePrivate deletePrivate, + Ignore ignore, + Unignore unignore, + Mute mute, + Unmute unmute, + SetWorkInProgress setWip, + SetReadyForReview setReady, + PutMessage putMessage, @Assisted ChangeResource change) { this.changeApi = changeApi; this.revert = revert; @@ -190,6 +218,15 @@ this.check = check; this.index = index; this.move = move; + this.postPrivate = postPrivate; + this.deletePrivate = deletePrivate; + this.ignore = ignore; + this.unignore = unignore; + this.mute = mute; + this.unmute = unmute; + this.setWip = setWip; + this.setReady = setReady; + this.putMessage = putMessage; this.change = change; } @@ -212,8 +249,8 @@ public RevisionApi revision(String id) throws RestApiException { try { return revisionApi.create(revisions.parse(change, IdString.fromDecoded(id))); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot parse revision", e); + } catch (Exception e) { + throw asRestApiException("Cannot parse revision", e); } } @@ -221,8 +258,8 @@ public ReviewerApi reviewer(String id) throws RestApiException { try { return reviewerApi.create(reviewers.parse(change, IdString.fromDecoded(id))); - } catch (OrmException e) { - throw new RestApiException("Cannot parse reviewer", e); + } catch (Exception e) { + throw asRestApiException("Cannot parse reviewer", e); } } @@ -235,8 +272,8 @@ public void abandon(AbandonInput in) throws RestApiException { try { abandon.apply(change, in); - } catch (OrmException | UpdateException e) { - throw new RestApiException("Cannot abandon change", e); + } catch (Exception e) { + throw asRestApiException("Cannot abandon change", e); } } @@ -249,8 +286,8 @@ public void restore(RestoreInput in) throws RestApiException { try { restore.apply(change, in); - } catch (OrmException | UpdateException e) { - throw new RestApiException("Cannot restore change", e); + } catch (Exception e) { + throw asRestApiException("Cannot restore change", e); } } @@ -265,8 +302,40 @@ public void move(MoveInput in) throws RestApiException { try { move.apply(change, in); - } catch (OrmException | UpdateException e) { - throw new RestApiException("Cannot move change", e); + } catch (Exception e) { + throw asRestApiException("Cannot move change", e); + } + } + + @Override + public void setPrivate(boolean value, @Nullable String message) throws RestApiException { + try { + SetPrivateOp.Input input = new SetPrivateOp.Input(message); + if (value) { + postPrivate.apply(change, input); + } else { + deletePrivate.apply(change, input); + } + } catch (Exception e) { + throw asRestApiException("Cannot change private status", e); + } + } + + @Override + public void setWorkInProgress(String message) throws RestApiException { + try { + setWip.apply(change, new WorkInProgressOp.Input(message)); + } catch (Exception e) { + throw asRestApiException("Cannot set work in progress state", e); + } + } + + @Override + public void setReadyForReview(String message) throws RestApiException { + try { + setReady.apply(change, new WorkInProgressOp.Input(message)); + } catch (Exception e) { + throw asRestApiException("Cannot set ready for review state", e); } } @@ -279,8 +348,8 @@ public ChangeApi revert(RevertInput in) throws RestApiException { try { return changeApi.id(revert.apply(change, in)._number); - } catch (OrmException | IOException | UpdateException e) { - throw new RestApiException("Cannot revert change", e); + } catch (Exception e) { + throw asRestApiException("Cannot revert change", e); } } @@ -288,8 +357,8 @@ public ChangeInfo createMergePatchSet(MergePatchSetInput in) throws RestApiException { try { return updateByMerge.apply(change, in).value(); - } catch (IOException | UpdateException | InvalidChangeOperationException | OrmException e) { - throw new RestApiException("Cannot update change by merge", e); + } catch (Exception e) { + throw asRestApiException("Cannot update change by merge", e); } } @@ -317,8 +386,8 @@ .addListChangesOption(listOptions) .addSubmittedTogetherOption(submitOptions) .applyInfo(change); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot query submittedTogether", e); + } catch (Exception e) { + throw asRestApiException("Cannot query submittedTogether", e); } } @@ -326,8 +395,8 @@ public void publish() throws RestApiException { try { publishDraftChange.apply(change, null); - } catch (UpdateException e) { - throw new RestApiException("Cannot publish change", e); + } catch (Exception e) { + throw asRestApiException("Cannot publish change", e); } } @@ -340,8 +409,8 @@ public void rebase(RebaseInput in) throws RestApiException { try { rebase.apply(change, in); - } catch (EmailException | OrmException | UpdateException | IOException e) { - throw new RestApiException("Cannot rebase change", e); + } catch (Exception e) { + throw asRestApiException("Cannot rebase change", e); } } @@ -349,8 +418,8 @@ public void delete() throws RestApiException { try { deleteChange.apply(change, null); - } catch (UpdateException e) { - throw new RestApiException("Cannot delete change", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete change", e); } } @@ -365,8 +434,8 @@ in.topic = topic; try { putTopic.apply(change, in); - } catch (UpdateException e) { - throw new RestApiException("Cannot set topic", e); + } catch (Exception e) { + throw asRestApiException("Cannot set topic", e); } } @@ -374,24 +443,24 @@ public IncludedInInfo includedIn() throws RestApiException { try { return includedIn.apply(change); - } catch (OrmException | IOException e) { - throw new RestApiException("Could not extract IncludedIn data", e); + } catch (Exception e) { + throw asRestApiException("Could not extract IncludedIn data", e); } } @Override - public void addReviewer(String reviewer) throws RestApiException { + public AddReviewerResult addReviewer(String reviewer) throws RestApiException { AddReviewerInput in = new AddReviewerInput(); in.reviewer = reviewer; - addReviewer(in); + return addReviewer(in); } @Override - public void addReviewer(AddReviewerInput in) throws RestApiException { + public AddReviewerResult addReviewer(AddReviewerInput in) throws RestApiException { try { - postReviewers.apply(change, in); - } catch (OrmException | IOException | UpdateException e) { - throw new RestApiException("Cannot add change reviewer", e); + return postReviewers.apply(change, in); + } catch (Exception e) { + throw asRestApiException("Cannot add change reviewer", e); } } @@ -416,8 +485,8 @@ suggestReviewers.setQuery(r.getQuery()); suggestReviewers.setLimit(r.getLimit()); return suggestReviewers.apply(change); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot retrieve suggested reviewers", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve suggested reviewers", e); } } @@ -425,8 +494,8 @@ public ChangeInfo get(EnumSet<ListChangesOption> s) throws RestApiException { try { return changeJson.create(s).format(change); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve change", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve change", e); } } @@ -446,6 +515,17 @@ } @Override + public void setMessage(String in) throws RestApiException { + try { + PutMessage.Input input = new PutMessage.Input(); + input.message = in; + putMessage.apply(change, input); + } catch (Exception e) { + throw asRestApiException("Cannot edit commit message", e); + } + } + + @Override public ChangeInfo info() throws RestApiException { return get(EnumSet.noneOf(ListChangesOption.class)); } @@ -454,8 +534,8 @@ public void setHashtags(HashtagsInput input) throws RestApiException { try { postHashtags.apply(change, input); - } catch (UpdateException e) { - throw new RestApiException("Cannot post hashtags", e); + } catch (Exception e) { + throw asRestApiException("Cannot post hashtags", e); } } @@ -463,17 +543,17 @@ public Set<String> getHashtags() throws RestApiException { try { return getHashtags.apply(change).value(); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot get hashtags", e); + } catch (Exception e) { + throw asRestApiException("Cannot get hashtags", e); } } @Override public AccountInfo setAssignee(AssigneeInput input) throws RestApiException { try { - return putAssignee.apply(change, input).value(); - } catch (UpdateException | IOException | OrmException e) { - throw new RestApiException("Cannot set assignee", e); + return putAssignee.apply(change, input); + } catch (Exception e) { + throw asRestApiException("Cannot set assignee", e); } } @@ -482,8 +562,8 @@ try { Response<AccountInfo> r = getAssignee.apply(change); return r.isNone() ? null : r.value(); - } catch (OrmException e) { - throw new RestApiException("Cannot get assignee", e); + } catch (Exception e) { + throw asRestApiException("Cannot get assignee", e); } } @@ -491,8 +571,8 @@ public List<AccountInfo> getPastAssignees() throws RestApiException { try { return getPastAssignees.apply(change).value(); - } catch (OrmException e) { - throw new RestApiException("Cannot get past assignees", e); + } catch (Exception e) { + throw asRestApiException("Cannot get past assignees", e); } } @@ -501,8 +581,8 @@ try { Response<AccountInfo> r = deleteAssignee.apply(change, null); return r.isNone() ? null : r.value(); - } catch (UpdateException | OrmException e) { - throw new RestApiException("Cannot delete assignee", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete assignee", e); } } @@ -510,8 +590,8 @@ public Map<String, List<CommentInfo>> comments() throws RestApiException { try { return listComments.apply(change); - } catch (OrmException e) { - throw new RestApiException("Cannot get comments", e); + } catch (Exception e) { + throw asRestApiException("Cannot get comments", e); } } @@ -519,8 +599,8 @@ public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException { try { return listChangeRobotComments.apply(change); - } catch (OrmException e) { - throw new RestApiException("Cannot get robot comments", e); + } catch (Exception e) { + throw asRestApiException("Cannot get robot comments", e); } } @@ -528,8 +608,8 @@ public Map<String, List<CommentInfo>> drafts() throws RestApiException { try { return listDrafts.apply(change); - } catch (OrmException e) { - throw new RestApiException("Cannot get drafts", e); + } catch (Exception e) { + throw asRestApiException("Cannot get drafts", e); } } @@ -537,17 +617,19 @@ public ChangeInfo check() throws RestApiException { try { return check.apply(change).value(); - } catch (OrmException e) { - throw new RestApiException("Cannot check change", e); + } catch (Exception e) { + throw asRestApiException("Cannot check change", e); } } @Override public ChangeInfo check(FixInput fix) throws RestApiException { try { + // TODO(dborowitz): Convert to RetryingRestModifyView. Needs to plumb BatchUpdate.Factory into + // ConsistencyChecker. return check.apply(change, fix).value(); - } catch (OrmException e) { - throw new RestApiException("Cannot check change", e); + } catch (Exception e) { + throw asRestApiException("Cannot check change", e); } } @@ -555,8 +637,30 @@ public void index() throws RestApiException { try { index.apply(change, new Index.Input()); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot index change", e); + } catch (Exception e) { + throw asRestApiException("Cannot index change", e); + } + } + + @Override + public void ignore(boolean ignore) throws RestApiException { + // TODO(dborowitz): Convert to RetryingRestModifyView. Needs to plumb BatchUpdate.Factory into + // StarredChangesUtil. + if (ignore) { + this.ignore.apply(change, new Ignore.Input()); + } else { + unignore.apply(change, new Unignore.Input()); + } + } + + @Override + public void mute(boolean mute) throws RestApiException { + // TODO(dborowitz): Convert to RetryingRestModifyView. Needs to plumb BatchUpdate.Factory into + // StarredChangesUtil. + if (mute) { + this.mute.apply(change, new Mute.Input()); + } else { + unmute.apply(change, new Unmute.Input()); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java index 80d5071..5184e89 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeEditApiImpl.java
@@ -14,6 +14,8 @@ 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.PublishChangeEditInput; import com.google.gerrit.extensions.common.EditInfo; @@ -30,7 +32,6 @@ import com.google.gerrit.server.change.DeleteChangeEdit; import com.google.gerrit.server.change.PublishChangeEdit; import com.google.gerrit.server.change.RebaseChangeEdit; -import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -88,8 +89,8 @@ try { Response<EditInfo> edit = editDetail.apply(changeResource); return edit.isNone() ? Optional.empty() : Optional.of(edit.value()); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot retrieve change edit", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve change edit", e); } } @@ -97,8 +98,8 @@ public void create() throws RestApiException { try { changeEditsPost.apply(changeResource, null); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot create change edit", e); + } catch (Exception e) { + throw asRestApiException("Cannot create change edit", e); } } @@ -106,8 +107,8 @@ public void delete() throws RestApiException { try { deleteChangeEdit.apply(changeResource, new DeleteChangeEdit.Input()); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot delete change edit", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete change edit", e); } } @@ -115,8 +116,8 @@ public void rebase() throws RestApiException { try { rebaseChangeEdit.apply(changeResource, null); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot rebase change edit", e); + } catch (Exception e) { + throw asRestApiException("Cannot rebase change edit", e); } } @@ -129,8 +130,8 @@ public void publish(PublishChangeEditInput publishChangeEditInput) throws RestApiException { try { publishChangeEdit.apply(changeResource, publishChangeEditInput); - } catch (IOException | OrmException | UpdateException e) { - throw new RestApiException("Cannot publish change edit", e); + } catch (Exception e) { + throw asRestApiException("Cannot publish change edit", e); } } @@ -140,8 +141,8 @@ ChangeEditResource changeEditResource = getChangeEditResource(filePath); Response<BinaryResult> fileResponse = changeEditsGet.apply(changeEditResource); return fileResponse.isNone() ? Optional.empty() : Optional.of(fileResponse.value()); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot retrieve file of change edit", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve file of change edit", e); } } @@ -152,8 +153,8 @@ renameInput.oldPath = oldFilePath; renameInput.newPath = newFilePath; changeEditsPost.apply(changeResource, renameInput); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot rename file of change edit", e); + } catch (Exception e) { + throw asRestApiException("Cannot rename file of change edit", e); } } @@ -163,8 +164,8 @@ ChangeEdits.Post.Input restoreInput = new ChangeEdits.Post.Input(); restoreInput.restorePath = filePath; changeEditsPost.apply(changeResource, restoreInput); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot restore file of change edit", e); + } catch (Exception e) { + throw asRestApiException("Cannot restore file of change edit", e); } } @@ -172,8 +173,8 @@ public void modifyFile(String filePath, RawInput newContent) throws RestApiException { try { changeEditsPut.apply(changeResource.getControl(), filePath, newContent); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot modify file of change edit", e); + } catch (Exception e) { + throw asRestApiException("Cannot modify file of change edit", e); } } @@ -181,8 +182,8 @@ public void deleteFile(String filePath) throws RestApiException { try { changeEditDeleteContent.apply(changeResource.getControl(), filePath); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot delete file of change edit", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete file of change edit", e); } } @@ -192,8 +193,8 @@ try (BinaryResult binaryResult = getChangeEditCommitMessage.apply(changeResource)) { return binaryResult.asString(); } - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot get commit message of change edit", e); + } catch (Exception e) { + throw asRestApiException("Cannot get commit message of change edit", e); } } @@ -203,8 +204,8 @@ input.message = newCommitMessage; try { modifyChangeEditCommitMessage.apply(changeResource, input); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot modify commit message of change edit", e); + } catch (Exception e) { + throw asRestApiException("Cannot modify commit message of change edit", e); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java index c77f86f..cc39883 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; @@ -24,7 +25,6 @@ import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeInput; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.TopLevelResource; @@ -32,14 +32,10 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.change.ChangesCollection; import com.google.gerrit.server.change.CreateChange; -import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gerrit.server.query.change.QueryChanges; -import com.google.gerrit.server.update.UpdateException; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import java.io.IOException; import java.util.List; @Singleton @@ -77,18 +73,24 @@ public ChangeApi id(String id) throws RestApiException { try { return api.create(changes.parse(TopLevelResource.INSTANCE, IdString.fromUrl(id))); - } catch (OrmException e) { - throw new RestApiException("Cannot parse change", e); + } catch (Exception e) { + throw asRestApiException("Cannot parse change", e); } } @Override + public ChangeApi id(String project, int id) throws RestApiException { + return id( + Joiner.on('~').join(ImmutableList.of(Url.encode(project), Url.encode(String.valueOf(id))))); + } + + @Override public ChangeApi create(ChangeInput in) throws RestApiException { try { ChangeInfo out = createChange.apply(TopLevelResource.INSTANCE, in).value(); return api.create(changes.parse(new Change.Id(out._number))); - } catch (OrmException | IOException | InvalidChangeOperationException | UpdateException e) { - throw new RestApiException("Cannot create change", e); + } catch (Exception e) { + throw asRestApiException("Cannot create change", e); } } @@ -107,7 +109,7 @@ return query().withQuery(query); } - private List<ChangeInfo> get(final QueryRequest q) throws RestApiException { + private List<ChangeInfo> get(QueryRequest q) throws RestApiException { QueryChanges qc = queryProvider.get(); if (q.getQuery() != null) { qc.addQuery(q.getQuery()); @@ -132,8 +134,8 @@ List<ChangeInfo> infos = (List<ChangeInfo>) result; return ImmutableList.copyOf(infos); - } catch (AuthException | OrmException e) { - throw new RestApiException("Cannot query changes", e); + } catch (Exception e) { + throw asRestApiException("Cannot query changes", e); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java index 5c61e23..6a2501e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java
@@ -14,12 +14,15 @@ package com.google.gerrit.server.api.changes; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + import com.google.gerrit.extensions.api.changes.CommentApi; +import com.google.gerrit.extensions.api.changes.DeleteCommentInput; import com.google.gerrit.extensions.common.CommentInfo; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.server.change.CommentResource; +import com.google.gerrit.server.change.DeleteComment; import com.google.gerrit.server.change.GetComment; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -29,11 +32,14 @@ } private final GetComment getComment; + private final DeleteComment deleteComment; private final CommentResource comment; @Inject - CommentApiImpl(GetComment getComment, @Assisted CommentResource comment) { + CommentApiImpl( + GetComment getComment, DeleteComment deleteComment, @Assisted CommentResource comment) { this.getComment = getComment; + this.deleteComment = deleteComment; this.comment = comment; } @@ -41,8 +47,17 @@ public CommentInfo get() throws RestApiException { try { return getComment.apply(comment); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve comment", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve comment", e); + } + } + + @Override + public CommentInfo delete(DeleteCommentInput input) throws RestApiException { + try { + return deleteComment.apply(comment, input); + } catch (Exception e) { + throw asRestApiException("Cannot delete comment", e); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java index 1bd9216..eada51b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
@@ -14,16 +14,18 @@ package com.google.gerrit.server.api.changes; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + +import com.google.gerrit.extensions.api.changes.DeleteCommentInput; import com.google.gerrit.extensions.api.changes.DraftApi; import com.google.gerrit.extensions.api.changes.DraftInput; import com.google.gerrit.extensions.common.CommentInfo; +import com.google.gerrit.extensions.restapi.NotImplementedException; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.server.change.DeleteDraftComment; import com.google.gerrit.server.change.DraftCommentResource; import com.google.gerrit.server.change.GetDraftComment; import com.google.gerrit.server.change.PutDraftComment; -import com.google.gerrit.server.update.UpdateException; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -53,8 +55,8 @@ public CommentInfo get() throws RestApiException { try { return getDraft.apply(draft); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve draft", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve draft", e); } } @@ -62,8 +64,8 @@ public CommentInfo update(DraftInput in) throws RestApiException { try { return putDraft.apply(draft, in).value(); - } catch (UpdateException | OrmException e) { - throw new RestApiException("Cannot update draft", e); + } catch (Exception e) { + throw asRestApiException("Cannot update draft", e); } } @@ -71,8 +73,13 @@ public void delete() throws RestApiException { try { deleteDraft.apply(draft, null); - } catch (UpdateException e) { - throw new RestApiException("Cannot delete draft", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete draft", e); } } + + @Override + public CommentInfo delete(DeleteCommentInput input) { + throw new NotImplementedException(); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java index aa66e7b..f0a934f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/FileApiImpl.java
@@ -14,6 +14,8 @@ package com.google.gerrit.server.api.changes; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + import com.google.gerrit.extensions.api.changes.FileApi; import com.google.gerrit.extensions.common.DiffInfo; import com.google.gerrit.extensions.restapi.BinaryResult; @@ -21,11 +23,8 @@ import com.google.gerrit.server.change.FileResource; import com.google.gerrit.server.change.GetContent; import com.google.gerrit.server.change.GetDiff; -import com.google.gerrit.server.project.InvalidChangeOperationException; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import java.io.IOException; class FileApiImpl implements FileApi { interface Factory { @@ -47,8 +46,8 @@ public BinaryResult content() throws RestApiException { try { return getContent.apply(file); - } catch (IOException | OrmException e) { - throw new RestApiException("Cannot retrieve file content", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve file content", e); } } @@ -56,8 +55,8 @@ public DiffInfo diff() throws RestApiException { try { return getDiff.apply(file).value(); - } catch (IOException | InvalidChangeOperationException | OrmException e) { - throw new RestApiException("Cannot retrieve diff", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve diff", e); } } @@ -65,8 +64,8 @@ public DiffInfo diff(String base) throws RestApiException { try { return getDiff.setBase(base).apply(file).value(); - } catch (IOException | InvalidChangeOperationException | OrmException e) { - throw new RestApiException("Cannot retrieve diff", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve diff", e); } } @@ -74,8 +73,8 @@ public DiffInfo diff(int parent) throws RestApiException { try { return getDiff.setParent(parent).apply(file).value(); - } catch (OrmException | InvalidChangeOperationException | IOException e) { - throw new RestApiException("Cannot retrieve diff", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve diff", e); } } @@ -104,8 +103,8 @@ } try { return getDiff.apply(file).value(); - } catch (IOException | InvalidChangeOperationException | OrmException e) { - throw new RestApiException("Cannot retrieve diff", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve diff", e); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java index 8ac874a..2f8b7d8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ReviewerApiImpl.java
@@ -14,6 +14,8 @@ package com.google.gerrit.server.api.changes; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; import com.google.gerrit.extensions.api.changes.DeleteVoteInput; import com.google.gerrit.extensions.api.changes.ReviewerApi; @@ -23,8 +25,6 @@ import com.google.gerrit.server.change.ReviewerResource; import com.google.gerrit.server.change.VoteResource; import com.google.gerrit.server.change.Votes; -import com.google.gerrit.server.update.UpdateException; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import java.util.Map; @@ -55,8 +55,8 @@ public Map<String, Short> votes() throws RestApiException { try { return listVotes.apply(reviewer); - } catch (OrmException e) { - throw new RestApiException("Cannot list votes", e); + } catch (Exception e) { + throw asRestApiException("Cannot list votes", e); } } @@ -64,8 +64,8 @@ public void deleteVote(String label) throws RestApiException { try { deleteVote.apply(new VoteResource(reviewer, label), null); - } catch (UpdateException e) { - throw new RestApiException("Cannot delete vote", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete vote", e); } } @@ -73,8 +73,8 @@ public void deleteVote(DeleteVoteInput input) throws RestApiException { try { deleteVote.apply(new VoteResource(reviewer, input.label), input); - } catch (UpdateException e) { - throw new RestApiException("Cannot delete vote", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete vote", e); } } @@ -87,8 +87,8 @@ public void remove(DeleteReviewerInput input) throws RestApiException { try { deleteReviewer.apply(reviewer, input); - } catch (UpdateException e) { - throw new RestApiException("Cannot remove reviewer", e); + } catch (Exception e) { + throw asRestApiException("Cannot remove reviewer", e); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java index 43be8df..1f9fcd1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -14,8 +14,9 @@ package com.google.gerrit.server.api.changes; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + import com.google.common.collect.ImmutableSet; -import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.extensions.api.changes.ChangeApi; import com.google.gerrit.extensions.api.changes.Changes; import com.google.gerrit.extensions.api.changes.CherryPickInput; @@ -25,6 +26,7 @@ import com.google.gerrit.extensions.api.changes.FileApi; import com.google.gerrit.extensions.api.changes.RebaseInput; import com.google.gerrit.extensions.api.changes.ReviewInput; +import com.google.gerrit.extensions.api.changes.ReviewResult; import com.google.gerrit.extensions.api.changes.RevisionApi; import com.google.gerrit.extensions.api.changes.RevisionReviewerApi; import com.google.gerrit.extensions.api.changes.RobotCommentApi; @@ -33,6 +35,7 @@ import com.google.gerrit.extensions.common.ActionInfo; import com.google.gerrit.extensions.common.CommentInfo; import com.google.gerrit.extensions.common.CommitInfo; +import com.google.gerrit.extensions.common.EditInfo; import com.google.gerrit.extensions.common.FileInfo; import com.google.gerrit.extensions.common.MergeableInfo; import com.google.gerrit.extensions.common.RobotCommentInfo; @@ -41,6 +44,7 @@ import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.server.change.ApplyFix; import com.google.gerrit.server.change.CherryPick; import com.google.gerrit.server.change.Comments; import com.google.gerrit.server.change.CreateDraftComment; @@ -48,6 +52,8 @@ import com.google.gerrit.server.change.DraftComments; import com.google.gerrit.server.change.FileResource; import com.google.gerrit.server.change.Files; +import com.google.gerrit.server.change.Fixes; +import com.google.gerrit.server.change.GetCommit; import com.google.gerrit.server.change.GetDescription; import com.google.gerrit.server.change.GetMergeList; import com.google.gerrit.server.change.GetPatch; @@ -69,13 +75,9 @@ import com.google.gerrit.server.change.Submit; import com.google.gerrit.server.change.TestSubmitType; import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.patch.PatchListNotAvailableException; -import com.google.gerrit.server.update.UpdateException; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; -import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Set; @@ -103,12 +105,15 @@ private final RevisionResource revision; private final Files files; private final Files.ListFiles listFiles; + private final GetCommit getCommit; private final GetPatch getPatch; private final PostReview review; private final Mergeable mergeable; private final FileApiImpl.Factory fileApi; private final ListRevisionComments listComments; private final ListRobotComments listRobotComments; + private final ApplyFix applyFix; + private final Fixes fixes; private final ListRevisionDrafts listDrafts; private final CreateDraftComment createDraft; private final DraftComments drafts; @@ -141,12 +146,15 @@ Reviewed.DeleteReviewed deleteReviewed, Files files, Files.ListFiles listFiles, + GetCommit getCommit, GetPatch getPatch, PostReview review, Mergeable mergeable, FileApiImpl.Factory fileApi, ListRevisionComments listComments, ListRobotComments listRobotComments, + ApplyFix applyFix, + Fixes fixes, ListRevisionDrafts listDrafts, CreateDraftComment createDraft, DraftComments drafts, @@ -178,12 +186,15 @@ this.putReviewed = putReviewed; this.deleteReviewed = deleteReviewed; this.listFiles = listFiles; + this.getCommit = getCommit; this.getPatch = getPatch; this.mergeable = mergeable; this.fileApi = fileApi; this.listComments = listComments; this.robotComments = robotComments; this.listRobotComments = listRobotComments; + this.applyFix = applyFix; + this.fixes = fixes; this.listDrafts = listDrafts; this.createDraft = createDraft; this.drafts = drafts; @@ -201,11 +212,11 @@ } @Override - public void review(ReviewInput in) throws RestApiException { + public ReviewResult review(ReviewInput in) throws RestApiException { try { - review.apply(revision, in); - } catch (OrmException | UpdateException | IOException e) { - throw new RestApiException("Cannot post review", e); + return review.apply(revision, in).value(); + } catch (Exception e) { + throw asRestApiException("Cannot post review", e); } } @@ -219,8 +230,8 @@ public void submit(SubmitInput in) throws RestApiException { try { submit.apply(revision, in); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot submit change", e); + } catch (Exception e) { + throw asRestApiException("Cannot submit change", e); } } @@ -234,8 +245,8 @@ try { submitPreview.setFormat(format); return submitPreview.apply(revision); - } catch (OrmException e) { - throw new RestApiException("Cannot get submit preview", e); + } catch (Exception e) { + throw asRestApiException("Cannot get submit preview", e); } } @@ -243,8 +254,8 @@ public void publish() throws RestApiException { try { publish.apply(revision, new PublishDraftPatchSet.Input()); - } catch (UpdateException e) { - throw new RestApiException("Cannot publish draft patch set", e); + } catch (Exception e) { + throw asRestApiException("Cannot publish draft patch set", e); } } @@ -252,8 +263,8 @@ public void delete() throws RestApiException { try { deleteDraft.apply(revision, null); - } catch (UpdateException e) { - throw new RestApiException("Cannot delete draft ps", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete draft ps", e); } } @@ -267,8 +278,8 @@ public ChangeApi rebase(RebaseInput in) throws RestApiException { try { return changes.id(rebase.apply(revision, in)._number); - } catch (OrmException | EmailException | UpdateException | IOException e) { - throw new RestApiException("Cannot rebase ps", e); + } catch (Exception e) { + throw asRestApiException("Cannot rebase ps", e); } } @@ -277,8 +288,8 @@ try (Repository repo = repoManager.openRepository(revision.getProject()); RevWalk rw = new RevWalk(repo)) { return rebaseUtil.canRebase(revision.getPatchSet(), revision.getChange().getDest(), repo, rw); - } catch (IOException e) { - throw new RestApiException("Cannot check if rebase is possible", e); + } catch (Exception e) { + throw asRestApiException("Cannot check if rebase is possible", e); } } @@ -286,8 +297,8 @@ public ChangeApi cherryPick(CherryPickInput in) throws RestApiException { try { return changes.id(cherryPick.apply(revision, in)._number); - } catch (OrmException | IOException | UpdateException e) { - throw new RestApiException("Cannot cherry pick", e); + } catch (Exception e) { + throw asRestApiException("Cannot cherry pick", e); } } @@ -296,8 +307,8 @@ try { return revisionReviewerApi.create( revisionReviewers.parse(revision, IdString.fromDecoded(id))); - } catch (OrmException e) { - throw new RestApiException("Cannot parse reviewer", e); + } catch (Exception e) { + throw asRestApiException("Cannot parse reviewer", e); } } @@ -312,7 +323,7 @@ } view.apply(files.parse(revision, IdString.fromDecoded(path)), new Reviewed.Input()); } catch (Exception e) { - throw new RestApiException("Cannot update reviewed flag", e); + throw asRestApiException("Cannot update reviewed flag", e); } } @@ -322,8 +333,8 @@ try { return ImmutableSet.copyOf( (Iterable<String>) listFiles.setReviewed(true).apply(revision).value()); - } catch (OrmException | IOException | PatchListNotAvailableException e) { - throw new RestApiException("Cannot list reviewed files", e); + } catch (Exception e) { + throw asRestApiException("Cannot list reviewed files", e); } } @@ -331,8 +342,8 @@ public MergeableInfo mergeable() throws RestApiException { try { return mergeable.apply(revision); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot check mergeability", e); + } catch (Exception e) { + throw asRestApiException("Cannot check mergeability", e); } } @@ -341,8 +352,8 @@ try { mergeable.setOtherBranches(true); return mergeable.apply(revision); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot check mergeability", e); + } catch (Exception e) { + throw asRestApiException("Cannot check mergeability", e); } } @@ -351,8 +362,8 @@ public Map<String, FileInfo> files() throws RestApiException { try { return (Map<String, FileInfo>) listFiles.apply(revision).value(); - } catch (OrmException | IOException | PatchListNotAvailableException e) { - throw new RestApiException("Cannot retrieve files", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve files", e); } } @@ -361,8 +372,8 @@ public Map<String, FileInfo> files(String base) throws RestApiException { try { return (Map<String, FileInfo>) listFiles.setBase(base).apply(revision).value(); - } catch (OrmException | IOException | PatchListNotAvailableException e) { - throw new RestApiException("Cannot retrieve files", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve files", e); } } @@ -371,8 +382,8 @@ public Map<String, FileInfo> files(int parentNum) throws RestApiException { try { return (Map<String, FileInfo>) listFiles.setParent(parentNum).apply(revision).value(); - } catch (OrmException | IOException | PatchListNotAvailableException e) { - throw new RestApiException("Cannot retrieve files", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve files", e); } } @@ -382,11 +393,20 @@ } @Override + public CommitInfo commit(boolean addLinks) throws RestApiException { + try { + return getCommit.setAddLinks(addLinks).apply(revision).value(); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve commit", e); + } + } + + @Override public Map<String, List<CommentInfo>> comments() throws RestApiException { try { return listComments.apply(revision); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve comments", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve comments", e); } } @@ -394,8 +414,8 @@ public Map<String, List<RobotCommentInfo>> robotComments() throws RestApiException { try { return listRobotComments.apply(revision); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve robot comments", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve robot comments", e); } } @@ -403,8 +423,8 @@ public List<CommentInfo> commentsAsList() throws RestApiException { try { return listComments.getComments(revision); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve comments", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve comments", e); } } @@ -412,8 +432,8 @@ public Map<String, List<CommentInfo>> drafts() throws RestApiException { try { return listDrafts.apply(revision); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve drafts", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve drafts", e); } } @@ -421,8 +441,17 @@ public List<RobotCommentInfo> robotCommentsAsList() throws RestApiException { try { return listRobotComments.getComments(revision); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve robot comments", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve robot comments", e); + } + } + + @Override + public EditInfo applyFix(String fixId) throws RestApiException { + try { + return applyFix.apply(fixes.parse(revision, IdString.fromDecoded(fixId)), null).value(); + } catch (Exception e) { + throw asRestApiException("Cannot apply fix", e); } } @@ -430,8 +459,8 @@ public List<CommentInfo> draftsAsList() throws RestApiException { try { return listDrafts.getComments(revision); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve drafts", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve drafts", e); } } @@ -439,8 +468,8 @@ public DraftApi draft(String id) throws RestApiException { try { return draftFactory.create(drafts.parse(revision, IdString.fromDecoded(id))); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve draft", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve draft", e); } } @@ -453,8 +482,8 @@ .id(revision.getChange().getId().get()) .revision(revision.getPatchSet().getId().get()) .draft(id); - } catch (UpdateException | OrmException e) { - throw new RestApiException("Cannot create draft", e); + } catch (Exception e) { + throw asRestApiException("Cannot create draft", e); } } @@ -462,8 +491,8 @@ public CommentApi comment(String id) throws RestApiException { try { return commentFactory.create(comments.parse(revision, IdString.fromDecoded(id))); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve comment", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve comment", e); } } @@ -471,8 +500,8 @@ public RobotCommentApi robotComment(String id) throws RestApiException { try { return robotCommentFactory.create(robotComments.parse(revision, IdString.fromDecoded(id))); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve robot comment", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve robot comment", e); } } @@ -480,8 +509,8 @@ public BinaryResult patch() throws RestApiException { try { return getPatch.apply(revision); - } catch (IOException e) { - throw new RestApiException("Cannot get patch", e); + } catch (Exception e) { + throw asRestApiException("Cannot get patch", e); } } @@ -489,8 +518,8 @@ public BinaryResult patch(String path) throws RestApiException { try { return getPatch.setPath(path).apply(revision); - } catch (IOException e) { - throw new RestApiException("Cannot get patch", e); + } catch (Exception e) { + throw asRestApiException("Cannot get patch", e); } } @@ -498,8 +527,8 @@ public Map<String, ActionInfo> actions() throws RestApiException { try { return revisionActions.apply(revision).value(); - } catch (OrmException e) { - throw new RestApiException("Cannot get actions", e); + } catch (Exception e) { + throw asRestApiException("Cannot get actions", e); } } @@ -507,8 +536,8 @@ public SubmitType submitType() throws RestApiException { try { return getSubmitType.apply(revision); - } catch (OrmException e) { - throw new RestApiException("Cannot get submit type", e); + } catch (Exception e) { + throw asRestApiException("Cannot get submit type", e); } } @@ -516,8 +545,8 @@ public SubmitType testSubmitType(TestSubmitRuleInput in) throws RestApiException { try { return testSubmitType.apply(revision, in); - } catch (OrmException e) { - throw new RestApiException("Cannot test submit type", e); + } catch (Exception e) { + throw asRestApiException("Cannot test submit type", e); } } @@ -531,8 +560,8 @@ gml.setUninterestingParent(getUninterestingParent()); gml.setAddLinks(getAddLinks()); return gml.apply(revision).value(); - } catch (IOException e) { - throw new RestApiException("Cannot get merge list", e); + } catch (Exception e) { + throw asRestApiException("Cannot get merge list", e); } } }; @@ -544,8 +573,8 @@ in.description = description; try { putDescription.apply(revision, in); - } catch (UpdateException e) { - throw new RestApiException("Cannot set description", e); + } catch (Exception e) { + throw asRestApiException("Cannot set description", e); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java index 5c56321..60dc1d2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionReviewerApiImpl.java
@@ -14,6 +14,8 @@ package com.google.gerrit.server.api.changes; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + import com.google.gerrit.extensions.api.changes.DeleteVoteInput; import com.google.gerrit.extensions.api.changes.RevisionReviewerApi; import com.google.gerrit.extensions.restapi.RestApiException; @@ -21,8 +23,6 @@ import com.google.gerrit.server.change.ReviewerResource; import com.google.gerrit.server.change.VoteResource; import com.google.gerrit.server.change.Votes; -import com.google.gerrit.server.update.UpdateException; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import java.util.Map; @@ -48,8 +48,8 @@ public Map<String, Short> votes() throws RestApiException { try { return listVotes.apply(reviewer); - } catch (OrmException e) { - throw new RestApiException("Cannot list votes", e); + } catch (Exception e) { + throw asRestApiException("Cannot list votes", e); } } @@ -57,8 +57,8 @@ public void deleteVote(String label) throws RestApiException { try { deleteVote.apply(new VoteResource(reviewer, label), null); - } catch (UpdateException e) { - throw new RestApiException("Cannot delete vote", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete vote", e); } } @@ -66,8 +66,8 @@ public void deleteVote(DeleteVoteInput input) throws RestApiException { try { deleteVote.apply(new VoteResource(reviewer, input.label), input); - } catch (UpdateException e) { - throw new RestApiException("Cannot delete vote", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete vote", e); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java index ded98cb..b19939b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RobotCommentApiImpl.java
@@ -14,12 +14,13 @@ package com.google.gerrit.server.api.changes; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + import com.google.gerrit.extensions.api.changes.RobotCommentApi; import com.google.gerrit.extensions.common.RobotCommentInfo; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.server.change.GetRobotComment; import com.google.gerrit.server.change.RobotCommentResource; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -41,8 +42,8 @@ public RobotCommentInfo get() throws RestApiException { try { return getComment.apply(comment); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve robot comment", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve robot comment", e); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java index 9b6ead0..87118c7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/config/ServerImpl.java
@@ -14,12 +14,20 @@ package com.google.gerrit.server.api.config; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + import com.google.gerrit.common.Version; +import com.google.gerrit.extensions.api.config.AccessCheckInfo; +import com.google.gerrit.extensions.api.config.AccessCheckInput; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInput; import com.google.gerrit.extensions.api.config.Server; import com.google.gerrit.extensions.client.DiffPreferencesInfo; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.common.ServerInfo; import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.server.config.CheckAccess; +import com.google.gerrit.server.config.CheckConsistency; import com.google.gerrit.server.config.ConfigResource; import com.google.gerrit.server.config.GetDiffPreferences; import com.google.gerrit.server.config.GetPreferences; @@ -27,9 +35,8 @@ import com.google.gerrit.server.config.SetDiffPreferences; import com.google.gerrit.server.config.SetPreferences; 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; @Singleton public class ServerImpl implements Server { @@ -38,6 +45,8 @@ private final GetDiffPreferences getDiffPreferences; private final SetDiffPreferences setDiffPreferences; private final GetServerInfo getServerInfo; + private final Provider<CheckConsistency> checkConsistency; + private final Provider<CheckAccess> checkAccess; @Inject ServerImpl( @@ -45,12 +54,16 @@ SetPreferences setPreferences, GetDiffPreferences getDiffPreferences, SetDiffPreferences setDiffPreferences, - GetServerInfo getServerInfo) { + GetServerInfo getServerInfo, + Provider<CheckConsistency> checkConsistency, + Provider<CheckAccess> checkAccess) { this.getPreferences = getPreferences; this.setPreferences = setPreferences; this.getDiffPreferences = getDiffPreferences; this.setDiffPreferences = setDiffPreferences; this.getServerInfo = getServerInfo; + this.checkConsistency = checkConsistency; + this.checkAccess = checkAccess; } @Override @@ -62,8 +75,8 @@ public ServerInfo getInfo() throws RestApiException { try { return getServerInfo.apply(new ConfigResource()); - } catch (IOException e) { - throw new RestApiException("Cannot get server info", e); + } catch (Exception e) { + throw asRestApiException("Cannot get server info", e); } } @@ -71,8 +84,8 @@ public GeneralPreferencesInfo getDefaultPreferences() throws RestApiException { try { return getPreferences.apply(new ConfigResource()); - } catch (IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot get default general preferences", e); + } catch (Exception e) { + throw asRestApiException("Cannot get default general preferences", e); } } @@ -81,8 +94,8 @@ throws RestApiException { try { return setPreferences.apply(new ConfigResource(), in); - } catch (IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot set default general preferences", e); + } catch (Exception e) { + throw asRestApiException("Cannot set default general preferences", e); } } @@ -90,8 +103,8 @@ public DiffPreferencesInfo getDefaultDiffPreferences() throws RestApiException { try { return getDiffPreferences.apply(new ConfigResource()); - } catch (IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot get default diff preferences", e); + } catch (Exception e) { + throw asRestApiException("Cannot get default diff preferences", e); } } @@ -100,8 +113,26 @@ throws RestApiException { try { return setDiffPreferences.apply(new ConfigResource(), in); - } catch (IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot set default diff preferences", e); + } catch (Exception e) { + throw asRestApiException("Cannot set default diff preferences", e); + } + } + + @Override + public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException { + try { + return checkConsistency.get().apply(new ConfigResource(), in); + } catch (Exception e) { + throw asRestApiException("Cannot check consistency", e); + } + } + + @Override + public AccessCheckInfo checkAccess(AccessCheckInput in) throws RestApiException { + try { + return checkAccess.get().apply(new ConfigResource(), in); + } catch (Exception e) { + throw asRestApiException("Cannot check access", e); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java index 15120d2..d7f868c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -14,6 +14,8 @@ package com.google.gerrit.server.api.groups; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + import com.google.gerrit.common.errors.NoSuchGroupException; import com.google.gerrit.extensions.api.groups.GroupApi; import com.google.gerrit.extensions.common.AccountInfo; @@ -41,10 +43,8 @@ import com.google.gerrit.server.group.PutName; import com.google.gerrit.server.group.PutOptions; import com.google.gerrit.server.group.PutOwner; -import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; -import java.io.IOException; import java.util.Arrays; import java.util.List; @@ -73,7 +73,7 @@ private final GroupResource rsrc; private final Index index; - @AssistedInject + @Inject GroupApiImpl( GetGroup getGroup, GetDetail getDetail, @@ -119,8 +119,8 @@ public GroupInfo get() throws RestApiException { try { return getGroup.apply(rsrc); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve group", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve group", e); } } @@ -128,8 +128,8 @@ public GroupInfo detail() throws RestApiException { try { return getDetail.apply(rsrc); - } catch (OrmException e) { - throw new RestApiException("Cannot retrieve group", e); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve group", e); } } @@ -146,8 +146,8 @@ putName.apply(rsrc, in); } catch (NoSuchGroupException e) { throw new ResourceNotFoundException(name, e); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot put group name", e); + } catch (Exception e) { + throw asRestApiException("Cannot put group name", e); } } @@ -155,8 +155,8 @@ public GroupInfo owner() throws RestApiException { try { return getOwner.apply(rsrc); - } catch (OrmException e) { - throw new RestApiException("Cannot get group owner", e); + } catch (Exception e) { + throw asRestApiException("Cannot get group owner", e); } } @@ -166,8 +166,8 @@ in.owner = owner; try { putOwner.apply(rsrc, in); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot put group owner", e); + } catch (Exception e) { + throw asRestApiException("Cannot put group owner", e); } } @@ -182,8 +182,8 @@ in.description = description; try { putDescription.apply(rsrc, in); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot put group description", e); + } catch (Exception e) { + throw asRestApiException("Cannot put group description", e); } } @@ -196,8 +196,8 @@ public void options(GroupOptionsInfo options) throws RestApiException { try { putOptions.apply(rsrc, options); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot put group options", e); + } catch (Exception e) { + throw asRestApiException("Cannot put group options", e); } } @@ -211,8 +211,8 @@ listMembers.setRecursive(recursive); try { return listMembers.apply(rsrc); - } catch (OrmException e) { - throw new RestApiException("Cannot list group members", e); + } catch (Exception e) { + throw asRestApiException("Cannot list group members", e); } } @@ -220,8 +220,8 @@ public void addMembers(String... members) throws RestApiException { try { addMembers.apply(rsrc, AddMembers.Input.fromMembers(Arrays.asList(members))); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot add group members", e); + } catch (Exception e) { + throw asRestApiException("Cannot add group members", e); } } @@ -229,8 +229,8 @@ public void removeMembers(String... members) throws RestApiException { try { deleteMembers.apply(rsrc, AddMembers.Input.fromMembers(Arrays.asList(members))); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot remove group members", e); + } catch (Exception e) { + throw asRestApiException("Cannot remove group members", e); } } @@ -238,8 +238,8 @@ public List<GroupInfo> includedGroups() throws RestApiException { try { return listGroups.apply(rsrc); - } catch (OrmException e) { - throw new RestApiException("Cannot list included groups", e); + } catch (Exception e) { + throw asRestApiException("Cannot list included groups", e); } } @@ -247,8 +247,8 @@ public void addGroups(String... groups) throws RestApiException { try { addGroups.apply(rsrc, AddIncludedGroups.Input.fromGroups(Arrays.asList(groups))); - } catch (OrmException e) { - throw new RestApiException("Cannot add group members", e); + } catch (Exception e) { + throw asRestApiException("Cannot add group members", e); } } @@ -256,8 +256,8 @@ public void removeGroups(String... groups) throws RestApiException { try { deleteGroups.apply(rsrc, AddIncludedGroups.Input.fromGroups(Arrays.asList(groups))); - } catch (OrmException e) { - throw new RestApiException("Cannot remove group members", e); + } catch (Exception e) { + throw asRestApiException("Cannot remove group members", e); } } @@ -265,8 +265,8 @@ public List<? extends GroupAuditEventInfo> auditLog() throws RestApiException { try { return getAuditLog.apply(rsrc); - } catch (OrmException e) { - throw new RestApiException("Cannot get audit log", e); + } catch (Exception e) { + throw asRestApiException("Cannot get audit log", e); } } @@ -274,8 +274,8 @@ public void index() throws RestApiException { try { index.apply(rsrc, new Index.Input()); - } catch (IOException e) { - throw new RestApiException("Cannot index group", e); + } catch (Exception e) { + throw asRestApiException("Cannot index group", e); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java index 1d725a8..5f816bb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupsImpl.java
@@ -15,7 +15,7 @@ package com.google.gerrit.server.api.groups; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; import com.google.gerrit.extensions.api.groups.GroupApi; import com.google.gerrit.extensions.api.groups.GroupInput; @@ -32,12 +32,12 @@ import com.google.gerrit.server.group.GroupsCollection; import com.google.gerrit.server.group.ListGroups; import com.google.gerrit.server.group.QueryGroups; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.project.ProjectsCollection; -import com.google.gwtorm.server.OrmException; 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.SortedMap; @@ -49,6 +49,7 @@ private final Provider<ListGroups> listGroups; private final Provider<QueryGroups> queryGroups; private final Provider<CurrentUser> user; + private final PermissionBackend permissionBackend; private final CreateGroup.Factory createGroup; private final GroupApiImpl.Factory api; @@ -60,6 +61,7 @@ Provider<ListGroups> listGroups, Provider<QueryGroups> queryGroups, Provider<CurrentUser> user, + PermissionBackend permissionBackend, CreateGroup.Factory createGroup, GroupApiImpl.Factory api) { this.accounts = accounts; @@ -68,6 +70,7 @@ this.listGroups = listGroups; this.queryGroups = queryGroups; this.user = user; + this.permissionBackend = permissionBackend; this.createGroup = createGroup; this.api = api; } @@ -89,12 +92,13 @@ if (checkNotNull(in, "GroupInput").name == null) { throw new BadRequestException("GroupInput must specify name"); } - checkRequiresCapability(user, null, CreateGroup.class); try { - GroupInfo info = createGroup.create(in.name).apply(TopLevelResource.INSTANCE, in); + CreateGroup impl = createGroup.create(in.name); + permissionBackend.user(user).checkAny(GlobalPermission.fromAnnotation(impl.getClass())); + GroupInfo info = impl.apply(TopLevelResource.INSTANCE, in); return id(info.id); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot create group " + in.name, e); + } catch (Exception e) { + throw asRestApiException("Cannot create group " + in.name, e); } } @@ -116,8 +120,8 @@ for (String project : req.getProjects()) { try { list.addProject(projects.parse(tlr, IdString.fromDecoded(project)).getControl()); - } catch (IOException e) { - throw new RestApiException("Error looking up project " + project, e); + } catch (Exception e) { + throw asRestApiException("Error looking up project " + project, e); } } @@ -130,8 +134,8 @@ if (req.getUser() != null) { try { list.setUser(accounts.parse(req.getUser()).getAccountId()); - } catch (OrmException e) { - throw new RestApiException("Error looking up user " + req.getUser(), e); + } catch (Exception e) { + throw asRestApiException("Error looking up user " + req.getUser(), e); } } @@ -142,8 +146,8 @@ list.setSuggest(req.getSuggest()); try { return list.apply(tlr); - } catch (OrmException e) { - throw new RestApiException("Cannot list groups", e); + } catch (Exception e) { + throw asRestApiException("Cannot list groups", e); } } @@ -172,8 +176,8 @@ myQueryGroups.addOption(option); } return myQueryGroups.apply(TopLevelResource.INSTANCE); - } catch (OrmException e) { - throw new RestApiException("Cannot query groups", e); + } catch (Exception e) { + throw asRestApiException("Cannot query groups", e); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java index 2fc7833..afcd273 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/BranchApiImpl.java
@@ -14,9 +14,12 @@ package com.google.gerrit.server.api.projects; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + import com.google.gerrit.extensions.api.projects.BranchApi; import com.google.gerrit.extensions.api.projects.BranchInfo; import com.google.gerrit.extensions.api.projects.BranchInput; +import com.google.gerrit.extensions.api.projects.ReflogEntryInfo; import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.RestApiException; @@ -27,11 +30,12 @@ import com.google.gerrit.server.project.FileResource; import com.google.gerrit.server.project.FilesCollection; import com.google.gerrit.server.project.GetContent; +import com.google.gerrit.server.project.GetReflog; import com.google.gerrit.server.project.ProjectResource; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import java.io.IOException; +import java.util.List; public class BranchApiImpl implements BranchApi { interface Factory { @@ -43,6 +47,7 @@ private final DeleteBranch deleteBranch; private final FilesCollection filesCollection; private final GetContent getContent; + private final GetReflog getReflog; private final String ref; private final ProjectResource project; @@ -53,6 +58,7 @@ DeleteBranch deleteBranch, FilesCollection filesCollection, GetContent getContent, + GetReflog getReflog, @Assisted ProjectResource project, @Assisted String ref) { this.branches = branches; @@ -60,6 +66,7 @@ this.deleteBranch = deleteBranch; this.filesCollection = filesCollection; this.getContent = getContent; + this.getReflog = getReflog; this.project = project; this.ref = ref; } @@ -69,8 +76,8 @@ try { createBranchFactory.create(ref).apply(project, input); return this; - } catch (IOException e) { - throw new RestApiException("Cannot create branch", e); + } catch (Exception e) { + throw asRestApiException("Cannot create branch", e); } } @@ -78,8 +85,8 @@ public BranchInfo get() throws RestApiException { try { return resource().getBranchInfo(); - } catch (IOException e) { - throw new RestApiException("Cannot read branch", e); + } catch (Exception e) { + throw asRestApiException("Cannot read branch", e); } } @@ -87,8 +94,8 @@ public void delete() throws RestApiException { try { deleteBranch.apply(resource(), new DeleteBranch.Input()); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot delete branch", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete branch", e); } } @@ -97,8 +104,17 @@ try { FileResource resource = filesCollection.parse(resource(), IdString.fromDecoded(path)); return getContent.apply(resource); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve file", e); + } + } + + @Override + public List<ReflogEntryInfo> reflog() throws RestApiException { + try { + return getReflog.apply(resource()); } catch (IOException e) { - throw new RestApiException("Cannot retrieve file", e); + throw new RestApiException("Cannot retrieve reflog", e); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java index 925b647..1595682 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ChildProjectApiImpl.java
@@ -19,8 +19,8 @@ import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.server.project.ChildProjectResource; import com.google.gerrit.server.project.GetChildProject; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; public class ChildProjectApiImpl implements ChildProjectApi { interface Factory { @@ -30,7 +30,7 @@ private final GetChildProject getChildProject; private final ChildProjectResource rsrc; - @AssistedInject + @Inject ChildProjectApiImpl(GetChildProject getChildProject, @Assisted ChildProjectResource rsrc) { this.getChildProject = getChildProject; this.rsrc = rsrc;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/CommitApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/CommitApiImpl.java new file mode 100644 index 0000000..cbdd03d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/CommitApiImpl.java
@@ -0,0 +1,54 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.projects; + +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + +import com.google.gerrit.extensions.api.changes.ChangeApi; +import com.google.gerrit.extensions.api.changes.Changes; +import com.google.gerrit.extensions.api.changes.CherryPickInput; +import com.google.gerrit.extensions.api.projects.CommitApi; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.server.change.CherryPickCommit; +import com.google.gerrit.server.project.CommitResource; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +public class CommitApiImpl implements CommitApi { + public interface Factory { + CommitApiImpl create(CommitResource r); + } + + private final Changes changes; + private final CherryPickCommit cherryPickCommit; + private final CommitResource commitResource; + + @Inject + CommitApiImpl( + Changes changes, CherryPickCommit cherryPickCommit, @Assisted CommitResource commitResource) { + this.changes = changes; + this.cherryPickCommit = cherryPickCommit; + this.commitResource = commitResource; + } + + @Override + public ChangeApi cherryPick(CherryPickInput input) throws RestApiException { + try { + return changes.id(cherryPickCommit.apply(commitResource, input)._number); + } catch (Exception e) { + throw asRestApiException("Cannot cherry pick", e); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java index 975e6c1..a4fe39b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/Module.java
@@ -26,5 +26,6 @@ factory(TagApiImpl.Factory.class); factory(ProjectApiImpl.Factory.class); factory(ChildProjectApiImpl.Factory.class); + factory(CommitApiImpl.Factory.class); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java index e29d633..104fb94 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectApiImpl.java
@@ -14,13 +14,14 @@ package com.google.gerrit.server.api.projects; -import static com.google.gerrit.server.account.CapabilityUtils.checkRequiresCapability; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; import com.google.gerrit.extensions.api.access.ProjectAccessInfo; import com.google.gerrit.extensions.api.access.ProjectAccessInput; import com.google.gerrit.extensions.api.projects.BranchApi; import com.google.gerrit.extensions.api.projects.BranchInfo; import com.google.gerrit.extensions.api.projects.ChildProjectApi; +import com.google.gerrit.extensions.api.projects.CommitApi; import com.google.gerrit.extensions.api.projects.ConfigInfo; import com.google.gerrit.extensions.api.projects.ConfigInput; import com.google.gerrit.extensions.api.projects.DeleteBranchesInput; @@ -38,7 +39,10 @@ import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.project.ChildProjectsCollection; +import com.google.gerrit.server.project.CommitsCollection; import com.google.gerrit.server.project.CreateProject; import com.google.gerrit.server.project.DeleteBranches; import com.google.gerrit.server.project.DeleteTags; @@ -54,12 +58,9 @@ import com.google.gerrit.server.project.PutConfig; import com.google.gerrit.server.project.PutDescription; import com.google.gerrit.server.project.SetAccess; -import com.google.gwtorm.server.OrmException; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; -import java.io.IOException; import java.util.List; -import org.eclipse.jgit.errors.ConfigInvalidException; public class ProjectApiImpl implements ProjectApi { interface Factory { @@ -69,6 +70,7 @@ } private final CurrentUser user; + private final PermissionBackend permissionBackend; private final CreateProject.Factory createProjectFactory; private final ProjectApiImpl.Factory projectApi; private final ProjectsCollection projects; @@ -89,10 +91,13 @@ private final ListTags listTags; private final DeleteBranches deleteBranches; private final DeleteTags deleteTags; + private final CommitsCollection commitsCollection; + private final CommitApiImpl.Factory commitApi; @AssistedInject ProjectApiImpl( CurrentUser user, + PermissionBackend permissionBackend, CreateProject.Factory createProjectFactory, ProjectApiImpl.Factory projectApi, ProjectsCollection projects, @@ -111,9 +116,12 @@ ListTags listTags, DeleteBranches deleteBranches, DeleteTags deleteTags, + CommitsCollection commitsCollection, + CommitApiImpl.Factory commitApi, @Assisted ProjectResource project) { this( user, + permissionBackend, createProjectFactory, projectApi, projects, @@ -133,12 +141,15 @@ deleteBranches, deleteTags, project, + commitsCollection, + commitApi, null); } @AssistedInject ProjectApiImpl( CurrentUser user, + PermissionBackend permissionBackend, CreateProject.Factory createProjectFactory, ProjectApiImpl.Factory projectApi, ProjectsCollection projects, @@ -157,9 +168,12 @@ ListTags listTags, DeleteBranches deleteBranches, DeleteTags deleteTags, + CommitsCollection commitsCollection, + CommitApiImpl.Factory commitApi, @Assisted String name) { this( user, + permissionBackend, createProjectFactory, projectApi, projects, @@ -179,11 +193,14 @@ deleteBranches, deleteTags, null, + commitsCollection, + commitApi, name); } private ProjectApiImpl( CurrentUser user, + PermissionBackend permissionBackend, CreateProject.Factory createProjectFactory, ProjectApiImpl.Factory projectApi, ProjectsCollection projects, @@ -203,8 +220,11 @@ DeleteBranches deleteBranches, DeleteTags deleteTags, ProjectResource project, + CommitsCollection commitsCollection, + CommitApiImpl.Factory commitApi, String name) { this.user = user; + this.permissionBackend = permissionBackend; this.createProjectFactory = createProjectFactory; this.projectApi = projectApi; this.projects = projects; @@ -225,6 +245,8 @@ this.listTags = listTags; this.deleteBranches = deleteBranches; this.deleteTags = deleteTags; + this.commitsCollection = commitsCollection; + this.commitApi = commitApi; } @Override @@ -241,11 +263,12 @@ if (in.name != null && !name.equals(in.name)) { throw new BadRequestException("name must match input.name"); } - checkRequiresCapability(user, null, CreateProject.class); - createProjectFactory.create(name).apply(TopLevelResource.INSTANCE, in); + CreateProject impl = createProjectFactory.create(name); + permissionBackend.user(user).checkAny(GlobalPermission.fromAnnotation(impl.getClass())); + impl.apply(TopLevelResource.INSTANCE, in); return projectApi.create(projects.parse(name)); - } catch (IOException | ConfigInvalidException e) { - throw new RestApiException("Cannot create project: " + e.getMessage(), e); + } catch (Exception e) { + throw asRestApiException("Cannot create project: " + e.getMessage(), e); } } @@ -266,8 +289,8 @@ public ProjectAccessInfo access() throws RestApiException { try { return getAccess.apply(checkExists()); - } catch (IOException e) { - throw new RestApiException("Cannot get access rights", e); + } catch (Exception e) { + throw asRestApiException("Cannot get access rights", e); } } @@ -275,8 +298,8 @@ public ProjectAccessInfo access(ProjectAccessInput p) throws RestApiException { try { return setAccess.apply(checkExists(), p); - } catch (IOException e) { - throw new RestApiException("Cannot put access rights", e); + } catch (Exception e) { + throw asRestApiException("Cannot put access rights", e); } } @@ -284,8 +307,8 @@ public void description(DescriptionInput in) throws RestApiException { try { putDescription.apply(checkExists(), in); - } catch (IOException e) { - throw new RestApiException("Cannot put project description", e); + } catch (Exception e) { + throw asRestApiException("Cannot put project description", e); } } @@ -317,8 +340,8 @@ listBranches.setMatchRegex(request.getRegex()); try { return listBranches.apply(checkExists()); - } catch (IOException e) { - throw new RestApiException("Cannot list branches", e); + } catch (Exception e) { + throw asRestApiException("Cannot list branches", e); } } @@ -339,8 +362,8 @@ listTags.setMatchRegex(request.getRegex()); try { return listTags.apply(checkExists()); - } catch (IOException e) { - throw new RestApiException("Cannot list tags", e); + } catch (Exception e) { + throw asRestApiException("Cannot list tags", e); } } @@ -353,15 +376,19 @@ public List<ProjectInfo> children(boolean recursive) throws RestApiException { ListChildProjects list = children.list(); list.setRecursive(recursive); - return list.apply(checkExists()); + try { + return list.apply(checkExists()); + } catch (Exception e) { + throw asRestApiException("Cannot list children", e); + } } @Override public ChildProjectApi child(String name) throws RestApiException { try { return childApi.create(children.parse(checkExists(), IdString.fromDecoded(name))); - } catch (IOException e) { - throw new RestApiException("Cannot parse child project", e); + } catch (Exception e) { + throw asRestApiException("Cannot parse child project", e); } } @@ -379,8 +406,8 @@ public void deleteBranches(DeleteBranchesInput in) throws RestApiException { try { deleteBranches.apply(checkExists(), in); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot delete branches", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete branches", e); } } @@ -388,8 +415,17 @@ public void deleteTags(DeleteTagsInput in) throws RestApiException { try { deleteTags.apply(checkExists(), in); - } catch (OrmException | IOException e) { - throw new RestApiException("Cannot delete tags", e); + } catch (Exception e) { + throw asRestApiException("Cannot delete tags", e); + } + } + + @Override + public CommitApi commit(String commit) throws RestApiException { + try { + return commitApi.create(commitsCollection.parse(checkExists(), IdString.fromDecoded(commit))); + } catch (Exception e) { + throw asRestApiException("Cannot parse commit", e); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java index 9483508..702a7e9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -14,6 +14,8 @@ package com.google.gerrit.server.api.projects; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + import com.google.gerrit.extensions.api.projects.ProjectApi; import com.google.gerrit.extensions.api.projects.ProjectInput; import com.google.gerrit.extensions.api.projects.Projects; @@ -21,13 +23,13 @@ import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ListProjects; import com.google.gerrit.server.project.ListProjects.FilterType; import com.google.gerrit.server.project.ProjectsCollection; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import java.io.IOException; import java.util.SortedMap; @Singleton @@ -52,8 +54,8 @@ return api.create(projects.parse(name)); } catch (UnprocessableEntityException e) { return api.create(name); - } catch (IOException e) { - throw new RestApiException("Cannot retrieve project"); + } catch (Exception e) { + throw asRestApiException("Cannot retrieve project", e); } } @@ -77,12 +79,17 @@ return new ListRequest() { @Override public SortedMap<String, ProjectInfo> getAsMap() throws RestApiException { - return list(this); + try { + return list(this); + } catch (Exception e) { + throw asRestApiException("project list unavailable", e); + } } }; } - private SortedMap<String, ProjectInfo> list(ListRequest request) throws RestApiException { + private SortedMap<String, ProjectInfo> list(ListRequest request) + throws RestApiException, PermissionBackendException { ListProjects lp = listProvider.get(); lp.setShowDescription(request.getDescription()); lp.setLimit(request.getLimit());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java index 4e81407..283d117 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/projects/TagApiImpl.java
@@ -14,6 +14,8 @@ package com.google.gerrit.server.api.projects; +import static com.google.gerrit.server.api.ApiUtil.asRestApiException; + import com.google.gerrit.extensions.api.projects.TagApi; import com.google.gerrit.extensions.api.projects.TagInfo; import com.google.gerrit.extensions.api.projects.TagInput; @@ -25,7 +27,6 @@ import com.google.gerrit.server.project.ProjectResource; import com.google.gerrit.server.project.TagResource; import com.google.gerrit.server.project.TagsCollection; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import java.io.IOException; @@ -63,8 +64,8 @@ try { createTagFactory.create(ref).apply(project, input); return this; - } catch (IOException e) { - throw new RestApiException("Cannot create tag", e); + } catch (Exception e) { + throw asRestApiException("Cannot create tag", e); } } @@ -72,8 +73,8 @@ public TagInfo get() throws RestApiException { try { return listTags.get(project, IdString.fromDecoded(ref)); - } catch (IOException e) { - throw new RestApiException(e.getMessage()); + } catch (Exception e) { + throw asRestApiException("Cannot get tag", e); } } @@ -81,8 +82,8 @@ public void delete() throws RestApiException { try { deleteTag.apply(resource(), new DeleteTag.Input()); - } catch (OrmException | IOException e) { - throw new RestApiException(e.getMessage()); + } catch (Exception e) { + throw asRestApiException("Cannot delete tag", e); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java index 4d135b8..d41f02c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupIdHandler.java
@@ -39,7 +39,7 @@ } @Override - public final int parseArguments(final Parameters params) throws CmdLineException { + public final int parseArguments(Parameters params) throws CmdLineException { final String n = params.getParameter(0); final AccountGroup group = groupCache.get(new AccountGroup.NameKey(n)); if (group == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java index 79ab8c8..d547b8c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountGroupUUIDHandler.java
@@ -41,7 +41,7 @@ } @Override - public final int parseArguments(final Parameters params) throws CmdLineException { + public final int parseArguments(Parameters params) throws CmdLineException { final String n = params.getParameter(0); GroupReference group = GroupBackends.findExactSuggestion(groupBackend, n); if (group == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java index 7562801..ce31cac 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/AccountIdHandler.java
@@ -27,6 +27,7 @@ import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import java.io.IOException; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.OptionDef; @@ -82,8 +83,12 @@ throw new CmdLineException(owner, "user \"" + token + "\" not found"); } } - } catch (OrmException | IOException e) { + } catch (OrmException e) { throw new CmdLineException(owner, "database is down"); + } catch (IOException e) { + throw new CmdLineException(owner, "Failed to load account", e); + } catch (ConfigInvalidException e) { + throw new CmdLineException(owner, "Invalid account config", e); } setter.addValue(accountId); return 1;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java index bdf0c91..0e841ec 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
@@ -45,7 +45,7 @@ } @Override - public final int parseArguments(final Parameters params) throws CmdLineException { + public final int parseArguments(Parameters params) throws CmdLineException { final String token = params.getParameter(0); final String[] tokens = token.split(","); if (tokens.length != 3) { @@ -57,7 +57,7 @@ final Change.Key key = Change.Key.parse(tokens[2]); final Project.NameKey project = new Project.NameKey(tokens[0]); final Branch.NameKey branch = new Branch.NameKey(project, tokens[1]); - for (final ChangeData cd : queryProvider.get().byBranchKey(branch, key)) { + for (ChangeData cd : queryProvider.get().byBranchKey(branch, key)) { setter.addValue(cd.getId()); return 1; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java index e8283be..cb70abf 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/PatchSetIdHandler.java
@@ -35,7 +35,7 @@ } @Override - public final int parseArguments(final Parameters params) throws CmdLineException { + public final int parseArguments(Parameters params) throws CmdLineException { final String token = params.getParameter(0); final PatchSet.Id id; try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java index 02e907f..1823527 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
@@ -15,8 +15,12 @@ package com.google.gerrit.server.args4j; import com.google.gerrit.common.ProjectUtil; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.CurrentUser; +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.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; import com.google.inject.Inject; @@ -34,23 +38,27 @@ public class ProjectControlHandler extends OptionHandler<ProjectControl> { private static final Logger log = LoggerFactory.getLogger(ProjectControlHandler.class); + private final ProjectControl.GenericFactory projectControlFactory; + private final PermissionBackend permissionBackend; private final Provider<CurrentUser> user; @Inject public ProjectControlHandler( - final ProjectControl.GenericFactory projectControlFactory, + ProjectControl.GenericFactory projectControlFactory, + PermissionBackend permissionBackend, Provider<CurrentUser> user, @Assisted final CmdLineParser parser, @Assisted final OptionDef option, @Assisted final Setter<ProjectControl> setter) { super(parser, option, setter); this.projectControlFactory = projectControlFactory; + this.permissionBackend = permissionBackend; this.user = user; } @Override - public final int parseArguments(final Parameters params) throws CmdLineException { + public final int parseArguments(Parameters params) throws CmdLineException { String projectName = params.getParameter(0); while (projectName.endsWith("/")) { @@ -69,14 +77,15 @@ String nameWithoutSuffix = ProjectUtil.stripGitSuffix(projectName); Project.NameKey nameKey = new Project.NameKey(nameWithoutSuffix); - final ProjectControl control; + ProjectControl control; try { - control = - projectControlFactory.validateFor( - nameKey, ProjectControl.OWNER | ProjectControl.VISIBLE, user.get()); + control = projectControlFactory.controlFor(nameKey, user.get()); + permissionBackend.user(user).project(nameKey).check(ProjectPermission.ACCESS); + } catch (AuthException e) { + throw new CmdLineException(owner, new NoSuchProjectException(nameKey).getMessage()); } catch (NoSuchProjectException e) { throw new CmdLineException(owner, e.getMessage()); - } catch (IOException e) { + } catch (PermissionBackendException | IOException e) { log.warn("Cannot load project " + nameWithoutSuffix, e); throw new CmdLineException(owner, new NoSuchProjectException(nameKey).getMessage()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SocketAddressHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SocketAddressHandler.java index e0193c5..4325c00 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SocketAddressHandler.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SocketAddressHandler.java
@@ -36,7 +36,7 @@ } @Override - public final int parseArguments(final Parameters params) throws CmdLineException { + public final int parseArguments(Parameters params) throws CmdLineException { final String token = params.getParameter(0); try { setter.addValue(SocketUtil.parse(token, 0));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SubcommandHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SubcommandHandler.java index b7af2e7..0be75a7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SubcommandHandler.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/SubcommandHandler.java
@@ -34,7 +34,7 @@ } @Override - public final int parseArguments(final Parameters params) throws CmdLineException { + public final int parseArguments(Parameters params) throws CmdLineException { setter.addValue(params.getParameter(0)); owner.stopOptionParsing(); return 1;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthenticationUnavailableException.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthenticationUnavailableException.java index b0b6142..1b1faa4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthenticationUnavailableException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthenticationUnavailableException.java
@@ -20,7 +20,7 @@ public class AuthenticationUnavailableException extends AccountException { private static final long serialVersionUID = 1L; - public AuthenticationUnavailableException(final String message, final Throwable why) { + public AuthenticationUnavailableException(String message, Throwable why) { super(message, why); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java index 3ad97b0..af9c51b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
@@ -33,7 +33,7 @@ } @Override - public AuthUser authenticate(final AuthRequest request) throws AuthException { + public AuthUser authenticate(AuthRequest request) throws AuthException { List<AuthUser> authUsers = new ArrayList<>(); List<AuthException> authExs = new ArrayList<>(); for (AuthBackend backend : authBackends) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java index 1acd647..20a2be6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -73,7 +73,7 @@ @Inject Helper( - @GerritServerConfig final Config config, + @GerritServerConfig Config config, @Named(LdapModule.PARENT_GROUPS_CACHE) Cache<String, ImmutableSet<String>> parentGroups) { this.config = config; this.server = LdapRealm.optional(config, "server"); @@ -135,7 +135,7 @@ return new InitialDirContext(env); } - private DirContext kerberosOpen(final Properties env) throws LoginException, NamingException { + private DirContext kerberosOpen(Properties env) throws LoginException, NamingException { LoginContext ctx = new LoginContext("KerberosLogin"); ctx.login(); Subject subject = ctx.getSubject(); @@ -207,8 +207,7 @@ } Set<AccountGroup.UUID> queryForGroups( - final DirContext ctx, final String username, LdapQuery.Result account) - throws NamingException { + final DirContext ctx, String username, LdapQuery.Result account) throws NamingException { final LdapSchema schema = getSchema(ctx); final Set<String> groupDNs = new HashSet<>(); @@ -330,7 +329,7 @@ final ParameterizedString groupName; final List<LdapQuery> groupMemberQueryList; - LdapSchema(final DirContext ctx) { + LdapSchema(DirContext ctx) { type = discoverLdapType(ctx); groupMemberQueryList = new ArrayList<>(); accountQueryList = new ArrayList<>(); @@ -360,7 +359,7 @@ throw new IllegalArgumentException("No variables in ldap.groupMemberPattern"); } - for (final String name : groupMemberQuery.getParameters()) { + for (String name : groupMemberQuery.getParameters()) { accountAtts.add(name); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java index 7feb745..3683b35 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -14,8 +14,8 @@ package com.google.gerrit.server.auth.ldap; -import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT; import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT; import static com.google.gerrit.server.auth.ldap.Helper.LDAP_UUID; import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_CACHE; import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_EXIST_CACHE; @@ -29,9 +29,9 @@ import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.account.ExternalId; import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.GroupMembership; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.auth.ldap.Helper.LdapSchema; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectControl; @@ -113,7 +113,7 @@ } @Override - public GroupDescription.Basic get(final AccountGroup.UUID uuid) { + public GroupDescription.Basic get(AccountGroup.UUID uuid) { if (!handles(uuid)) { return null; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java index 28eb05d..3d25e86 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
@@ -61,8 +61,7 @@ return pattern.getParameterNames(); } - List<Result> query(final DirContext ctx, final Map<String, String> params) - throws NamingException { + List<Result> query(DirContext ctx, Map<String, String> params) throws NamingException { final SearchControls sc = new SearchControls(); final NamingEnumeration<SearchResult> res; @@ -87,9 +86,9 @@ class Result { private final Map<String, Attribute> atts = new HashMap<>(); - Result(final SearchResult sr) { + Result(SearchResult sr) { if (returnAttributes != null) { - for (final String attName : returnAttributes) { + for (String attName : returnAttributes) { final Attribute a = sr.getAttributes().get(attName); if (a != null && a.size() > 0) { atts.put(attName, a); @@ -111,12 +110,12 @@ return get("dn"); } - String get(final String attName) throws NamingException { + String get(String attName) throws NamingException { final Attribute att = getAll(attName); return att != null && 0 < att.size() ? String.valueOf(att.get(0)) : null; } - Attribute getAll(final String attName) { + Attribute getAll(String attName) { return atts.get(attName); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java index 66b279f..a34e3fc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -14,7 +14,7 @@ package com.google.gerrit.server.auth.ldap; -import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT; import com.google.common.base.Strings; import com.google.common.cache.CacheLoader; @@ -25,17 +25,16 @@ import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.AbstractRealm; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.EmailExpander; -import com.google.gerrit.server.account.ExternalId; import com.google.gerrit.server.account.GroupBackends; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; import com.google.gerrit.server.auth.AuthenticationUnavailableException; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.GerritServerConfig; -import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; @@ -82,11 +81,9 @@ AuthConfig authConfig, EmailExpander emailExpander, LdapGroupBackend groupBackend, - @Named(LdapModule.GROUP_CACHE) - final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache, - @Named(LdapModule.USERNAME_CACHE) - final LoadingCache<String, Optional<Account.Id>> usernameCache, - @GerritServerConfig final Config config) { + @Named(LdapModule.GROUP_CACHE) LoadingCache<String, Set<AccountGroup.UUID>> membershipCache, + @Named(LdapModule.USERNAME_CACHE) LoadingCache<String, Optional<Account.Id>> usernameCache, + @GerritServerConfig Config config) { this.helper = helper; this.authConfig = authConfig; this.emailExpander = emailExpander; @@ -111,11 +108,11 @@ mandatoryGroup = optional(config, "mandatoryGroup"); } - static SearchScope scope(final Config c, final String setting) { + static SearchScope scope(Config c, String setting) { return c.getEnum("ldap", null, setting, SearchScope.SUBTREE); } - static String optional(final Config config, final String name) { + static String optional(Config config, String name) { return config.getString("ldap", null, name); } @@ -135,7 +132,7 @@ return config.getBoolean("ldap", name, defaultValue); } - static String required(final Config config, final String name) { + static String required(Config config, String name) { final String v = optional(config, name); if (v == null || "".equals(v)) { throw new IllegalArgumentException("No ldap." + name + " configured"); @@ -143,12 +140,12 @@ return v; } - static List<String> optionalList(final Config config, final String name) { + static List<String> optionalList(Config config, String name) { String[] s = config.getStringList("ldap", null, name); return Arrays.asList(s); } - static List<String> requiredList(final Config config, final String name) { + static List<String> requiredList(Config config, String name) { List<String> vlist = optionalList(config, name); if (vlist.isEmpty()) { @@ -158,7 +155,7 @@ return vlist; } - static String optdef(final Config c, final String n, final String d) { + static String optdef(Config c, String n, String d) { final String[] v = c.getStringList("ldap", null, n); if (v == null || v.length == 0) { return d; @@ -172,7 +169,7 @@ } } - static String reqdef(final Config c, final String n, final String d) { + static String reqdef(Config c, String n, String d) { final String v = optdef(c, n, d); if (v == null) { throw new IllegalArgumentException("No ldap." + n + " configured"); @@ -201,7 +198,7 @@ } @Override - public boolean allowsEdit(final AccountFieldName field) { + public boolean allowsEdit(AccountFieldName field) { return !readOnlyAccountFields.contains(field); } @@ -211,7 +208,7 @@ } final Map<String, String> values = new HashMap<>(); - for (final String name : m.attributes()) { + for (String name : m.attributes()) { values.put(name, m.get(name)); } @@ -220,7 +217,7 @@ } @Override - public AuthRequest authenticate(final AuthRequest who) throws AccountException { + public AuthRequest authenticate(AuthRequest who) throws AccountException { if (config.getBoolean("ldap", "localUsernameToLowerCase", false)) { who.setLocalUser(who.getLocalUser().toLowerCase(Locale.US)); } @@ -299,7 +296,7 @@ } @Override - public void onCreateAccount(final AuthRequest who, final Account account) { + public void onCreateAccount(AuthRequest who, Account account) { usernameCache.put(who.getLocalUser(), Optional.of(account.getId())); } @@ -318,24 +315,17 @@ } static class UserLoader extends CacheLoader<String, Optional<Account.Id>> { - private final SchemaFactory<ReviewDb> schema; + private final ExternalIds externalIds; @Inject - UserLoader(SchemaFactory<ReviewDb> schema) { - this.schema = schema; + UserLoader(ExternalIds externalIds) { + this.externalIds = externalIds; } @Override public Optional<Account.Id> load(String username) throws Exception { - try (ReviewDb db = schema.open()) { - return Optional.ofNullable( - ExternalId.from( - db.accountExternalIds() - .get( - ExternalId.Key.create(SCHEME_GERRIT, username) - .asAccountExternalIdKey()))) - .map(ExternalId::accountId); - } + return Optional.ofNullable(externalIds.get(ExternalId.Key.create(SCHEME_GERRIT, username))) + .map(ExternalId::accountId); } } @@ -343,7 +333,7 @@ private final Helper helper; @Inject - MemberLoader(final Helper helper) { + MemberLoader(Helper helper) { this.helper = helper; } @@ -366,12 +356,12 @@ private final Helper helper; @Inject - ExistenceLoader(final Helper helper) { + ExistenceLoader(Helper helper) { this.helper = helper; } @Override - public Boolean load(final String groupDn) throws Exception { + public Boolean load(String groupDn) throws Exception { final DirContext ctx = helper.open(); try { Name compositeGroupName = new CompositeName().add(groupDn);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapType.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapType.java index 5df13f9..fe1f1ff 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapType.java
@@ -22,7 +22,7 @@ abstract class LdapType { static final LdapType RFC_2307 = new Rfc2307(); - static LdapType guessType(final DirContext ctx) throws NamingException { + static LdapType guessType(DirContext ctx) throws NamingException { final Attributes rootAtts = ctx.getAttributes(""); Attribute supported = rootAtts.get("supportedCapabilities"); if (supported != null
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/SearchScope.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/SearchScope.java index 369914d..0038608 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/SearchScope.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/SearchScope.java
@@ -36,7 +36,7 @@ private final int scope; - SearchScope(final int scope) { + SearchScope(int scope) { this.scope = scope; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/openid/OpenIdProviderPattern.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/openid/OpenIdProviderPattern.java index d30e667..75f4213 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/openid/OpenIdProviderPattern.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/openid/OpenIdProviderPattern.java
@@ -14,7 +14,7 @@ package com.google.gerrit.server.auth.openid; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; public class OpenIdProviderPattern { public static OpenIdProviderPattern create(String pattern) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheMetrics.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheMetrics.java index 862f4e8..11f2034 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheMetrics.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheMetrics.java
@@ -30,65 +30,60 @@ @Singleton public class CacheMetrics { @Inject - public CacheMetrics(MetricMaker metrics, final DynamicMap<Cache<?, ?>> cacheMap) { + public CacheMetrics(MetricMaker metrics, DynamicMap<Cache<?, ?>> cacheMap) { Field<String> F_NAME = Field.ofString("cache_name"); - final CallbackMetric1<String, Long> memEnt = + CallbackMetric1<String, Long> memEnt = metrics.newCallbackMetric( "caches/memory_cached", Long.class, new Description("Memory entries").setGauge().setUnit("entries"), F_NAME); - final CallbackMetric1<String, Double> memHit = + CallbackMetric1<String, Double> memHit = metrics.newCallbackMetric( "caches/memory_hit_ratio", Double.class, new Description("Memory hit ratio").setGauge().setUnit("percent"), F_NAME); - final CallbackMetric1<String, Long> memEvict = + CallbackMetric1<String, Long> memEvict = metrics.newCallbackMetric( "caches/memory_eviction_count", Long.class, new Description("Memory eviction count").setGauge().setUnit("evicted entries"), F_NAME); - final CallbackMetric1<String, Long> perDiskEnt = + CallbackMetric1<String, Long> perDiskEnt = metrics.newCallbackMetric( "caches/disk_cached", Long.class, new Description("Disk entries used by persistent cache").setGauge().setUnit("entries"), F_NAME); - final CallbackMetric1<String, Double> perDiskHit = + CallbackMetric1<String, Double> perDiskHit = metrics.newCallbackMetric( "caches/disk_hit_ratio", Double.class, new Description("Disk hit ratio for persistent cache").setGauge().setUnit("percent"), F_NAME); - final Set<CallbackMetric<?>> cacheMetrics = + Set<CallbackMetric<?>> cacheMetrics = ImmutableSet.<CallbackMetric<?>>of(memEnt, memHit, memEvict, perDiskEnt, perDiskHit); metrics.newTrigger( cacheMetrics, - new Runnable() { - @Override - public void run() { - for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) { - Cache<?, ?> c = e.getProvider().get(); - String name = metricNameOf(e); - CacheStats cstats = c.stats(); - memEnt.set(name, c.size()); - memHit.set(name, cstats.hitRate() * 100); - memEvict.set(name, cstats.evictionCount()); - if (c instanceof PersistentCache) { - PersistentCache.DiskStats d = ((PersistentCache) c).diskStats(); - perDiskEnt.set(name, d.size()); - perDiskHit.set(name, hitRatio(d)); - } - } - for (CallbackMetric<?> cbm : cacheMetrics) { - cbm.prune(); + () -> { + for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) { + Cache<?, ?> c = e.getProvider().get(); + String name = metricNameOf(e); + CacheStats cstats = c.stats(); + memEnt.set(name, c.size()); + memHit.set(name, cstats.hitRate() * 100); + memEvict.set(name, cstats.evictionCount()); + if (c instanceof PersistentCache) { + PersistentCache.DiskStats d = ((PersistentCache) c).diskStats(); + perDiskEnt.set(name, d.size()); + perDiskHit.set(name, hitRatio(d)); } } + cacheMetrics.forEach(CallbackMetric::prune); }); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java index 0cafe6d..5073e4a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -21,10 +21,8 @@ import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.common.ChangeInfo; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.webui.UiAction; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; @@ -32,25 +30,26 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.git.AbandonOp; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; +import java.io.IOException; import java.util.Collection; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton -public class Abandon - implements RestModifyView<ChangeResource, AbandonInput>, UiAction<ChangeResource> { - private static final Logger log = LoggerFactory.getLogger(Abandon.class); - +public class Abandon extends RetryingRestModifyView<ChangeResource, AbandonInput, ChangeInfo> + implements UiAction<ChangeResource> { private final Provider<ReviewDb> dbProvider; private final ChangeJson.Factory json; - private final BatchUpdate.Factory batchUpdateFactory; private final AbandonOp.Factory abandonOpFactory; private final NotifyUtil notifyUtil; @@ -58,39 +57,51 @@ Abandon( Provider<ReviewDb> dbProvider, ChangeJson.Factory json, - BatchUpdate.Factory batchUpdateFactory, + RetryHelper retryHelper, AbandonOp.Factory abandonOpFactory, NotifyUtil notifyUtil) { + super(retryHelper); this.dbProvider = dbProvider; this.json = json; - this.batchUpdateFactory = batchUpdateFactory; this.abandonOpFactory = abandonOpFactory; this.notifyUtil = notifyUtil; } @Override - public ChangeInfo apply(ChangeResource req, AbandonInput input) - throws RestApiException, UpdateException, OrmException { - ChangeControl control = req.getControl(); - if (!control.canAbandon(dbProvider.get())) { - throw new AuthException("abandon not permitted"); - } + protected ChangeInfo applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource req, AbandonInput input) + throws RestApiException, UpdateException, OrmException, PermissionBackendException, + IOException, ConfigInvalidException { + req.permissions().database(dbProvider).check(ChangePermission.ABANDON); + + NotifyHandling notify = input.notify == null ? defaultNotify(req.getControl()) : input.notify; Change change = abandon( - control, input.message, input.notify, notifyUtil.resolveAccounts(input.notifyDetails)); + updateFactory, + req.getControl(), + input.message, + notify, + notifyUtil.resolveAccounts(input.notifyDetails)); return json.noOptions().format(change); } - public Change abandon(ChangeControl control) throws RestApiException, UpdateException { - return abandon(control, "", NotifyHandling.ALL, ImmutableListMultimap.of()); + private NotifyHandling defaultNotify(ChangeControl control) { + return control.getChange().hasReviewStarted() ? NotifyHandling.ALL : NotifyHandling.OWNER; } - public Change abandon(ChangeControl control, String msgTxt) + public Change abandon(BatchUpdate.Factory updateFactory, ChangeControl control) throws RestApiException, UpdateException { - return abandon(control, msgTxt, NotifyHandling.ALL, ImmutableListMultimap.of()); + return abandon(updateFactory, control, "", defaultNotify(control), ImmutableListMultimap.of()); + } + + public Change abandon(BatchUpdate.Factory updateFactory, ChangeControl control, String msgTxt) + throws RestApiException, UpdateException { + return abandon( + updateFactory, control, msgTxt, defaultNotify(control), ImmutableListMultimap.of()); } public Change abandon( + BatchUpdate.Factory updateFactory, ChangeControl control, String msgTxt, NotifyHandling notifyHandling, @@ -100,7 +111,7 @@ Account account = user.isIdentifiedUser() ? user.asIdentifiedUser().getAccount() : null; AbandonOp op = abandonOpFactory.create(account, msgTxt, notifyHandling, accountsToNotify); try (BatchUpdate u = - batchUpdateFactory.create( + updateFactory.create( dbProvider.get(), control.getProject().getNameKey(), control.getUser(), @@ -118,6 +129,7 @@ * matching project from its ChangeControl. Violations will result in a ResourceConflictException. */ public void batchAbandon( + BatchUpdate.Factory updateFactory, Project.NameKey project, CurrentUser user, Collection<ChangeControl> controls, @@ -129,8 +141,7 @@ return; } Account account = user.isIdentifiedUser() ? user.asIdentifiedUser().getAccount() : null; - try (BatchUpdate u = - batchUpdateFactory.create(dbProvider.get(), project, user, TimeUtil.nowTs())) { + try (BatchUpdate u = updateFactory.create(dbProvider.get(), project, user, TimeUtil.nowTs())) { for (ChangeControl control : controls) { if (!project.equals(control.getProject().getNameKey())) { throw new ResourceConflictException( @@ -147,31 +158,41 @@ } public void batchAbandon( - Project.NameKey project, CurrentUser user, Collection<ChangeControl> controls, String msgTxt) + BatchUpdate.Factory updateFactory, + Project.NameKey project, + CurrentUser user, + Collection<ChangeControl> controls, + String msgTxt) throws RestApiException, UpdateException { - batchAbandon(project, user, controls, msgTxt, NotifyHandling.ALL, ImmutableListMultimap.of()); + batchAbandon( + updateFactory, + project, + user, + controls, + msgTxt, + NotifyHandling.ALL, + ImmutableListMultimap.of()); } public void batchAbandon( - Project.NameKey project, CurrentUser user, Collection<ChangeControl> controls) + BatchUpdate.Factory updateFactory, + Project.NameKey project, + CurrentUser user, + Collection<ChangeControl> controls) throws RestApiException, UpdateException { - batchAbandon(project, user, controls, "", NotifyHandling.ALL, ImmutableListMultimap.of()); + batchAbandon( + updateFactory, project, user, controls, "", NotifyHandling.ALL, ImmutableListMultimap.of()); } @Override - public UiAction.Description getDescription(ChangeResource resource) { - boolean canAbandon = false; - try { - canAbandon = resource.getControl().canAbandon(dbProvider.get()); - } catch (OrmException e) { - log.error("Cannot check canAbandon status. Assuming false.", e); - } + public UiAction.Description getDescription(ChangeResource rsrc) { + Change change = rsrc.getChange(); return new UiAction.Description() .setLabel("Abandon") .setTitle("Abandon the change") .setVisible( - resource.getChange().getStatus().isOpen() - && resource.getChange().getStatus() != Change.Status.DRAFT - && canAbandon); + change.getStatus().isOpen() + && change.getStatus() != Change.Status.DRAFT + && rsrc.permissions().database(dbProvider).testOrFalse(ChangePermission.ABANDON)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java index 7c408c8..02ad66e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/AbandonUtil.java
@@ -24,6 +24,7 @@ import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeQueryBuilder; import com.google.gerrit.server.query.change.ChangeQueryProcessor; +import com.google.gerrit.server.update.BatchUpdate; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -58,7 +59,7 @@ internalUser = internalUserFactory.create(); } - public void abandonInactiveOpenChanges() { + public void abandonInactiveOpenChanges(BatchUpdate.Factory updateFactory) { if (cfg.getAbandonAfter() <= 0) { return; } @@ -85,7 +86,7 @@ for (Project.NameKey project : abandons.keySet()) { Collection<ChangeControl> changes = getValidChanges(abandons.get(project), query); try { - abandon.batchAbandon(project, internalUser, changes, message); + abandon.batchAbandon(updateFactory, project, internalUser, changes, message); count += changes.size(); } catch (Throwable e) { StringBuilder msg = new StringBuilder("Failed to auto-abandon inactive change(s):");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java index 519a4bc..19fdcfb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ActionJson.java
@@ -30,14 +30,11 @@ import com.google.gerrit.extensions.restapi.RestView; 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.project.ChangeControl; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; -import com.google.inject.Provider; import com.google.inject.Singleton; -import com.google.inject.util.Providers; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -48,6 +45,7 @@ private final Revisions revisions; private final ChangeJson.Factory changeJsonFactory; private final ChangeResource.Factory changeResourceFactory; + private final UiActions uiActions; private final DynamicMap<RestView<ChangeResource>> changeViews; private final DynamicSet<ActionVisitor> visitorSet; @@ -56,11 +54,13 @@ Revisions revisions, ChangeJson.Factory changeJsonFactory, ChangeResource.Factory changeResourceFactory, + UiActions uiActions, DynamicMap<RestView<ChangeResource>> changeViews, DynamicSet<ActionVisitor> visitorSet) { this.revisions = revisions; this.changeJsonFactory = changeJsonFactory; this.changeResourceFactory = changeResourceFactory; + this.uiActions = uiActions; this.changeViews = changeViews; this.visitorSet = visitorSet; } @@ -122,6 +122,7 @@ copy.mergeable = changeInfo.mergeable; copy.insertions = changeInfo.insertions; copy.deletions = changeInfo.deletions; + copy.isPrivate = changeInfo.isPrivate; copy.subject = changeInfo.subject; copy.status = changeInfo.status; copy.owner = changeInfo.owner; @@ -161,9 +162,9 @@ return out; } - Provider<CurrentUser> userProvider = Providers.of(ctl.getUser()); FluentIterable<UiAction.Description> descs = - UiActions.from(changeViews, changeResourceFactory.create(ctl), userProvider); + uiActions.from(changeViews, changeResourceFactory.create(ctl)); + // 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. @@ -197,10 +198,10 @@ if (!rsrc.getControl().getUser().isIdentifiedUser()) { return ImmutableMap.of(); } + Map<String, ActionInfo> out = new LinkedHashMap<>(); - Provider<CurrentUser> userProvider = Providers.of(rsrc.getControl().getUser()); ACTION: - for (UiAction.Description d : UiActions.from(revisions, rsrc, userProvider)) { + for (UiAction.Description d : uiActions.from(revisions, rsrc)) { ActionInfo actionInfo = new ActionInfo(d); for (ActionVisitor visitor : visitors) { if (!visitor.visit(d.getId(), actionInfo, changeInfo, revisionInfo)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java new file mode 100644 index 0000000..aee97fc --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ApplyFix.java
@@ -0,0 +1,84 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.extensions.common.EditInfo; +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.RestModifyView; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.Project; +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.tree.TreeModification; +import com.google.gerrit.server.fixes.FixReplacementInterpreter; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.project.InvalidChangeOperationException; +import com.google.gerrit.server.project.ProjectState; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.List; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; + +@Singleton +public class ApplyFix implements RestModifyView<FixResource, Void> { + + private final GitRepositoryManager gitRepositoryManager; + private final FixReplacementInterpreter fixReplacementInterpreter; + private final ChangeEditModifier changeEditModifier; + private final ChangeEditJson changeEditJson; + + @Inject + public ApplyFix( + GitRepositoryManager gitRepositoryManager, + FixReplacementInterpreter fixReplacementInterpreter, + ChangeEditModifier changeEditModifier, + ChangeEditJson changeEditJson) { + this.gitRepositoryManager = gitRepositoryManager; + this.fixReplacementInterpreter = fixReplacementInterpreter; + this.changeEditModifier = changeEditModifier; + this.changeEditJson = changeEditJson; + } + + @Override + public Response<EditInfo> apply(FixResource fixResource, Void nothing) + throws AuthException, OrmException, ResourceConflictException, IOException, + ResourceNotFoundException, PermissionBackendException { + RevisionResource revisionResource = fixResource.getRevisionResource(); + Project.NameKey project = revisionResource.getProject(); + ProjectState projectState = revisionResource.getControl().getProjectControl().getProjectState(); + PatchSet patchSet = revisionResource.getPatchSet(); + ObjectId patchSetCommitId = ObjectId.fromString(patchSet.getRevision().get()); + + try (Repository repository = gitRepositoryManager.openRepository(project)) { + List<TreeModification> treeModifications = + fixReplacementInterpreter.toTreeModifications( + repository, projectState, patchSetCommitId, fixResource.getFixReplacements()); + ChangeEdit changeEdit = + changeEditModifier.combineWithModifiedPatchSetTree( + repository, revisionResource.getControl(), patchSet, treeModifications); + return Response.ok(changeEditJson.toEditInfo(changeEdit, false)); + } catch (InvalidChangeOperationException e) { + throw new ResourceConflictException(e.getMessage()); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeCleanupRunner.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeCleanupRunner.java index 4299188..eb7d099 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeCleanupRunner.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeCleanupRunner.java
@@ -17,10 +17,13 @@ import static com.google.gerrit.server.config.ScheduleConfig.MISSING_CONFIG; import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.lifecycle.LifecycleModule; import com.google.gerrit.server.config.ChangeCleanupConfig; import com.google.gerrit.server.config.ScheduleConfig; import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.ManualRequestContext; import com.google.gerrit.server.util.OneOffRequestContext; import com.google.gwtorm.server.OrmException; @@ -81,19 +84,29 @@ private final OneOffRequestContext oneOffRequestContext; private final AbandonUtil abandonUtil; + private final RetryHelper retryHelper; @Inject - ChangeCleanupRunner(OneOffRequestContext oneOffRequestContext, AbandonUtil abandonUtil) { + ChangeCleanupRunner( + OneOffRequestContext oneOffRequestContext, AbandonUtil abandonUtil, RetryHelper retryHelper) { this.oneOffRequestContext = oneOffRequestContext; this.abandonUtil = abandonUtil; + this.retryHelper = retryHelper; } @Override public void run() { log.info("Running change cleanups."); try (ManualRequestContext ctx = oneOffRequestContext.open()) { - abandonUtil.abandonInactiveOpenChanges(); - } catch (OrmException e) { + // 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.execute( + updateFactory -> { + abandonUtil.abandonInactiveOpenChanges(updateFactory); + return null; + }); + } catch (RestApiException | UpdateException | OrmException e) { log.error("Failed to cleanup changes.", e); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java index 108e180..1695d0c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEditResource.java
@@ -16,8 +16,6 @@ import com.google.gerrit.extensions.restapi.RestResource; import com.google.gerrit.extensions.restapi.RestView; -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.edit.ChangeEdit; import com.google.gerrit.server.project.ChangeControl; import com.google.inject.TypeLiteral; @@ -67,12 +65,4 @@ public String getPath() { return path; } - - Account.Id getAccountId() { - return getUser().getAccountId(); - } - - IdentifiedUser getUser() { - return edit.getUser(); - } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java index fbb2115..929268b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -46,6 +46,7 @@ import com.google.gerrit.server.edit.UnchangedCommitMessageException; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.patch.PatchListNotAvailableException; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gwtorm.server.OrmException; @@ -154,7 +155,8 @@ @Override public Response<?> apply(ChangeResource resource, Put.Input input) - throws AuthException, ResourceConflictException, IOException, OrmException { + throws AuthException, ResourceConflictException, IOException, OrmException, + PermissionBackendException { putEdit.apply(resource.getControl(), path, input.content); return Response.none(); } @@ -178,7 +180,8 @@ @Override public Response<?> apply(ChangeResource rsrc, DeleteFile.Input in) - throws IOException, AuthException, ResourceConflictException, OrmException { + throws IOException, AuthException, ResourceConflictException, OrmException, + PermissionBackendException { return deleteContent.apply(rsrc.getControl(), path); } } @@ -229,7 +232,8 @@ } try { editInfo.files = - fileInfoJson.toFileInfoMap(rsrc.getChange(), edit.get().getRevision(), basePatchSet); + fileInfoJson.toFileInfoMap( + rsrc.getChange(), edit.get().getEditCommit(), basePatchSet); } catch (PatchListNotAvailableException e) { throw new ResourceNotFoundException(e.getMessage()); } @@ -267,7 +271,8 @@ @Override public Response<?> apply(ChangeResource resource, Post.Input input) - throws AuthException, IOException, ResourceConflictException, OrmException { + throws AuthException, IOException, ResourceConflictException, OrmException, + PermissionBackendException { Project.NameKey project = resource.getProject(); try (Repository repository = repositoryManager.openRepository(project)) { ChangeControl changeControl = resource.getControl(); @@ -313,12 +318,14 @@ @Override public Response<?> apply(ChangeEditResource rsrc, Input input) - throws AuthException, ResourceConflictException, IOException, OrmException { + throws AuthException, ResourceConflictException, IOException, OrmException, + PermissionBackendException { return apply(rsrc.getControl(), rsrc.getPath(), input.content); } public Response<?> apply(ChangeControl changeControl, String path, RawInput newContent) - throws ResourceConflictException, AuthException, IOException, OrmException { + throws ResourceConflictException, AuthException, IOException, OrmException, + PermissionBackendException { if (Strings.isNullOrEmpty(path) || path.charAt(0) == '/') { throw new ResourceConflictException("Invalid path: " + path); } @@ -355,12 +362,14 @@ @Override public Response<?> apply(ChangeEditResource rsrc, DeleteContent.Input input) - throws AuthException, ResourceConflictException, OrmException, IOException { + throws AuthException, ResourceConflictException, OrmException, IOException, + PermissionBackendException { return apply(rsrc.getControl(), rsrc.getPath()); } public Response<?> apply(ChangeControl changeControl, String filePath) - throws AuthException, IOException, OrmException, ResourceConflictException { + throws AuthException, IOException, OrmException, ResourceConflictException, + PermissionBackendException { Project.NameKey project = changeControl.getChange().getProject(); try (Repository repository = repositoryManager.openRepository(project)) { editModifier.deleteFile(repository, changeControl, filePath); @@ -395,9 +404,10 @@ rsrc.getControl().getProjectControl().getProjectState(), base ? ObjectId.fromString(edit.getBasePatchSet().getRevision().get()) - : ObjectId.fromString(edit.getRevision().get()), - rsrc.getPath())); - } catch (ResourceNotFoundException rnfe) { + : edit.getEditCommit(), + rsrc.getPath(), + null)); + } catch (ResourceNotFoundException | BadRequestException e) { return Response.none(); } } @@ -454,7 +464,7 @@ @Override public Object apply(ChangeResource rsrc, Input input) throws AuthException, IOException, BadRequestException, ResourceConflictException, - OrmException { + OrmException, PermissionBackendException { if (input == null || Strings.isNullOrEmpty(input.message)) { throw new BadRequestException("commit message must be provided"); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java index da34064..bf417d0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -51,7 +51,9 @@ import com.google.gerrit.server.mail.send.CreateChangeSender; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.patch.PatchSetInfoFactory; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.project.ProjectControl; @@ -68,6 +70,7 @@ import java.io.IOException; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -75,6 +78,7 @@ import java.util.concurrent.Future; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.util.ChangeIdUtil; import org.slf4j.Logger; @@ -82,11 +86,12 @@ public class ChangeInserter implements InsertChangeOp { public interface Factory { - ChangeInserter create(Change.Id cid, RevCommit rc, String refName); + ChangeInserter create(Change.Id cid, ObjectId commitId, String refName); } private static final Logger log = LoggerFactory.getLogger(ChangeInserter.class); + private final PermissionBackend permissionBackend; private final ProjectControl.GenericFactory projectControlFactory; private final IdentifiedUser.GenericFactory userFactory; private final ChangeControl.GenericFactory changeControlFactory; @@ -99,10 +104,11 @@ private final CommitValidators.Factory commitValidatorsFactory; private final RevisionCreated revisionCreated; private final CommentAdded commentAdded; + private final NotesMigration migration; private final Change.Id changeId; private final PatchSet.Id psId; - private final RevCommit commit; + private final ObjectId commitId; private final String refName; // Fields exposed as setters. @@ -110,20 +116,22 @@ private String topic; private String message; private String patchSetDescription; + private boolean isPrivate; + private boolean workInProgress; private List<String> groups = Collections.emptyList(); - private CommitValidators.Policy validatePolicy = CommitValidators.Policy.GERRIT; + private boolean validate = true; private NotifyHandling notify = NotifyHandling.ALL; private ListMultimap<RecipientType, Account.Id> accountsToNotify = ImmutableListMultimap.of(); private Set<Account.Id> reviewers; private Set<Account.Id> extraCC; private Map<String, Short> approvals; private RequestScopePropagator requestScopePropagator; - private ReceiveCommand updateRefCommand; private boolean fireRevisionCreated; private boolean sendMail; private boolean updateRef; // Fields set during the insertion process. + private ReceiveCommand cmd; private Change change; private ChangeMessage changeMessage; private PatchSetInfo patchSetInfo; @@ -132,6 +140,7 @@ @Inject ChangeInserter( + PermissionBackend permissionBackend, ProjectControl.GenericFactory projectControlFactory, IdentifiedUser.GenericFactory userFactory, ChangeControl.GenericFactory changeControlFactory, @@ -144,9 +153,11 @@ CommitValidators.Factory commitValidatorsFactory, CommentAdded commentAdded, RevisionCreated revisionCreated, + NotesMigration migration, @Assisted Change.Id changeId, - @Assisted RevCommit commit, + @Assisted ObjectId commitId, @Assisted String refName) { + this.permissionBackend = permissionBackend; this.projectControlFactory = projectControlFactory; this.userFactory = userFactory; this.changeControlFactory = changeControlFactory; @@ -159,57 +170,62 @@ this.commitValidatorsFactory = commitValidatorsFactory; this.revisionCreated = revisionCreated; this.commentAdded = commentAdded; + this.migration = migration; this.changeId = changeId; this.psId = new PatchSet.Id(changeId, INITIAL_PATCH_SET_ID); - this.commit = commit; + this.commitId = commitId.copy(); this.refName = refName; this.reviewers = Collections.emptySet(); this.extraCC = Collections.emptySet(); this.approvals = Collections.emptyMap(); - this.updateRefCommand = null; this.fireRevisionCreated = true; this.sendMail = true; this.updateRef = true; } @Override - public Change createChange(Context ctx) { + public Change createChange(Context ctx) throws IOException { change = new Change( - getChangeKey(commit), + getChangeKey(ctx.getRevWalk(), commitId), changeId, ctx.getAccountId(), new Branch.NameKey(ctx.getProject(), refName), ctx.getWhen()); change.setStatus(MoreObjects.firstNonNull(status, Change.Status.NEW)); change.setTopic(topic); + change.setPrivate(isPrivate); + change.setWorkInProgress(workInProgress); + change.setReviewStarted(!workInProgress); return change; } - private static Change.Key getChangeKey(RevCommit commit) { + private static Change.Key getChangeKey(RevWalk rw, ObjectId id) throws IOException { + RevCommit commit = rw.parseCommit(id); + rw.parseBody(commit); List<String> idList = commit.getFooterLines(FooterConstants.CHANGE_ID); if (!idList.isEmpty()) { return new Change.Key(idList.get(idList.size() - 1).trim()); } - ObjectId id = + ObjectId changeId = ChangeIdUtil.computeChangeId( commit.getTree(), commit, commit.getAuthorIdent(), commit.getCommitterIdent(), commit.getShortMessage()); - StringBuilder changeId = new StringBuilder(); - changeId.append("I").append(ObjectId.toString(id)); - return new Change.Key(changeId.toString()); + StringBuilder changeIdStr = new StringBuilder(); + changeIdStr.append("I").append(ObjectId.toString(changeId)); + return new Change.Key(changeIdStr.toString()); } public PatchSet.Id getPatchSetId() { return psId; } - public RevCommit getCommit() { - return commit; + public ObjectId getCommitId() { + return commitId; } public Change getChange() { @@ -233,8 +249,8 @@ return this; } - public ChangeInserter setValidatePolicy(CommitValidators.Policy validate) { - this.validatePolicy = checkNotNull(validate); + public ChangeInserter setValidate(boolean validate) { + this.validate = validate; return this; } @@ -259,11 +275,22 @@ return this; } + public ChangeInserter setPrivate(boolean isPrivate) { + checkState(change == null, "setPrivate(boolean) only valid before creating change"); + this.isPrivate = isPrivate; + return this; + } + public ChangeInserter setDraft(boolean draft) { checkState(change == null, "setDraft(boolean) only valid before creating change"); return setStatus(draft ? Change.Status.DRAFT : Change.Status.NEW); } + public ChangeInserter setWorkInProgress(boolean workInProgress) { + this.workInProgress = workInProgress; + return this; + } + public ChangeInserter setStatus(Change.Status status) { checkState(change == null, "setStatus(Change.Status) only valid before creating change"); this.status = status; @@ -292,10 +319,6 @@ return this; } - public void setUpdateRefCommand(ReceiveCommand cmd) { - updateRefCommand = cmd; - } - public void setPushCertificate(String cert) { pushCert = cert; } @@ -310,6 +333,18 @@ return this; } + /** + * Set whether to include the new patch set ref update in this update. + * + * <p>If false, the caller is responsible for creating the patch set ref <strong>before</strong> + * executing the containing {@code BatchUpdate}. + * + * <p>Should not be used in new code, as it doesn't result in a single atomic batch ref update for + * code and NoteDb meta refs. + * + * @param updateRef whether to update the ref during {@code updateRepo}. + */ + @Deprecated public ChangeInserter setUpdateRef(boolean updateRef) { this.updateRef = updateRef; return this; @@ -323,17 +358,18 @@ return changeMessage; } + public ReceiveCommand getCommand() { + return cmd; + } + @Override public void updateRepo(RepoContext ctx) throws ResourceConflictException, IOException { + cmd = new ReceiveCommand(ObjectId.zeroId(), commitId, psId.toRefName()); validate(ctx); if (!updateRef) { return; } - if (updateRefCommand == null) { - ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), commit, psId.toRefName())); - } else { - ctx.addRefUpdate(updateRefCommand); - } + ctx.addRefUpdate(cmd); } @Override @@ -342,7 +378,8 @@ change = ctx.getChange(); // Use defensive copy created by ChangeControl. ReviewDb db = ctx.getDb(); ChangeControl ctl = ctx.getControl(); - patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId); + patchSetInfo = + patchSetInfoFactory.get(ctx.getRevWalk(), ctx.getRevWalk().parseCommit(commitId), psId); ctx.getChange().setCurrentPatchSet(patchSetInfo); ChangeUpdate update = ctx.getUpdate(psId); @@ -351,11 +388,13 @@ update.setBranch(change.getDest().get()); update.setTopic(change.getTopic()); update.setPsDescription(patchSetDescription); + update.setPrivate(isPrivate); + update.setWorkInProgress(workInProgress); boolean draft = status == Change.Status.DRAFT; List<String> newGroups = groups; if (newGroups.isEmpty()) { - newGroups = GroupCollector.getDefaultGroups(commit); + newGroups = GroupCollector.getDefaultGroups(commitId); } patchSet = psUtil.insert( @@ -363,7 +402,7 @@ ctx.getRevWalk(), update, psId, - commit, + commitId, draft, newGroups, pushCert, @@ -379,6 +418,14 @@ */ update.fixStatus(change.getStatus()); + Set<Account.Id> reviewersToAdd = new HashSet<>(reviewers); + if (migration.readChanges()) { + approvalsUtil.addCcs( + ctx.getNotes(), update, filterOnChangeVisibility(db, ctx.getNotes(), extraCC)); + } else { + reviewersToAdd.addAll(extraCC); + } + LabelTypes labelTypes = ctl.getProjectControl().getLabelTypes(); approvalsUtil.addReviewers( db, @@ -387,7 +434,7 @@ change, patchSet, patchSetInfo, - filterOnChangeVisibility(db, ctx.getNotes(), reviewers), + filterOnChangeVisibility(db, ctx.getNotes(), reviewersToAdd), Collections.<Account.Id>emptySet()); approvalsUtil.addApprovalsForNewPatchSet( db, update, labelTypes, patchSet, ctx.getControl(), approvals); @@ -404,14 +451,14 @@ ctx.getUser(), patchSet.getCreatedOn(), message, - ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET); + ChangeMessagesUtil.uploadedPatchSetTag(workInProgress)); cmUtil.addChangeMessage(db, update, changeMessage); } return true; } private Set<Account.Id> filterOnChangeVisibility( - final ReviewDb db, final ChangeNotes notes, Set<Account.Id> accounts) { + final ReviewDb db, ChangeNotes notes, Set<Account.Id> accounts) { return accounts .stream() .filter( @@ -497,24 +544,27 @@ } private void validate(RepoContext ctx) throws IOException, ResourceConflictException { - if (validatePolicy == CommitValidators.Policy.NONE) { + if (!validate) { return; } + PermissionBackend.ForRef perm = + permissionBackend.user(ctx.getUser()).project(ctx.getProject()).ref(refName); try { RefControl refControl = projectControlFactory.controlFor(ctx.getProject(), ctx.getUser()).controlForRef(refName); - String refName = psId.toRefName(); - CommitReceivedEvent event = + try (CommitReceivedEvent event = new CommitReceivedEvent( - new ReceiveCommand(ObjectId.zeroId(), commit.getId(), refName), + cmd, refControl.getProjectControl().getProject(), change.getDest().get(), - commit, - ctx.getIdentifiedUser()); - commitValidatorsFactory - .create(validatePolicy, refControl, new NoSshInfo(), ctx.getRepository()) - .validate(event); + ctx.getRevWalk().getObjectReader(), + commitId, + ctx.getIdentifiedUser())) { + commitValidatorsFactory + .forGerritCommits(perm, refControl, new NoSshInfo(), ctx.getRevWalk()) + .validate(event); + } } catch (CommitValidationException e) { throw new ResourceConflictException(e.getFullMessage()); } catch (NoSuchProjectException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java index 41d101b..33a1565 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -59,8 +59,6 @@ import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; import com.google.gerrit.common.data.LabelValue; -import com.google.gerrit.common.data.Permission; -import com.google.gerrit.common.data.PermissionRange; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.common.data.SubmitTypeRecord; import com.google.gerrit.extensions.api.changes.FixInput; @@ -97,6 +95,8 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.GpgException; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.ReviewerByEmailSet; +import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.ReviewerStatusUpdate; import com.google.gerrit.server.StarredChangesUtil; import com.google.gerrit.server.WebLinks; @@ -104,25 +104,28 @@ import com.google.gerrit.server.api.accounts.AccountInfoComparator; import com.google.gerrit.server.api.accounts.GpgApiAdapter; import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.git.LabelNormalizer; import com.google.gerrit.server.git.MergeUtil; import com.google.gerrit.server.index.change.ChangeField; import com.google.gerrit.server.index.change.ChangeIndexCollection; +import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ReviewerStateInternal; import com.google.gerrit.server.patch.PatchListNotAvailableException; +import com.google.gerrit.server.permissions.LabelPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.SubmitRuleOptions; import com.google.gerrit.server.query.QueryResult; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeData.ChangedLines; +import com.google.gerrit.server.query.change.PluginDefinedAttributesFactory; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; @@ -186,9 +189,9 @@ } private final Provider<ReviewDb> db; - private final LabelNormalizer labelNormalizer; private final Provider<CurrentUser> userProvider; private final AnonymousUser anonymous; + private final PermissionBackend permissionBackend; private final GitRepositoryManager repoManager; private final ProjectCache projectCache; private final MergeUtil.Factory mergeUtilFactory; @@ -213,13 +216,14 @@ private boolean lazyLoad = true; private AccountLoader accountLoader; private FixInput fix; + private PluginDefinedAttributesFactory pluginDefinedAttributesFactory; - @AssistedInject + @Inject ChangeJson( Provider<ReviewDb> db, - LabelNormalizer ln, Provider<CurrentUser> user, AnonymousUser au, + PermissionBackend permissionBackend, GitRepositoryManager repoManager, ProjectCache projectCache, MergeUtil.Factory mergeUtilFactory, @@ -241,10 +245,10 @@ ApprovalsUtil approvalsUtil, @Assisted Iterable<ListChangesOption> options) { this.db = db; - this.labelNormalizer = ln; this.userProvider = user; this.anonymous = au; this.changeDataFactory = cdf; + this.permissionBackend = permissionBackend; this.repoManager = repoManager; this.userFactory = uf; this.projectCache = projectCache; @@ -276,6 +280,10 @@ return this; } + public void setPluginDefinedAttributesFactory(PluginDefinedAttributesFactory pluginsFactory) { + this.pluginDefinedAttributesFactory = pluginsFactory; + } + public ChangeInfo format(ChangeResource rsrc) throws OrmException { return format(changeDataFactory.create(db.get(), rsrc.getControl())); } @@ -316,6 +324,7 @@ | GpgException | OrmException | IOException + | PermissionBackendException | RuntimeException e) { if (!has(CHECK)) { Throwables.throwIfInstanceOf(e, OrmException.class); @@ -393,6 +402,7 @@ | GpgException | OrmException | IOException + | PermissionBackendException | RuntimeException e) { if (has(CHECK)) { i = checkOnly(cd); @@ -439,6 +449,9 @@ info.updated = c.getLastUpdatedOn(); info._number = c.getId().get(); info.problems = result.problems(); + info.isPrivate = c.isPrivate() ? true : null; + info.workInProgress = c.isWorkInProgress() ? true : null; + info.hasReviewStarted = c.hasReviewStarted(); finish(info); } else { info = new ChangeInfo(); @@ -449,7 +462,8 @@ } private ChangeInfo toChangeInfo(ChangeData cd, Optional<PatchSet.Id> limitToPsId) - throws PatchListNotAvailableException, GpgException, OrmException, IOException { + throws PatchListNotAvailableException, GpgException, OrmException, IOException, + PermissionBackendException { ChangeInfo out = new ChangeInfo(); CurrentUser user = userProvider.get(); ChangeControl ctl = cd.changeControl().forUser(user); @@ -465,6 +479,7 @@ } } + PermissionBackend.ForChange perm = permissionBackend.user(user).database(db).change(cd); Change in = cd.change(); out.project = in.getProject().get(); out.branch = in.getDest().getShortName(); @@ -491,6 +506,9 @@ out.insertions = changedLines.get().insertions; out.deletions = changedLines.get().deletions; } + out.isPrivate = in.isPrivate() ? true : null; + out.workInProgress = in.isWorkInProgress() ? true : null; + out.hasReviewStarted = in.hasReviewStarted(); out.subject = in.getSubject(); out.status = in.getStatus().asChangeStatus(); out.owner = accountLoader.get(in.getOwner()); @@ -502,6 +520,10 @@ if (user.isIdentifiedUser()) { Collection<String> stars = cd.stars(user.getAccountId()); out.starred = stars.contains(StarredChangesUtil.DEFAULT_LABEL) ? true : null; + out.muted = + stars.contains(StarredChangesUtil.MUTE_LABEL + "/" + cd.currentPatchSet().getPatchSetId()) + ? true + : null; if (!stars.isEmpty()) { out.stars = stars; } @@ -509,28 +531,31 @@ if (in.getStatus().isOpen() && has(REVIEWED) && user.isIdentifiedUser()) { Account.Id accountId = user.getAccountId(); - out.reviewed = cd.reviewedBy().contains(accountId) ? true : null; + if (out.muted != null) { + out.reviewed = true; + } else { + out.reviewed = cd.reviewedBy().contains(accountId) ? true : null; + } } - out.labels = labelsFor(ctl, cd, has(LABELS), has(DETAILED_LABELS)); + out.labels = labelsFor(perm, ctl, cd, has(LABELS), has(DETAILED_LABELS)); out.submitted = getSubmittedOn(cd); + out.plugins = + pluginDefinedAttributesFactory != null ? pluginDefinedAttributesFactory.create(cd) : null; if (out.labels != null && has(DETAILED_LABELS)) { // If limited to specific patch sets but not the current patch set, don't // list permitted labels, since users can't vote on those patch sets. - if (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId())) { + if (user.isIdentifiedUser() + && (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId()))) { out.permittedLabels = cd.change().getStatus() != Change.Status.ABANDONED - ? permittedLabels(ctl, cd) + ? permittedLabels(perm, cd) : ImmutableMap.of(); } - out.reviewers = new HashMap<>(); - for (Map.Entry<ReviewerStateInternal, Map<Account.Id, Timestamp>> e : - cd.reviewers().asTable().rowMap().entrySet()) { - out.reviewers.put(e.getKey().asReviewerState(), toAccountInfo(e.getValue().keySet())); - } - + out.reviewers = reviewerMap(cd.reviewers(), cd.reviewersByEmail(), false); + out.pendingReviewers = reviewerMap(cd.pendingReviewers(), cd.pendingReviewersByEmail(), true); out.removableReviewers = removableReviewers(ctl, out); } @@ -572,6 +597,22 @@ return out; } + 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); + } + } + return reviewerMap; + } + private Collection<ReviewerUpdateInfo> reviewerUpdates(ChangeData cd) throws OrmException { List<ReviewerStatusUpdate> reviewerUpdates = cd.reviewerUpdates(); List<ReviewerUpdateInfo> result = new ArrayList<>(reviewerUpdates.size()); @@ -595,7 +636,12 @@ } private Map<String, LabelInfo> labelsFor( - ChangeControl ctl, ChangeData cd, boolean standard, boolean detailed) throws OrmException { + PermissionBackend.ForChange perm, + ChangeControl ctl, + ChangeData cd, + boolean standard, + boolean detailed) + throws OrmException, PermissionBackendException { if (!standard && !detailed) { return null; } @@ -604,20 +650,24 @@ return null; } - LabelTypes labelTypes = ctl.getLabelTypes(); + LabelTypes labelTypes = cd.getLabelTypes(); Map<String, LabelWithStatus> withStatus = cd.change().getStatus().isOpen() - ? labelsForOpenChange(ctl, cd, labelTypes, standard, detailed) - : labelsForClosedChange(ctl, cd, labelTypes, standard, detailed); + ? labelsForOpenChange(perm, cd, labelTypes, standard, detailed) + : labelsForClosedChange(perm, cd, labelTypes, standard, detailed); return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label)); } private Map<String, LabelWithStatus> labelsForOpenChange( - ChangeControl ctl, ChangeData cd, LabelTypes labelTypes, boolean standard, boolean detailed) - throws OrmException { + PermissionBackend.ForChange perm, + ChangeData cd, + LabelTypes labelTypes, + boolean standard, + boolean detailed) + throws OrmException, PermissionBackendException { Map<String, LabelWithStatus> labels = initLabels(cd, labelTypes, standard); if (detailed) { - setAllApprovals(ctl, cd, labels); + setAllApprovals(perm, cd, labels); } for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) { LabelType type = labelTypes.byLabel(e.getKey()); @@ -704,8 +754,8 @@ } private void setAllApprovals( - ChangeControl baseCtrl, ChangeData cd, Map<String, LabelWithStatus> labels) - throws OrmException { + PermissionBackend.ForChange basePerm, ChangeData cd, Map<String, LabelWithStatus> labels) + throws OrmException, PermissionBackendException { Change.Status status = cd.change().getStatus(); checkState(status.isOpen(), "should not call setAllApprovals on %s change", status); @@ -719,17 +769,17 @@ } Table<Account.Id, String, PatchSetApproval> current = - HashBasedTable.create(allUsers.size(), baseCtrl.getLabelTypes().getLabelTypes().size()); + HashBasedTable.create(allUsers.size(), cd.getLabelTypes().getLabelTypes().size()); for (PatchSetApproval psa : cd.currentApprovals()) { current.put(psa.getAccountId(), psa.getLabel(), psa); } + LabelTypes labelTypes = cd.getLabelTypes(); for (Account.Id accountId : allUsers) { - IdentifiedUser user = userFactory.create(accountId); - ChangeControl ctl = baseCtrl.forUser(user); - Map<String, VotingRangeInfo> pvr = getPermittedVotingRanges(permittedLabels(ctl, cd)); + PermissionBackend.ForChange perm = basePerm.user(userFactory.create(accountId)); + Map<String, VotingRangeInfo> pvr = getPermittedVotingRanges(permittedLabels(perm, cd)); for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) { - LabelType lt = ctl.getLabelTypes().byLabel(e.getKey()); + LabelType lt = labelTypes.byLabel(e.getKey()); if (lt == null) { // Ignore submit record for undefined label; likely the submit rule // author didn't intend for the label to show up in the table. @@ -746,7 +796,7 @@ // This may be a dummy approval that was inserted when the reviewer // was added. Explicitly check whether the user can vote on this // label. - value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null; + value = perm.test(new LabelPermission(lt)) ? 0 : null; } tag = psa.getTag(); date = psa.getGranted(); @@ -757,7 +807,7 @@ // Either the user cannot vote on this label, or they were added as a // reviewer but have not responded yet. Explicitly check whether the // user can vote on this label. - value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null; + value = perm.test(new LabelPermission(lt)) ? 0 : null; } addApproval( e.getValue().label(), approvalInfo(accountId, value, permittedVotingRange, tag, date)); @@ -805,12 +855,12 @@ } private Map<String, LabelWithStatus> labelsForClosedChange( - ChangeControl baseCtrl, + PermissionBackend.ForChange basePerm, ChangeData cd, LabelTypes labelTypes, boolean standard, boolean detailed) - throws OrmException { + throws OrmException, PermissionBackendException { Set<Account.Id> allUsers = new HashSet<>(); if (detailed) { // Users expect to see all reviewers on closed changes, even if they @@ -878,8 +928,8 @@ Map<String, ApprovalInfo> byLabel = Maps.newHashMapWithExpectedSize(labels.size()); Map<String, VotingRangeInfo> pvr = Collections.emptyMap(); if (detailed) { - ChangeControl ctl = baseCtrl.forUser(userFactory.create(accountId)); - pvr = getPermittedVotingRanges(permittedLabels(ctl, cd)); + PermissionBackend.ForChange perm = basePerm.user(userFactory.create(accountId)); + pvr = getPermittedVotingRanges(permittedLabels(perm, cd)); for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) { ApprovalInfo ai = approvalInfo(accountId, 0, null, null, null); byLabel.put(entry.getKey(), ai); @@ -953,15 +1003,25 @@ } } - private Map<String, Collection<String>> permittedLabels(ChangeControl ctl, ChangeData cd) - throws OrmException { - if (ctl == null || !ctl.getUser().isIdentifiedUser()) { - return null; + private Map<String, Collection<String>> permittedLabels( + PermissionBackend.ForChange perm, ChangeData cd) + throws OrmException, PermissionBackendException { + boolean isMerged = cd.change().getStatus() == Change.Status.MERGED; + LabelTypes labelTypes = cd.getLabelTypes(); + Map<String, LabelType> toCheck = new HashMap<>(); + for (SubmitRecord rec : submitRecords(cd)) { + if (rec.labels != null) { + for (SubmitRecord.Label r : rec.labels) { + LabelType type = labelTypes.byLabel(r.label); + if (type != null && (!isMerged || type.allowPostSubmit())) { + toCheck.put(type.getName(), type); + } + } + } } Map<String, Short> labels = null; - boolean isMerged = ctl.getChange().getStatus() == Change.Status.MERGED; - LabelTypes labelTypes = ctl.getLabelTypes(); + Set<LabelPermission.WithValue> can = perm.testLabels(toCheck.values()); SetMultimap<String, String> permitted = LinkedHashMultimap.create(); for (SubmitRecord rec : submitRecords(cd)) { if (rec.labels == null) { @@ -972,12 +1032,12 @@ if (type == null || (isMerged && !type.allowPostSubmit())) { continue; } - PermissionRange range = ctl.getRange(Permission.forLabel(r.label)); + for (LabelValue v : type.getValues()) { - boolean ok = range.contains(v.getValue()); + boolean ok = can.contains(new LabelPermission.WithValue(type, v)); if (isMerged) { if (labels == null) { - labels = currentLabels(ctl); + labels = currentLabels(perm, cd); } short prev = labels.getOrDefault(type.getName(), (short) 0); ok &= v.getValue() >= prev; @@ -988,6 +1048,7 @@ } } } + List<String> toClear = Lists.newArrayListWithCapacity(permitted.keySet().size()); for (Map.Entry<String, Collection<String>> e : permitted.asMap().entrySet()) { if (isOnlyZero(e.getValue())) { @@ -1000,11 +1061,14 @@ return permitted.asMap(); } - private Map<String, Short> currentLabels(ChangeControl ctl) throws OrmException { + private Map<String, Short> currentLabels(PermissionBackend.ForChange perm, ChangeData cd) + throws OrmException { + IdentifiedUser user = perm.user().asIdentifiedUser(); + ChangeControl ctl = cd.changeControl().forUser(user); Map<String, Short> result = new HashMap<>(); for (PatchSetApproval psa : approvalsUtil.byPatchSetUser( - db.get(), ctl, ctl.getChange().currentPatchSetId(), ctl.getUser().getAccountId())) { + db.get(), ctl, cd.change().currentPatchSetId(), user.getAccountId(), null, null)) { result.put(psa.getLabel(), psa.getValue()); } return result; @@ -1029,6 +1093,10 @@ cmi.message = message.getMessage(); cmi.tag = message.getTag(); cmi._revisionNumber = patchNum != null ? patchNum.get() : null; + Account.Id realAuthor = message.getRealAuthor(); + if (realAuthor != null) { + cmi.realAuthor = accountLoader.get(realAuthor); + } result.add(cmi); } } @@ -1070,9 +1138,11 @@ Collection<AccountInfo> ccs = out.reviewers.get(ReviewerState.CC); if (ccs != null) { for (AccountInfo ai : ccs) { - Account.Id id = new Account.Id(ai._accountId); - if (ctl.canRemoveReviewer(id, 0)) { - removable.add(id); + if (ai._accountId != null) { + Account.Id id = new Account.Id(ai._accountId); + if (ctl.canRemoveReviewer(id, 0)) { + removable.add(id); + } } } } @@ -1086,6 +1156,14 @@ 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; } @@ -1097,6 +1175,14 @@ .collect(toList()); } + private Collection<AccountInfo> toAccountInfoByEmail(Collection<Address> addresses) { + return addresses + .stream() + .map(a -> new AccountInfo(a.getName(), a.getEmail())) + .sorted(AccountInfoComparator.ORDER_NULLS_FIRST) + .collect(toList()); + } + @Nullable private Repository openRepoIfNecessary(ChangeControl ctl) throws IOException { if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) { @@ -1105,15 +1191,21 @@ return null; } + @Nullable + private RevWalk newRevWalk(@Nullable Repository repo) { + return repo != null ? new RevWalk(repo) : null; + } + private Map<String, RevisionInfo> revisions( ChangeControl ctl, ChangeData cd, Map<PatchSet.Id, PatchSet> map, ChangeInfo changeInfo) throws PatchListNotAvailableException, GpgException, OrmException, IOException { Map<String, RevisionInfo> res = new LinkedHashMap<>(); - try (Repository repo = openRepoIfNecessary(ctl)) { + try (Repository repo = openRepoIfNecessary(ctl); + RevWalk rw = newRevWalk(repo)) { for (PatchSet in : map.values()) { if ((has(ALL_REVISIONS) || in.getId().equals(ctl.getChange().currentPatchSetId())) && ctl.isPatchVisible(in, db.get())) { - res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in, repo, false, changeInfo)); + res.put(in.getRevision().get(), toRevisionInfo(ctl, cd, in, repo, rw, false, changeInfo)); } } return res; @@ -1150,9 +1242,10 @@ public RevisionInfo getRevisionInfo(ChangeControl ctl, PatchSet in) throws PatchListNotAvailableException, GpgException, OrmException, IOException { accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS)); - try (Repository repo = openRepoIfNecessary(ctl)) { + try (Repository repo = openRepoIfNecessary(ctl); + RevWalk rw = newRevWalk(repo)) { RevisionInfo rev = - toRevisionInfo(ctl, changeDataFactory.create(db.get(), ctl), in, repo, true, null); + toRevisionInfo(ctl, changeDataFactory.create(db.get(), ctl), in, repo, rw, true, null); accountLoader.fill(); return rev; } @@ -1163,6 +1256,7 @@ ChangeData cd, PatchSet in, @Nullable Repository repo, + @Nullable RevWalk rw, boolean fillCommit, @Nullable ChangeInfo changeInfo) throws PatchListNotAvailableException, GpgException, OrmException, IOException { @@ -1175,32 +1269,32 @@ out.uploader = accountLoader.get(in.getUploader()); out.draft = in.isDraft() ? true : null; out.fetch = makeFetchMap(ctl, in); - out.kind = changeKindCache.getChangeKind(repo, cd, in); + out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in); out.description = in.getDescription(); boolean setCommit = has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT)); boolean addFooters = out.isCurrent && has(COMMIT_FOOTERS); if (setCommit || addFooters) { + checkState(rw != null); + checkState(repo != null); Project.NameKey project = c.getProject(); - try (RevWalk rw = new RevWalk(repo)) { - String rev = in.getRevision().get(); - RevCommit commit = rw.parseCommit(ObjectId.fromString(rev)); - rw.parseBody(commit); - if (setCommit) { - out.commit = toCommit(ctl, rw, commit, has(WEB_LINKS), fillCommit); + String rev = in.getRevision().get(); + RevCommit commit = rw.parseCommit(ObjectId.fromString(rev)); + rw.parseBody(commit); + if (setCommit) { + out.commit = toCommit(ctl, rw, commit, has(WEB_LINKS), fillCommit); + } + if (addFooters) { + Ref ref = repo.exactRef(ctl.getChange().getDest().get()); + RevCommit mergeTip = null; + if (ref != null) { + mergeTip = rw.parseCommit(ref.getObjectId()); + rw.parseBody(mergeTip); } - if (addFooters) { - Ref ref = repo.exactRef(ctl.getChange().getDest().get()); - RevCommit mergeTip = null; - if (ref != null) { - mergeTip = rw.parseCommit(ref.getObjectId()); - rw.parseBody(mergeTip); - } - out.commitWithFooters = - mergeUtilFactory - .create(projectCache.get(project)) - .createCommitMessageOnSubmit(commit, mergeTip, ctl, in.getId()); - } + out.commitWithFooters = + mergeUtilFactory + .create(projectCache.get(project)) + .createCommitMessageOnSubmit(commit, mergeTip, ctl, in.getId()); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java index aa47827..6baeefc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCache.java
@@ -21,8 +21,9 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.query.change.ChangeData; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; /** * Cache of {@link ChangeKind} per commit. @@ -32,9 +33,14 @@ */ public interface ChangeKindCache { ChangeKind getChangeKind( - Project.NameKey project, @Nullable Repository repo, ObjectId prior, ObjectId next); + Project.NameKey project, + @Nullable RevWalk rw, + @Nullable Config repoConfig, + ObjectId prior, + ObjectId next); ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch); - ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd, PatchSet patch); + ChangeKind getChangeKind( + @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java index 030ddd2..7a6c209 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.change; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull; import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull; @@ -94,10 +95,14 @@ @Override public ChangeKind getChangeKind( - Project.NameKey project, @Nullable Repository repo, ObjectId prior, ObjectId next) { + Project.NameKey project, + @Nullable RevWalk rw, + @Nullable Config repoConfig, + ObjectId prior, + ObjectId next) { try { Key key = new Key(prior, next, useRecursiveMerge); - return new Loader(key, repoManager, project, repo).call(); + return new Loader(key, repoManager, project, rw, repoConfig).call(); } catch (IOException e) { log.warn( "Cannot check trivial rebase of new patch set " + next.name() + " in " + project, e); @@ -111,8 +116,9 @@ } @Override - public ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd, PatchSet patch) { - return getChangeKindInternal(this, repo, cd, patch); + public ChangeKind getChangeKind( + @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch) { + return getChangeKindInternal(this, rw, repoConfig, cd, patch); } } @@ -182,36 +188,47 @@ private final Key key; private final GitRepositoryManager repoManager; private final Project.NameKey projectName; - private final Repository alreadyOpenRepo; + private final RevWalk alreadyOpenRw; + private final Config repoConfig; private Loader( Key key, GitRepositoryManager repoManager, Project.NameKey projectName, - @Nullable Repository alreadyOpenRepo) { + @Nullable RevWalk rw, + @Nullable Config repoConfig) { + checkArgument( + (rw == null && repoConfig == null) || (rw != null && repoConfig != null), + "must either provide both revwalk/config, or neither; got %s/%s", + rw, + repoConfig); this.key = key; this.repoManager = repoManager; this.projectName = projectName; - this.alreadyOpenRepo = alreadyOpenRepo; + this.alreadyOpenRw = rw; + this.repoConfig = repoConfig; } + @SuppressWarnings("resource") // Resources are manually managed. @Override public ChangeKind call() throws IOException { if (Objects.equals(key.prior, key.next)) { return ChangeKind.NO_CODE_CHANGE; } - Repository repo = alreadyOpenRepo; - boolean close = false; - if (repo == null) { + RevWalk rw = alreadyOpenRw; + Config config = repoConfig; + Repository repo = null; + if (alreadyOpenRw == null) { repo = repoManager.openRepository(projectName); - close = true; + rw = new RevWalk(repo); + config = repo.getConfig(); } - try (RevWalk walk = new RevWalk(repo)) { - RevCommit prior = walk.parseCommit(key.prior); - walk.parseBody(prior); - RevCommit next = walk.parseCommit(key.next); - walk.parseBody(next); + try { + RevCommit prior = rw.parseCommit(key.prior); + rw.parseBody(prior); + RevCommit next = rw.parseCommit(key.next); + rw.parseBody(next); if (!next.getFullMessage().equals(prior.getFullMessage())) { if (isSameDeltaAndTree(prior, next)) { @@ -233,8 +250,8 @@ // A trivial rebase can be detected by looking for the next commit // having the same tree as would exist when the prior commit is // cherry-picked onto the next commit's new first parent. - try (ObjectInserter ins = new InMemoryInserter(repo)) { - ThreeWayMerger merger = MergeUtil.newThreeWayMerger(repo, ins, key.strategyName); + try (ObjectInserter ins = new InMemoryInserter(rw.getObjectReader())) { + ThreeWayMerger merger = MergeUtil.newThreeWayMerger(ins, config, key.strategyName); merger.setBase(prior.getParent(0)); if (merger.merge(next.getParent(0), prior) && merger.getResultTreeId().equals(next.getTree())) { @@ -249,7 +266,8 @@ } return ChangeKind.REWORK; } finally { - if (close) { + if (repo != null) { + rw.close(); repo.close(); } } @@ -327,10 +345,14 @@ @Override public ChangeKind getChangeKind( - Project.NameKey project, @Nullable Repository repo, ObjectId prior, ObjectId next) { + Project.NameKey project, + @Nullable RevWalk rw, + @Nullable Config repoConfig, + ObjectId prior, + ObjectId next) { try { Key key = new Key(prior, next, useRecursiveMerge); - return cache.get(key, new Loader(key, repoManager, project, repo)); + return cache.get(key, new Loader(key, repoManager, project, rw, repoConfig)); } catch (ExecutionException e) { log.warn("Cannot check trivial rebase of new patch set " + next.name() + " in " + project, e); return ChangeKind.REWORK; @@ -343,12 +365,17 @@ } @Override - public ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd, PatchSet patch) { - return getChangeKindInternal(this, repo, cd, patch); + public ChangeKind getChangeKind( + @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch) { + return getChangeKindInternal(this, rw, repoConfig, cd, patch); } private static ChangeKind getChangeKindInternal( - ChangeKindCache cache, @Nullable Repository repo, ChangeData change, PatchSet patch) { + ChangeKindCache cache, + @Nullable RevWalk rw, + @Nullable Config repoConfig, + ChangeData change, + PatchSet patch) { ChangeKind kind = ChangeKind.REWORK; // Trivial case: if we're on the first patch, we don't need to use // the repository. @@ -372,7 +399,8 @@ kind = cache.getChangeKind( change.project(), - repo, + rw, + repoConfig, ObjectId.fromString(priorPs.getRevision().get()), ObjectId.fromString(patch.getRevision().get())); } @@ -401,8 +429,11 @@ // Trivial case: if we're on the first patch, we don't need to open // the repository. if (patch.getId().get() > 1) { - try (Repository repo = repoManager.openRepository(change.getProject())) { - kind = getChangeKindInternal(cache, repo, changeDataFactory.create(db, change), patch); + try (Repository repo = repoManager.openRepository(change.getProject()); + RevWalk rw = new RevWalk(repo)) { + kind = + getChangeKindInternal( + cache, rw, repo.getConfig(), changeDataFactory.create(db, change), patch); } catch (IOException e) { // Do nothing; assume we have a complex change log.warn(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java index 92b4150..41b6855 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java
@@ -23,6 +23,10 @@ } public String revertChangeDefaultMessage; + + public String reviewerCantSeeChange; + public String reviewerInactive; + public String reviewerInvalid; public String reviewerNotFoundUser; public String reviewerNotFoundUserOrGroup;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java index b06f05f..f27c53b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -22,6 +22,7 @@ import com.google.gerrit.extensions.restapi.RestResource; import com.google.gerrit.extensions.restapi.RestResource.HasETag; import com.google.gerrit.extensions.restapi.RestView; +import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; @@ -29,12 +30,13 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.StarredChangesUtil; import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ProjectState; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.TypeLiteral; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import org.eclipse.jgit.lib.ObjectId; public class ChangeResource implements RestResource, HasETag { @@ -53,15 +55,24 @@ ChangeResource create(ChangeControl ctl); } + private final PermissionBackend permissionBackend; private final StarredChangesUtil starredChangesUtil; private final ChangeControl control; - @AssistedInject - ChangeResource(StarredChangesUtil starredChangesUtil, @Assisted ChangeControl control) { + @Inject + ChangeResource( + PermissionBackend permissionBackend, + StarredChangesUtil starredChangesUtil, + @Assisted ChangeControl control) { + this.permissionBackend = permissionBackend; this.starredChangesUtil = starredChangesUtil; this.control = control; } + public PermissionBackend.ForChange permissions() { + return permissionBackend.user(getControl().getUser()).change(getNotes()); + } + public ChangeControl getControl() { return control; } @@ -74,6 +85,13 @@ return getControl().getId(); } + /** @return true if {@link #getUser()} is the change's owner. */ + public boolean isUserOwner() { + CurrentUser user = getControl().getUser(); + Account.Id owner = getChange().getOwner(); + return user.isIdentifiedUser() && user.asIdentifiedUser().getAccountId().equals(owner); + } + public Change getChange() { return getControl().getChange(); } @@ -119,7 +137,7 @@ @Override public String getETag() { CurrentUser user = control.getUser(); - Hasher h = Hashing.md5().newHasher(); + Hasher h = Hashing.murmur3_128().newHasher(); if (user.isIdentifiedUser()) { h.putString(starredChangesUtil.getObjectId(user.getAccountId(), getId()).name(), UTF_8); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java index 3b67930..a36a1d3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Check.java
@@ -17,21 +17,28 @@ import com.google.gerrit.extensions.api.changes.FixInput; import com.google.gerrit.extensions.client.ListChangesOption; import com.google.gerrit.extensions.common.ChangeInfo; -import com.google.gerrit.extensions.restapi.AuthException; 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.extensions.restapi.RestReadView; -import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; +import com.google.inject.Provider; public class Check implements RestReadView<ChangeResource>, RestModifyView<ChangeResource, FixInput> { + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; private final ChangeJson.Factory jsonFactory; @Inject - Check(ChangeJson.Factory json) { + Check(PermissionBackend permissionBackend, Provider<CurrentUser> user, ChangeJson.Factory json) { + this.permissionBackend = permissionBackend; + this.user = user; this.jsonFactory = json; } @@ -42,12 +49,9 @@ @Override public Response<ChangeInfo> apply(ChangeResource rsrc, FixInput input) - throws RestApiException, OrmException { - ChangeControl ctl = rsrc.getControl(); - if (!ctl.isOwner() - && !ctl.getProjectControl().isOwner() - && !ctl.getUser().getCapabilities().canMaintainServer()) { - throw new AuthException("Cannot fix change"); + throws RestApiException, OrmException, PermissionBackendException { + if (!rsrc.isUserOwner() && !rsrc.getControl().getProjectControl().isOwner()) { + permissionBackend.user(user).check(GlobalPermission.MAINTAIN_SERVER); } return Response.withMustRevalidate(newChangeJson().fix(input).format(rsrc)); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java index e5a4d0f..993148e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -14,90 +14,85 @@ package com.google.gerrit.server.change; -import com.google.gerrit.common.data.Capable; import com.google.gerrit.extensions.api.changes.CherryPickInput; import com.google.gerrit.extensions.common.ChangeInfo; -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.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.webui.UiAction; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.RefNames; -import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.git.IntegrationException; -import com.google.gerrit.server.project.ChangeControl; +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.InvalidChangeOperationException; import com.google.gerrit.server.project.NoSuchChangeException; -import com.google.gerrit.server.project.ProjectControl; -import com.google.gerrit.server.project.RefControl; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; 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; @Singleton public class CherryPick - implements RestModifyView<RevisionResource, CherryPickInput>, UiAction<RevisionResource> { - private final Provider<ReviewDb> dbProvider; + extends RetryingRestModifyView<RevisionResource, CherryPickInput, ChangeInfo> + implements UiAction<RevisionResource> { + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; private final CherryPickChange cherryPickChange; private final ChangeJson.Factory json; @Inject CherryPick( - Provider<ReviewDb> dbProvider, CherryPickChange cherryPickChange, ChangeJson.Factory json) { - this.dbProvider = dbProvider; + PermissionBackend permissionBackend, + Provider<CurrentUser> user, + RetryHelper retryHelper, + CherryPickChange cherryPickChange, + ChangeJson.Factory json) { + super(retryHelper); + this.permissionBackend = permissionBackend; + this.user = user; this.cherryPickChange = cherryPickChange; this.json = json; } @Override - public ChangeInfo apply(RevisionResource revision, CherryPickInput input) - throws OrmException, IOException, UpdateException, RestApiException { - final ChangeControl control = revision.getControl(); - int parent = input.parent == null ? 1 : input.parent; - + public ChangeInfo applyImpl( + BatchUpdate.Factory updateFactory, RevisionResource rsrc, CherryPickInput input) + throws OrmException, IOException, UpdateException, RestApiException, + PermissionBackendException, ConfigInvalidException { + input.parent = input.parent == null ? 1 : input.parent; if (input.message == null || input.message.trim().isEmpty()) { throw new BadRequestException("message must be non-empty"); } else if (input.destination == null || input.destination.trim().isEmpty()) { throw new BadRequestException("destination must be non-empty"); } - @SuppressWarnings("resource") - ReviewDb db = dbProvider.get(); - if (!control.isVisible(db)) { - throw new AuthException("Cherry pick not permitted"); - } - - ProjectControl projectControl = control.getProjectControl(); - Capable capable = projectControl.canPushToAtLeastOneRef(); - if (capable != Capable.OK) { - throw new AuthException(capable.getMessage()); - } - String refName = RefNames.fullName(input.destination); - RefControl refControl = projectControl.controlForRef(refName); - if (!refControl.canUpload()) { - throw new AuthException( - "Not allowed to cherry pick " - + revision.getChange().getId().toString() - + " to " - + input.destination); - } + CreateChange.checkValidCLA(rsrc.getControl().getProjectControl()); + permissionBackend + .user(user) + .project(rsrc.getChange().getProject()) + .ref(refName) + .check(RefPermission.CREATE_CHANGE); try { Change.Id cherryPickedChangeId = cherryPickChange.cherryPick( - revision.getChange(), - revision.getPatchSet(), - input.message, - refName, - refControl, - parent); - return json.noOptions().format(revision.getProject(), cherryPickedChangeId); + updateFactory, + rsrc.getChange(), + rsrc.getPatchSet(), + input, + rsrc.getControl().getProjectControl().controlForRef(refName)); + return json.noOptions().format(rsrc.getProject(), cherryPickedChangeId); } catch (InvalidChangeOperationException e) { throw new BadRequestException(e.getMessage()); } catch (IntegrationException | NoSuchChangeException e) { @@ -106,10 +101,15 @@ } @Override - public UiAction.Description getDescription(RevisionResource resource) { + public UiAction.Description getDescription(RevisionResource rsrc) { return new UiAction.Description() .setLabel("Cherry Pick") .setTitle("Cherry pick change to a different branch") - .setVisible(resource.getControl().getProjectControl().canUpload() && resource.isCurrent()); + .setVisible( + rsrc.isCurrent() + && permissionBackend + .user(user) + .project(rsrc.getProject()) + .testOrFalse(ProjectPermission.CREATE_CHANGE)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java index b2455f1..fbb692c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -16,12 +16,17 @@ import com.google.common.base.Strings; import com.google.gerrit.common.FooterConstants; +import com.google.gerrit.common.Nullable; import com.google.gerrit.common.TimeUtil; -import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.CherryPickInput; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MergeConflictException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Change.Status; import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.Project; @@ -39,10 +44,8 @@ import com.google.gerrit.server.git.IntegrationException; import com.google.gerrit.server.git.MergeIdenticalTreeException; import com.google.gerrit.server.git.MergeUtil; -import com.google.gerrit.server.git.validators.CommitValidators; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.InvalidChangeOperationException; -import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.project.RefControl; import com.google.gerrit.server.query.change.ChangeData; @@ -59,21 +62,22 @@ import java.sql.Timestamp; import java.util.List; import java.util.TimeZone; -import org.eclipse.jgit.errors.IncorrectObjectTypeException; -import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.InvalidObjectIdException; 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.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.util.ChangeIdUtil; @Singleton public class CherryPickChange { - private final Provider<ReviewDb> db; + private final Provider<ReviewDb> dbProvider; private final Sequences seq; private final Provider<InternalChangeQuery> queryProvider; private final GitRepositoryManager gitManager; @@ -84,11 +88,11 @@ private final MergeUtil.Factory mergeUtilFactory; private final ChangeMessagesUtil changeMessagesUtil; private final PatchSetUtil psUtil; - private final BatchUpdate.Factory batchUpdateFactory; + private final NotifyUtil notifyUtil; @Inject CherryPickChange( - Provider<ReviewDb> db, + Provider<ReviewDb> dbProvider, Sequences seq, Provider<InternalChangeQuery> queryProvider, @GerritPersonIdent PersonIdent myIdent, @@ -99,8 +103,8 @@ MergeUtil.Factory mergeUtilFactory, ChangeMessagesUtil changeMessagesUtil, PatchSetUtil psUtil, - BatchUpdate.Factory batchUpdateFactory) { - this.db = db; + NotifyUtil notifyUtil) { + this.dbProvider = dbProvider; this.seq = seq; this.queryProvider = queryProvider; this.gitManager = gitManager; @@ -111,27 +115,42 @@ this.mergeUtilFactory = mergeUtilFactory; this.changeMessagesUtil = changeMessagesUtil; this.psUtil = psUtil; - this.batchUpdateFactory = batchUpdateFactory; + this.notifyUtil = notifyUtil; } public Change.Id cherryPick( + BatchUpdate.Factory batchUpdateFactory, Change change, PatchSet patch, - final String message, - final String ref, - final RefControl refControl, - int parent) - throws NoSuchChangeException, OrmException, MissingObjectException, - IncorrectObjectTypeException, IOException, InvalidChangeOperationException, - IntegrationException, UpdateException, RestApiException { + CherryPickInput input, + RefControl refControl) + throws OrmException, IOException, InvalidChangeOperationException, IntegrationException, + UpdateException, RestApiException, ConfigInvalidException { + return cherryPick( + batchUpdateFactory, + change.getId(), + patch.getId(), + change.getDest(), + change.getTopic(), + change.getProject(), + ObjectId.fromString(patch.getRevision().get()), + input, + refControl); + } - if (Strings.isNullOrEmpty(ref)) { - throw new InvalidChangeOperationException( - "Cherry Pick: Destination branch cannot be null or empty"); - } + public Change.Id cherryPick( + BatchUpdate.Factory batchUpdateFactory, + @Nullable Change.Id sourceChangeId, + @Nullable PatchSet.Id sourcePatchId, + @Nullable Branch.NameKey sourceBranch, + @Nullable String sourceChangeTopic, + Project.NameKey project, + ObjectId sourceCommit, + CherryPickInput input, + RefControl destRefControl) + throws OrmException, IOException, InvalidChangeOperationException, IntegrationException, + UpdateException, RestApiException, ConfigInvalidException { - Project.NameKey project = change.getProject(); - String destinationBranch = RefNames.shortName(ref); IdentifiedUser identifiedUser = user.get(); try (Repository git = gitManager.openRepository(project); // This inserter and revwalk *must* be passed to any BatchUpdates @@ -140,23 +159,23 @@ ObjectInserter oi = git.newObjectInserter(); ObjectReader reader = oi.newReader(); CodeReviewRevWalk revWalk = CodeReviewCommit.newRevWalk(reader)) { - Ref destRef = git.getRefDatabase().exactRef(ref); + String destRefName = destRefControl.getRefName(); + Ref destRef = git.getRefDatabase().exactRef(destRefName); if (destRef == null) { throw new InvalidChangeOperationException( - String.format("Branch %s does not exist.", destinationBranch)); + String.format("Branch %s does not exist.", destRefName)); } - CodeReviewCommit mergeTip = revWalk.parseCommit(destRef.getObjectId()); + RevCommit baseCommit = getBaseCommit(destRef, project.get(), revWalk, input.base); - CodeReviewCommit commitToCherryPick = - revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get())); + CodeReviewCommit commitToCherryPick = revWalk.parseCommit(sourceCommit); - if (parent <= 0 || parent > commitToCherryPick.getParentCount()) { + if (input.parent <= 0 || input.parent > commitToCherryPick.getParentCount()) { throw new InvalidChangeOperationException( String.format( "Cherry Pick: Parent %s does not exist. Please specify a parent in" + " range [1, %s].", - parent, commitToCherryPick.getParentCount())); + input.parent, commitToCherryPick.getParentCount())); } Timestamp now = TimeUtil.nowTs(); @@ -165,27 +184,27 @@ final ObjectId computedChangeId = ChangeIdUtil.computeChangeId( commitToCherryPick.getTree(), - mergeTip, + baseCommit, commitToCherryPick.getAuthorIdent(), committerIdent, - message); - String commitMessage = ChangeIdUtil.insertId(message, computedChangeId).trim() + '\n'; + input.message); + String commitMessage = ChangeIdUtil.insertId(input.message, computedChangeId).trim() + '\n'; CodeReviewCommit cherryPickCommit; try { - ProjectState projectState = refControl.getProjectControl().getProjectState(); + ProjectState projectState = destRefControl.getProjectControl().getProjectState(); cherryPickCommit = mergeUtilFactory .create(projectState) .createCherryPickFromCommit( - git, oi, - mergeTip, + git.getConfig(), + baseCommit, commitToCherryPick, committerIdent, commitMessage, revWalk, - parent - 1, + input.parent - 1, false); Change.Key changeKey; @@ -197,7 +216,7 @@ changeKey = new Change.Key("I" + computedChangeId.name()); } - Branch.NameKey newDest = new Branch.NameKey(change.getProject(), destRef.getName()); + Branch.NameKey newDest = new Branch.NameKey(project, destRef.getName()); List<ChangeData> destChanges = queryProvider.get().setLimit(2).byBranchKey(newDest, changeKey); if (destChanges.size() > 1) { @@ -208,31 +227,41 @@ + "Cannot create a new patch set."); } try (BatchUpdate bu = - batchUpdateFactory.create( - db.get(), change.getDest().getParentKey(), identifiedUser, now)) { + batchUpdateFactory.create(dbProvider.get(), project, identifiedUser, now)) { bu.setRepository(git, revWalk, oi); Change.Id result; if (destChanges.size() == 1) { // The change key exists on the destination branch. The cherry pick // will be added as a new patch set. ChangeControl destCtl = - refControl.getProjectControl().controlFor(destChanges.get(0).notes()); - result = insertPatchSet(bu, git, destCtl, cherryPickCommit); + destRefControl.getProjectControl().controlFor(destChanges.get(0).notes()); + result = insertPatchSet(bu, git, destCtl, cherryPickCommit, input); } else { // Change key not found on destination branch. We can create a new // change. String newTopic = null; - if (!Strings.isNullOrEmpty(change.getTopic())) { - newTopic = change.getTopic() + "-" + newDest.getShortName(); + if (!Strings.isNullOrEmpty(sourceChangeTopic)) { + newTopic = sourceChangeTopic + "-" + newDest.getShortName(); } result = createNewChange( - bu, cherryPickCommit, refControl.getRefName(), newTopic, change.getDest()); + bu, + cherryPickCommit, + destRefControl.getRefName(), + newTopic, + sourceBranch, + sourceCommit, + input); - bu.addOp( - change.getId(), - new AddMessageToSourceChangeOp( - changeMessagesUtil, patch.getId(), destinationBranch, cherryPickCommit)); + if (sourceChangeId != null && sourcePatchId != null) { + bu.addOp( + sourceChangeId, + new AddMessageToSourceChangeOp( + changeMessagesUtil, + sourcePatchId, + RefNames.shortName(destRefName), + cherryPickCommit)); + } } bu.execute(); return result; @@ -240,26 +269,70 @@ } catch (MergeIdenticalTreeException | MergeConflictException e) { throw new IntegrationException("Cherry pick failed: " + e.getMessage()); } - } catch (RepositoryNotFoundException e) { - throw new NoSuchChangeException(change.getId(), e); } } + private RevCommit getBaseCommit(Ref destRef, String project, RevWalk revWalk, String base) + throws RestApiException, IOException, OrmException { + RevCommit destRefTip = revWalk.parseCommit(destRef.getObjectId()); + // The tip commit of the destination ref is the default base for the newly created change. + if (Strings.isNullOrEmpty(base)) { + return destRefTip; + } + + ObjectId baseObjectId; + try { + baseObjectId = ObjectId.fromString(base); + } catch (InvalidObjectIdException e) { + throw new BadRequestException(String.format("Base %s doesn't represent a valid SHA-1", base)); + } + + RevCommit baseCommit = revWalk.parseCommit(baseObjectId); + InternalChangeQuery changeQuery = queryProvider.get(); + changeQuery.enforceVisibility(true); + List<ChangeData> changeDatas = changeQuery.byBranchCommit(project, destRef.getName(), base); + + if (changeDatas.isEmpty()) { + if (revWalk.isMergedInto(baseCommit, destRefTip)) { + // The base commit is a merged commit with no change associated. + return baseCommit; + } + throw new UnprocessableEntityException( + String.format("Commit %s does not exist on branch %s", base, destRef.getName())); + } else if (changeDatas.size() != 1) { + throw new ResourceConflictException("Multiple changes found for commit " + base); + } + + Change change = changeDatas.get(0).change(); + Change.Status status = change.getStatus(); + if (status == Status.NEW || status == Status.MERGED) { + // The base commit is a valid change revision. + return baseCommit; + } + + throw new ResourceConflictException( + String.format( + "Change %s with commit %s is %s", change.getChangeId(), base, status.asChangeStatus())); + } + private Change.Id insertPatchSet( - BatchUpdate bu, Repository git, ChangeControl destCtl, CodeReviewCommit cherryPickCommit) - throws IOException, OrmException { + BatchUpdate bu, + Repository git, + ChangeControl destCtl, + CodeReviewCommit cherryPickCommit, + CherryPickInput input) + throws IOException, OrmException, BadRequestException, ConfigInvalidException { Change destChange = destCtl.getChange(); PatchSet.Id psId = ChangeUtil.nextPatchSetId(git, destChange.currentPatchSetId()); - PatchSetInserter inserter = patchSetInserterFactory.create(destCtl, psId, cherryPickCommit); - PatchSet.Id newPatchSetId = inserter.getPatchSetId(); - PatchSet current = psUtil.current(db.get(), destCtl.getNotes()); + PatchSet current = psUtil.current(dbProvider.get(), destCtl.getNotes()); - bu.addOp( - destChange.getId(), - inserter - .setMessage("Uploaded patch set " + newPatchSetId.get() + ".") - .setDraft(current.isDraft()) - .setNotify(NotifyHandling.NONE)); + PatchSetInserter inserter = patchSetInserterFactory.create(destCtl, psId, cherryPickCommit); + inserter + .setMessage("Uploaded patch set " + inserter.getPatchSetId().get() + ".") + .setDraft(current.isDraft()) + .setNotify(input.notify) + .setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails)); + bu.addOp(destChange.getId(), inserter); return destChange.getId(); } @@ -268,16 +341,16 @@ CodeReviewCommit cherryPickCommit, String refName, String topic, - Branch.NameKey sourceBranch) - throws OrmException { + Branch.NameKey sourceBranch, + ObjectId sourceCommit, + CherryPickInput input) + throws OrmException, IOException, BadRequestException, ConfigInvalidException { Change.Id changeId = new Change.Id(seq.nextChangeId()); ChangeInserter ins = - changeInserterFactory - .create(changeId, cherryPickCommit, refName) - .setValidatePolicy(CommitValidators.Policy.GERRIT) - .setTopic(topic); - - ins.setMessage(messageForDestinationChange(ins.getPatchSetId(), sourceBranch)); + changeInserterFactory.create(changeId, cherryPickCommit, refName).setTopic(topic); + ins.setMessage(messageForDestinationChange(ins.getPatchSetId(), sourceBranch, sourceCommit)) + .setNotify(input.notify) + .setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails)); bu.insertChange(ins); return changeId; } @@ -319,12 +392,16 @@ } } - private String messageForDestinationChange(PatchSet.Id patchSetId, Branch.NameKey sourceBranch) { - return new StringBuilder("Patch Set ") - .append(patchSetId.get()) - .append(": Cherry Picked from branch ") - .append(sourceBranch.getShortName()) - .append(".") - .toString(); + private String messageForDestinationChange( + PatchSet.Id patchSetId, Branch.NameKey sourceBranch, ObjectId sourceCommit) { + StringBuilder stringBuilder = new StringBuilder("Patch Set ").append(patchSetId.get()); + + if (sourceBranch != null) { + stringBuilder.append(": Cherry Picked from branch ").append(sourceBranch.getShortName()); + } else { + stringBuilder.append(": Cherry Picked from commit ").append(sourceCommit.getName()); + } + + return stringBuilder.append(".").toString(); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java new file mode 100644 index 0000000..ac70e4a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickCommit.java
@@ -0,0 +1,110 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.base.Strings; +import com.google.gerrit.extensions.api.changes.CherryPickInput; +import com.google.gerrit.extensions.common.ChangeInfo; +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.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.git.IntegrationException; +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.CommitResource; +import com.google.gerrit.server.project.InvalidChangeOperationException; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; +import com.google.gerrit.server.update.UpdateException; +import com.google.gwtorm.server.OrmException; +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.revwalk.RevCommit; + +@Singleton +public class CherryPickCommit + extends RetryingRestModifyView<CommitResource, CherryPickInput, ChangeInfo> { + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; + private final CherryPickChange cherryPickChange; + private final ChangeJson.Factory json; + + @Inject + CherryPickCommit( + RetryHelper retryHelper, + Provider<CurrentUser> user, + CherryPickChange cherryPickChange, + ChangeJson.Factory json, + PermissionBackend permissionBackend) { + super(retryHelper); + this.permissionBackend = permissionBackend; + this.user = user; + this.cherryPickChange = cherryPickChange; + this.json = json; + } + + @Override + public ChangeInfo applyImpl( + BatchUpdate.Factory updateFactory, CommitResource rsrc, CherryPickInput input) + throws OrmException, IOException, UpdateException, RestApiException, + PermissionBackendException, ConfigInvalidException { + RevCommit commit = rsrc.getCommit(); + String message = Strings.nullToEmpty(input.message).trim(); + input.message = message.isEmpty() ? commit.getFullMessage() : message; + String destination = Strings.nullToEmpty(input.destination).trim(); + input.parent = input.parent == null ? 1 : input.parent; + Project.NameKey projectName = rsrc.getProject().getProject().getNameKey(); + + if (destination.isEmpty()) { + throw new BadRequestException("destination must be non-empty"); + } + + String refName = RefNames.fullName(destination); + CreateChange.checkValidCLA(rsrc.getProject()); + permissionBackend + .user(user) + .project(projectName) + .ref(refName) + .check(RefPermission.CREATE_CHANGE); + + try { + Change.Id cherryPickedChangeId = + cherryPickChange.cherryPick( + updateFactory, + null, + null, + null, + null, + projectName, + commit, + input, + rsrc.getProject().controlForRef(refName)); + return json.noOptions().format(projectName, cherryPickedChangeId); + } catch (InvalidChangeOperationException e) { + throw new BadRequestException(e.getMessage()); + } catch (IntegrationException e) { + throw new ResourceConflictException(e.getMessage()); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java index 40c8515..f7fc576 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
@@ -48,4 +48,8 @@ Account.Id getAuthorId() { return comment.author.getId(); } + + RevisionResource getRevisionResource() { + return rev; + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java index 41845e3..9fcb13d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -22,6 +22,7 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.MultimapBuilder; import com.google.common.collect.SetMultimap; @@ -43,8 +44,8 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.account.Accounts; import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.git.validators.CommitValidators; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.PatchSetState; import com.google.gerrit.server.patch.PatchSetInfoFactory; @@ -54,6 +55,7 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.RepoContext; +import com.google.gerrit.server.update.RetryHelper; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -66,6 +68,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.errors.RepositoryNotFoundException; @@ -77,7 +80,6 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.transport.ReceiveCommand; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -103,9 +105,9 @@ public abstract List<ProblemInfo> problems(); } - private final BatchUpdate.Factory updateFactory; private final ChangeControl.GenericFactory changeControlFactory; private final ChangeNotes.Factory notesFactory; + private final Accounts accounts; private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore; private final GitRepositoryManager repoManager; private final PatchSetInfoFactory patchSetInfoFactory; @@ -114,11 +116,14 @@ private final Provider<CurrentUser> user; private final Provider<PersonIdent> serverIdent; private final Provider<ReviewDb> db; + private final RetryHelper retryHelper; + private BatchUpdate.Factory updateFactory; private FixInput fix; private ChangeControl ctl; private Repository repo; private RevWalk rw; + private ObjectInserter oi; private RevCommit tip; private SetMultimap<ObjectId, PatchSet> patchSetsBySha; @@ -130,16 +135,18 @@ @Inject ConsistencyChecker( @GerritPersonIdent Provider<PersonIdent> serverIdent, - BatchUpdate.Factory updateFactory, ChangeControl.GenericFactory changeControlFactory, ChangeNotes.Factory notesFactory, + Accounts accounts, DynamicItem<AccountPatchReviewStore> accountPatchReviewStore, GitRepositoryManager repoManager, PatchSetInfoFactory patchSetInfoFactory, PatchSetInserter.Factory patchSetInserterFactory, PatchSetUtil psUtil, Provider<CurrentUser> user, - Provider<ReviewDb> db) { + Provider<ReviewDb> db, + RetryHelper retryHelper) { + this.accounts = accounts; this.accountPatchReviewStore = accountPatchReviewStore; this.changeControlFactory = changeControlFactory; this.db = db; @@ -148,13 +155,14 @@ this.patchSetInserterFactory = patchSetInserterFactory; this.psUtil = psUtil; this.repoManager = repoManager; + this.retryHelper = retryHelper; this.serverIdent = serverIdent; - this.updateFactory = updateFactory; this.user = user; reset(); } private void reset() { + updateFactory = null; ctl = null; repo = null; rw = null; @@ -168,21 +176,38 @@ public Result check(ChangeControl cc, @Nullable FixInput f) { checkNotNull(cc); try { - reset(); - ctl = cc; - fix = f; - checkImpl(); - return result(); - } finally { - if (rw != null) { - rw.close(); - } - if (repo != null) { - repo.close(); - } + return retryHelper.execute( + buf -> { + try { + reset(); + this.updateFactory = buf; + ctl = cc; + fix = f; + checkImpl(); + return result(); + } finally { + if (rw != null) { + rw.getObjectReader().close(); + rw.close(); + oi.close(); + } + if (repo != null) { + repo.close(); + } + } + }); + } catch (RestApiException e) { + return logAndReturnOneProblem(e, cc, "Error checking change: " + e.getMessage()); + } catch (UpdateException e) { + return logAndReturnOneProblem(e, cc, "Error checking change"); } } + private Result logAndReturnOneProblem(Exception e, ChangeControl cc, String problem) { + log.warn("Error checking change " + cc.getId(), e); + return Result.create(cc, ImmutableList.of(problem(problem))); + } + private void checkImpl() { checkOwner(); checkCurrentPatchSetEntity(); @@ -199,10 +224,10 @@ private void checkOwner() { try { - if (db.get().accounts().get(change().getOwner()) == null) { + if (accounts.get(db.get(), change().getOwner()) == null) { problem("Missing change owner: " + change().getOwner()); } - } catch (OrmException e) { + } catch (OrmException | IOException | ConfigInvalidException e) { error("Failed to look up owner", e); } } @@ -223,7 +248,8 @@ Project.NameKey project = change().getDest().getParentKey(); try { repo = repoManager.openRepository(project); - rw = new RevWalk(repo); + oi = repo.newObjectInserter(); + rw = new RevWalk(oi.newReader()); return true; } catch (RepositoryNotFoundException e) { return error("Destination repository not found: " + project, e); @@ -450,7 +476,7 @@ } private void insertMergedPatchSet( - final RevCommit commit, final @Nullable PatchSet.Id psIdToDelete, boolean reuseOldPsId) { + final RevCommit commit, @Nullable PatchSet.Id psIdToDelete, boolean reuseOldPsId) { ProblemInfo notFound = problem("No patch set found for merged commit " + commit.name()); if (!user.get().isIdentifiedUser()) { notFound.status = Status.FIX_FAILED; @@ -490,8 +516,7 @@ ? psIdToDelete : ChangeUtil.nextPatchSetId(repo, change().currentPatchSetId()); PatchSetInserter inserter = patchSetInserterFactory.create(ctl, psId, commit); - try (BatchUpdate bu = newBatchUpdate(); - ObjectInserter oi = repo.newObjectInserter()) { + try (BatchUpdate bu = newBatchUpdate()) { bu.setRepository(repo, rw, oi); if (psIdToDelete != null) { @@ -502,8 +527,7 @@ new BatchUpdateOp() { @Override public void updateRepo(RepoContext ctx) throws IOException { - ctx.addRefUpdate( - new ReceiveCommand(commit, ObjectId.zeroId(), psIdToDelete.toRefName())); + ctx.addRefUpdate(commit, ObjectId.zeroId(), psIdToDelete.toRefName()); } }); if (!reuseOldPsId) { @@ -516,7 +540,7 @@ bu.addOp( ctl.getId(), inserter - .setValidatePolicy(CommitValidators.Policy.NONE) + .setValidate(false) .setFireRevisionCreated(false) .setNotify(NotifyHandling.NONE) .setAllowClosed(true) @@ -555,8 +579,7 @@ } private void fixMerged(ProblemInfo p) { - try (BatchUpdate bu = newBatchUpdate(); - ObjectInserter oi = repo.newObjectInserter()) { + try (BatchUpdate bu = newBatchUpdate()) { bu.setRepository(repo, rw, oi); bu.addOp(ctl.getId(), new FixMergedOp(p)); bu.execute(); @@ -607,8 +630,7 @@ } private void deletePatchSets(List<DeletePatchSetFromDbOp> ops) { - try (BatchUpdate bu = newBatchUpdate(); - ObjectInserter oi = repo.newObjectInserter()) { + try (BatchUpdate bu = newBatchUpdate()) { bu.setRepository(repo, rw, oi); for (DeletePatchSetFromDbOp op : ops) { checkArgument(op.psId.getParentKey().equals(ctl.getId()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java index 656c9d1..bbe04f5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -33,7 +33,6 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException; 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.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.reviewdb.client.Change; @@ -53,14 +52,17 @@ import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MergeUtil; -import com.google.gerrit.server.git.validators.CommitValidators; +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.ChangeControl; import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectResource; import com.google.gerrit.server.project.ProjectsCollection; -import com.google.gerrit.server.project.RefControl; import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -72,6 +74,7 @@ import java.util.Collections; import java.util.List; import java.util.TimeZone; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; @@ -86,20 +89,20 @@ import org.eclipse.jgit.util.ChangeIdUtil; @Singleton -public class CreateChange implements RestModifyView<TopLevelResource, ChangeInput> { - +public class CreateChange + extends RetryingRestModifyView<TopLevelResource, ChangeInput, Response<ChangeInfo>> { private final String anonymousCowardName; private final Provider<ReviewDb> db; private final GitRepositoryManager gitManager; private final AccountCache accountCache; private final Sequences seq; private final TimeZone serverTimeZone; + private final PermissionBackend permissionBackend; private final Provider<CurrentUser> user; private final ProjectsCollection projectsCollection; private final ChangeInserter.Factory changeInserterFactory; private final ChangeJson.Factory jsonFactory; private final ChangeFinder changeFinder; - private final BatchUpdate.Factory updateFactory; private final PatchSetUtil psUtil; private final boolean allowDrafts; private final MergeUtil.Factory mergeUtilFactory; @@ -114,28 +117,30 @@ AccountCache accountCache, Sequences seq, @GerritPersonIdent PersonIdent myIdent, + PermissionBackend permissionBackend, Provider<CurrentUser> user, ProjectsCollection projectsCollection, ChangeInserter.Factory changeInserterFactory, ChangeJson.Factory json, ChangeFinder changeFinder, - BatchUpdate.Factory updateFactory, + RetryHelper retryHelper, PatchSetUtil psUtil, @GerritServerConfig Config config, MergeUtil.Factory mergeUtilFactory, NotifyUtil notifyUtil) { + super(retryHelper); this.anonymousCowardName = anonymousCowardName; this.db = db; this.gitManager = gitManager; this.accountCache = accountCache; this.seq = seq; this.serverTimeZone = myIdent.getTimeZone(); + this.permissionBackend = permissionBackend; this.user = user; this.projectsCollection = projectsCollection; this.changeInserterFactory = changeInserterFactory; this.jsonFactory = json; this.changeFinder = changeFinder; - this.updateFactory = updateFactory; this.psUtil = psUtil; this.allowDrafts = config.getBoolean("change", "allowDrafts", true); this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY); @@ -144,9 +149,10 @@ } @Override - public Response<ChangeInfo> apply(TopLevelResource parent, ChangeInput input) + protected Response<ChangeInfo> applyImpl( + BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input) throws OrmException, IOException, InvalidChangeOperationException, RestApiException, - UpdateException { + UpdateException, PermissionBackendException, ConfigInvalidException { if (Strings.isNullOrEmpty(input.project)) { throw new BadRequestException("project must be non-empty"); } @@ -163,26 +169,18 @@ if (input.status != ChangeStatus.NEW && input.status != ChangeStatus.DRAFT) { throw new BadRequestException("unsupported change status"); } - if (!allowDrafts && input.status == ChangeStatus.DRAFT) { throw new MethodNotAllowedException("draft workflow is disabled"); } } - String refName = RefNames.fullName(input.branch); ProjectResource rsrc = projectsCollection.parse(input.project); - - Capable r = rsrc.getControl().canPushToAtLeastOneRef(); - if (r != Capable.OK) { - throw new AuthException(r.getMessage()); - } - - RefControl refControl = rsrc.getControl().controlForRef(refName); - if (!refControl.canUpload() || !refControl.canRead()) { - throw new AuthException("cannot upload review"); - } + checkValidCLA(rsrc.getControl()); Project.NameKey project = rsrc.getNameKey(); + String refName = RefNames.fullName(input.branch); + permissionBackend.user(user).project(project).ref(refName).check(RefPermission.CREATE_CHANGE); + try (Repository git = gitManager.openRepository(project); ObjectInserter oi = git.newObjectInserter(); ObjectReader reader = oi.newReader(); @@ -192,11 +190,11 @@ if (input.baseChange != null) { List<ChangeControl> ctls = changeFinder.find(input.baseChange, rsrc.getControl().getUser()); if (ctls.size() != 1) { - throw new InvalidChangeOperationException("Base change not found: " + input.baseChange); + throw new UnprocessableEntityException("Base change not found: " + input.baseChange); } ChangeControl ctl = Iterables.getOnlyElement(ctls); if (!ctl.isVisible(db.get())) { - throw new InvalidChangeOperationException("Base change not found: " + input.baseChange); + throw new UnprocessableEntityException("Base change not found: " + input.baseChange); } PatchSet ps = psUtil.current(db.get(), ctl.getNotes()); parentCommit = ObjectId.fromString(ps.getRevision().get()); @@ -252,10 +250,7 @@ } Change.Id changeId = new Change.Id(seq.nextChangeId()); - ChangeInserter ins = - changeInserterFactory - .create(changeId, c, refName) - .setValidatePolicy(CommitValidators.Policy.GERRIT); + ChangeInserter ins = changeInserterFactory.create(changeId, c, refName); ins.setMessage(String.format("Uploaded patch set %s.", ins.getPatchSetId().get())); String topic = input.topic; if (topic != null) { @@ -263,6 +258,8 @@ } ins.setTopic(topic); ins.setDraft(input.status == ChangeStatus.DRAFT); + ins.setPrivate(input.isPrivate != null && input.isPrivate); + ins.setWorkInProgress(input.workInProgress != null && input.workInProgress); ins.setGroups(groups); ins.setNotify(input.notify); ins.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails)); @@ -324,7 +321,14 @@ Strings.emptyToNull(merge.strategy), mergeUtil.mergeStrategyName()); return MergeUtil.createMergeCommit( - repo, oi, mergeTip, sourceCommit, mergeStrategy, authorIdent, commitMessage, rw); + oi, + repo.getConfig(), + mergeTip, + sourceCommit, + mergeStrategy, + authorIdent, + commitMessage, + rw); } private static ObjectId insert(ObjectInserter inserter, CommitBuilder commit) @@ -337,4 +341,11 @@ private static ObjectId emptyTreeId(ObjectInserter inserter) throws IOException { return inserter.insert(new TreeFormatter()); } + + static void checkValidCLA(ProjectControl ctl) throws AuthException { + Capable capable = ctl.canPushToAtLeastOneRef(); + if (capable != Capable.OK) { + throw new AuthException(capable.getMessage()); + } + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java index 5032e57..002c8b7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraftComment.java
@@ -24,7 +24,6 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.reviewdb.client.Comment; @@ -37,6 +36,8 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -45,9 +46,9 @@ import java.util.Collections; @Singleton -public class CreateDraftComment implements RestModifyView<RevisionResource, DraftInput> { +public class CreateDraftComment + extends RetryingRestModifyView<RevisionResource, DraftInput, Response<CommentInfo>> { private final Provider<ReviewDb> db; - private final BatchUpdate.Factory updateFactory; private final Provider<CommentJson> commentJson; private final CommentsUtil commentsUtil; private final PatchSetUtil psUtil; @@ -56,13 +57,13 @@ @Inject CreateDraftComment( Provider<ReviewDb> db, - BatchUpdate.Factory updateFactory, + RetryHelper retryHelper, Provider<CommentJson> commentJson, CommentsUtil commentsUtil, PatchSetUtil psUtil, PatchListCache patchListCache) { + super(retryHelper); this.db = db; - this.updateFactory = updateFactory; this.commentJson = commentJson; this.commentsUtil = commentsUtil; this.psUtil = psUtil; @@ -70,7 +71,8 @@ } @Override - public Response<CommentInfo> apply(RevisionResource rsrc, DraftInput in) + protected Response<CommentInfo> applyImpl( + BatchUpdate.Factory updateFactory, RevisionResource rsrc, DraftInput in) throws RestApiException, UpdateException, OrmException { if (Strings.isNullOrEmpty(in.path)) { throw new BadRequestException("path must be non-empty"); @@ -122,7 +124,7 @@ commentsUtil.putComments( ctx.getDb(), ctx.getUpdate(psId), Status.DRAFT, Collections.singleton(comment)); - ctx.bumpLastUpdatedOn(false); + ctx.dontBumpLastUpdatedOn(); return true; } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java index 5bd651d..b53bdd9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateMergePatchSet.java
@@ -22,13 +22,11 @@ import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.MergeInput; import com.google.gerrit.extensions.common.MergePatchSetInput; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MergeConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; @@ -42,10 +40,14 @@ import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MergeIdenticalTreeException; import com.google.gerrit.server.git.MergeUtil; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -65,7 +67,8 @@ import org.eclipse.jgit.util.ChangeIdUtil; @Singleton -public class CreateMergePatchSet implements RestModifyView<ChangeResource, MergePatchSetInput> { +public class CreateMergePatchSet + extends RetryingRestModifyView<ChangeResource, MergePatchSetInput, Response<ChangeInfo>> { private final Provider<ReviewDb> db; private final GitRepositoryManager gitManager; @@ -74,7 +77,6 @@ private final ChangeJson.Factory jsonFactory; private final PatchSetUtil psUtil; private final MergeUtil.Factory mergeUtilFactory; - private final BatchUpdate.Factory batchUpdateFactory; private final PatchSetInserter.Factory patchSetInserterFactory; @Inject @@ -86,8 +88,9 @@ ChangeJson.Factory json, PatchSetUtil psUtil, MergeUtil.Factory mergeUtilFactory, - BatchUpdate.Factory batchUpdateFactory, + RetryHelper retryHelper, PatchSetInserter.Factory patchSetInserterFactory) { + super(retryHelper); this.db = db; this.gitManager = gitManager; this.serverTimeZone = myIdent.getTimeZone(); @@ -95,32 +98,23 @@ this.jsonFactory = json; this.psUtil = psUtil; this.mergeUtilFactory = mergeUtilFactory; - this.batchUpdateFactory = batchUpdateFactory; this.patchSetInserterFactory = patchSetInserterFactory; } @Override - public Response<ChangeInfo> apply(ChangeResource req, MergePatchSetInput in) + protected Response<ChangeInfo> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, MergePatchSetInput in) throws OrmException, IOException, InvalidChangeOperationException, RestApiException, - UpdateException { - if (in.merge == null) { - throw new BadRequestException("merge field is required"); - } + UpdateException, PermissionBackendException { + rsrc.permissions().database(db).check(ChangePermission.ADD_PATCH_SET); MergeInput merge = in.merge; - if (Strings.isNullOrEmpty(merge.source)) { + if (merge == null || Strings.isNullOrEmpty(merge.source)) { throw new BadRequestException("merge.source must be non-empty"); } - ChangeControl ctl = req.getControl(); - if (!ctl.isVisible(db.get())) { - throw new InvalidChangeOperationException("Base change not found: " + req.getId()); - } + ChangeControl ctl = rsrc.getControl(); PatchSet ps = psUtil.current(db.get(), ctl.getNotes()); - if (!ctl.canAddPatchSet(db.get())) { - throw new AuthException("cannot add patch set"); - } - ProjectControl projectControl = ctl.getProjectControl(); Change change = ctl.getChange(); Project.NameKey project = change.getProject(); @@ -137,11 +131,9 @@ } RevCommit currentPsCommit = rw.parseCommit(ObjectId.fromString(ps.getRevision().get())); - Timestamp now = TimeUtil.nowTs(); IdentifiedUser me = user.get().asIdentifiedUser(); PersonIdent author = me.newCommitterIdent(now, serverTimeZone); - RevCommit newCommit = createMergeCommit( in, @@ -157,14 +149,15 @@ PatchSet.Id nextPsId = ChangeUtil.nextPatchSetId(ps.getId()); PatchSetInserter psInserter = patchSetInserterFactory.create(ctl, nextPsId, newCommit); - try (BatchUpdate bu = batchUpdateFactory.create(db.get(), project, me, now)) { + try (BatchUpdate bu = updateFactory.create(db.get(), project, me, now)) { bu.setRepository(git, rw, oi); bu.addOp( ctl.getId(), psInserter .setMessage("Uploaded patch set " + nextPsId.get() + ".") .setDraft(ps.isDraft()) - .setNotify(NotifyHandling.NONE)); + .setNotify(NotifyHandling.NONE) + .setCheckAddPatchSetPermission(false)); bu.execute(); } @@ -216,6 +209,6 @@ mergeUtilFactory.create(projectControl.getProjectState()).mergeStrategyName()); return MergeUtil.createMergeCommit( - git, oi, mergeTip, sourceCommit, mergeStrategy, author, commitMsg, rw); + oi, git.getConfig(), mergeTip, sourceCommit, mergeStrategy, author, commitMsg, rw); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java index b8556d6..d3feb31 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteAssignee.java
@@ -16,10 +16,8 @@ import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.common.AccountInfo; -import com.google.gerrit.extensions.restapi.AuthException; 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.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; @@ -30,10 +28,14 @@ import com.google.gerrit.server.change.DeleteAssignee.Input; import com.google.gerrit.server.extensions.events.AssigneeChanged; import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -41,10 +43,10 @@ import com.google.inject.Singleton; @Singleton -public class DeleteAssignee implements RestModifyView<ChangeResource, Input> { +public class DeleteAssignee + extends RetryingRestModifyView<ChangeResource, Input, Response<AccountInfo>> { public static class Input {} - private final BatchUpdate.Factory batchUpdateFactory; private final ChangeMessagesUtil cmUtil; private final Provider<ReviewDb> db; private final AssigneeChanged assigneeChanged; @@ -53,13 +55,13 @@ @Inject DeleteAssignee( - BatchUpdate.Factory batchUpdateFactory, + RetryHelper retryHelper, ChangeMessagesUtil cmUtil, Provider<ReviewDb> db, AssigneeChanged assigneeChanged, IdentifiedUser.GenericFactory userFactory, AccountLoader.Factory accountLoaderFactory) { - this.batchUpdateFactory = batchUpdateFactory; + super(retryHelper); this.cmUtil = cmUtil; this.db = db; this.assigneeChanged = assigneeChanged; @@ -68,10 +70,13 @@ } @Override - public Response<AccountInfo> apply(ChangeResource rsrc, Input input) - throws RestApiException, UpdateException, OrmException { + protected Response<AccountInfo> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input) + throws RestApiException, UpdateException, OrmException, PermissionBackendException { + rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE); + try (BatchUpdate bu = - batchUpdateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { + updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { Op op = new Op(); bu.addOp(rsrc.getChange().getId(), op); bu.execute(); @@ -88,9 +93,6 @@ @Override public boolean updateChange(ChangeContext ctx) throws RestApiException, OrmException { - if (!ctx.getControl().canEditAssignee()) { - throw new AuthException("Delete Assignee not permitted"); - } change = ctx.getChange(); ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); Account.Id currentAssigneeId = change.getAssignee();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java index ad823d4..b9b05e8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChange.java
@@ -15,50 +15,74 @@ package com.google.gerrit.server.change; import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.MethodNotAllowedException; 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.extensions.webui.UiAction; import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.Change.Status; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.change.DeleteChange.Input; import com.google.gerrit.server.config.GerritServerConfig; -import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.Order; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import org.eclipse.jgit.lib.Config; @Singleton -public class DeleteChange - implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> { +public class DeleteChange extends RetryingRestModifyView<ChangeResource, Input, Response<?>> + implements UiAction<ChangeResource> { public static class Input {} private final Provider<ReviewDb> db; - private final BatchUpdate.Factory updateFactory; private final Provider<DeleteChangeOp> opProvider; + private final Provider<CurrentUser> user; + private final PermissionBackend permissionBackend; private final boolean allowDrafts; @Inject public DeleteChange( Provider<ReviewDb> db, - BatchUpdate.Factory updateFactory, + RetryHelper retryHelper, Provider<DeleteChangeOp> opProvider, + Provider<CurrentUser> user, + PermissionBackend permissionBackend, @GerritServerConfig Config cfg) { + super(retryHelper); this.db = db; - this.updateFactory = updateFactory; this.opProvider = opProvider; + this.user = user; + this.permissionBackend = permissionBackend; this.allowDrafts = DeleteChangeOp.allowDrafts(cfg); } @Override - public Response<?> apply(ChangeResource rsrc, Input input) - throws RestApiException, UpdateException { + protected Response<?> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input) + throws RestApiException, UpdateException, PermissionBackendException { + if (rsrc.getChange().getStatus() == Change.Status.MERGED) { + throw new MethodNotAllowedException("delete not permitted"); + } else if (!allowDrafts && rsrc.getChange().getStatus() == Change.Status.DRAFT) { + // If drafts are disabled, only an administrator can delete a draft. + try { + permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER); + } catch (AuthException e) { + throw new MethodNotAllowedException("Draft workflow is disabled"); + } + } else { + rsrc.permissions().database(db).check(ChangePermission.DELETE); + } + try (BatchUpdate bu = updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { Change.Id id = rsrc.getChange().getId(); @@ -71,21 +95,33 @@ @Override public UiAction.Description getDescription(ChangeResource rsrc) { - try { - Change.Status status = rsrc.getChange().getStatus(); - ChangeControl changeControl = rsrc.getControl(); - boolean visible = - isActionAllowed(changeControl, status) && changeControl.canDelete(db.get(), status); - return new UiAction.Description() - .setLabel("Delete") - .setTitle("Delete change " + rsrc.getId()) - .setVisible(visible); - } catch (OrmException e) { - throw new IllegalStateException(e); - } + Change.Status status = rsrc.getChange().getStatus(); + PermissionBackend.ForChange perm = rsrc.permissions().database(db); + return new UiAction.Description() + .setLabel("Delete") + .setTitle("Delete change " + rsrc.getId()) + .setVisible(couldDeleteWhenIn(status) && perm.testOrFalse(ChangePermission.DELETE)); } - private boolean isActionAllowed(ChangeControl changeControl, Status status) { - return status != Status.DRAFT || allowDrafts || changeControl.isAdmin(); + private boolean couldDeleteWhenIn(Change.Status status) { + switch (status) { + case NEW: + case ABANDONED: + // New or abandoned changes can be deleted with the right permissions. + return true; + + case MERGED: + // Merged changes should never be deleted. + return false; + + case DRAFT: + if (allowDrafts) { + // Drafts can only be deleted if the server has drafts enabled. + return true; + } + // If drafts are disabled, only administrators may delete. + return permissionBackend.user(user).testOrFalse(GlobalPermission.ADMINISTRATE_SERVER); + } + return false; } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java index 9d819b1..8ab1d2a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteChangeOp.java
@@ -17,7 +17,6 @@ import static com.google.common.base.Preconditions.checkState; import com.google.gerrit.extensions.registration.DynamicItem; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestApiException; @@ -27,7 +26,6 @@ import com.google.gerrit.reviewdb.server.ReviewDbUtil; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.StarredChangesUtil; -import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.BatchUpdateReviewDb; @@ -38,14 +36,11 @@ import com.google.inject.Inject; import java.io.IOException; import java.util.Collection; -import java.util.Collections; +import java.util.Map; +import java.util.Optional; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; -import org.eclipse.jgit.transport.ReceiveCommand; class DeleteChangeOp implements BatchUpdateOp { static boolean allowDrafts(Config cfg) { @@ -65,7 +60,6 @@ private final PatchSetUtil psUtil; private final StarredChangesUtil starredChangesUtil; private final DynamicItem<AccountPatchReviewStore> accountPatchReviewStore; - private final boolean allowDrafts; private Change.Id id; @@ -73,12 +67,10 @@ DeleteChangeOp( PatchSetUtil psUtil, StarredChangesUtil starredChangesUtil, - DynamicItem<AccountPatchReviewStore> accountPatchReviewStore, - @GerritServerConfig Config cfg) { + DynamicItem<AccountPatchReviewStore> accountPatchReviewStore) { this.psUtil = psUtil; this.starredChangesUtil = starredChangesUtil; this.accountPatchReviewStore = accountPatchReviewStore; - this.allowDrafts = allowDrafts(cfg); } @Override @@ -102,8 +94,7 @@ } private void ensureDeletable(ChangeContext ctx, Change.Id id, Collection<PatchSet> patchSets) - throws ResourceConflictException, MethodNotAllowedException, OrmException, AuthException, - IOException { + throws ResourceConflictException, MethodNotAllowedException, IOException { Change.Status status = ctx.getChange().getStatus(); if (status == Change.Status.MERGED) { throw new MethodNotAllowedException("Deleting merged change " + id + " is not allowed"); @@ -117,14 +108,7 @@ } } - if (!ctx.getControl().canDelete(ctx.getDb(), status)) { - throw new AuthException("Deleting change " + id + " is not permitted"); - } - if (status == Change.Status.DRAFT) { - if (!allowDrafts && !ctx.getControl().isAdmin()) { - throw new MethodNotAllowedException("Draft workflow is disabled"); - } for (PatchSet ps : patchSets) { if (!ps.isDraft()) { throw new ResourceConflictException( @@ -139,17 +123,14 @@ } private boolean isPatchSetMerged(ChangeContext ctx, PatchSet patchSet) throws IOException { - Repository repository = ctx.getRepository(); - Ref destinationRef = repository.exactRef(ctx.getChange().getDest().get()); - if (destinationRef == null) { + Optional<ObjectId> destId = ctx.getRepoView().getRef(ctx.getChange().getDest().get()); + if (!destId.isPresent()) { return false; } RevWalk revWalk = ctx.getRevWalk(); ObjectId objectId = ObjectId.fromString(patchSet.getRevision().get()); - RevCommit revCommit = revWalk.parseCommit(objectId); - return IncludedInResolver.includedInOne( - repository, revWalk, revCommit, Collections.singletonList(destinationRef)); + return revWalk.isMergedInto(revWalk.parseCommit(objectId), revWalk.parseCommit(destId.get())); } private void deleteChangeElementsFromDb(ChangeContext ctx, Change.Id id) throws OrmException { @@ -177,8 +158,8 @@ public void updateRepo(RepoContext ctx) throws IOException { String prefix = new PatchSet.Id(id, 1).toRefName(); prefix = prefix.substring(0, prefix.length() - 1); - for (Ref ref : ctx.getRepository().getRefDatabase().getRefs(prefix).values()) { - ctx.addRefUpdate(new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName())); + for (Map.Entry<String, ObjectId> e : ctx.getRepoView().getRefs(prefix).entrySet()) { + ctx.addRefUpdate(e.getValue(), ObjectId.zeroId(), prefix + e.getKey()); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteComment.java new file mode 100644 index 0000000..17665b0 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteComment.java
@@ -0,0 +1,136 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.base.Strings; +import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.api.changes.DeleteCommentInput; +import com.google.gerrit.extensions.common.CommentInfo; +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.reviewdb.client.Comment; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CommentsUtil; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; +import com.google.gerrit.server.update.UpdateException; +import com.google.gwtorm.server.OrmException; +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; + +@Singleton +public class DeleteComment + extends RetryingRestModifyView<CommentResource, DeleteCommentInput, CommentInfo> { + + private final Provider<CurrentUser> userProvider; + private final Provider<ReviewDb> dbProvider; + private final PermissionBackend permissionBackend; + private final CommentsUtil commentsUtil; + private final Provider<CommentJson> commentJson; + private final ChangeNotes.Factory notesFactory; + + @Inject + public DeleteComment( + Provider<CurrentUser> userProvider, + Provider<ReviewDb> dbProvider, + PermissionBackend permissionBackend, + RetryHelper retryHelper, + CommentsUtil commentsUtil, + Provider<CommentJson> commentJson, + ChangeNotes.Factory notesFactory) { + super(retryHelper); + this.userProvider = userProvider; + this.dbProvider = dbProvider; + this.permissionBackend = permissionBackend; + this.commentsUtil = commentsUtil; + this.commentJson = commentJson; + this.notesFactory = notesFactory; + } + + @Override + public CommentInfo applyImpl( + BatchUpdate.Factory batchUpdateFactory, CommentResource rsrc, DeleteCommentInput input) + throws RestApiException, IOException, ConfigInvalidException, OrmException, + PermissionBackendException, UpdateException { + CurrentUser user = userProvider.get(); + permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER); + + String newMessage = getCommentNewMessage(user.asIdentifiedUser().getName(), input.reason); + DeleteCommentOp deleteCommentOp = new DeleteCommentOp(rsrc, newMessage); + try (BatchUpdate batchUpdate = + batchUpdateFactory.create( + dbProvider.get(), rsrc.getRevisionResource().getProject(), user, TimeUtil.nowTs())) { + batchUpdate.addOp(rsrc.getRevisionResource().getChange().getId(), deleteCommentOp).execute(); + } + + ChangeNotes updatedNotes = + notesFactory.createChecked(rsrc.getRevisionResource().getChange().getId()); + List<Comment> changeComments = commentsUtil.publishedByChange(dbProvider.get(), updatedNotes); + Optional<Comment> updatedComment = + changeComments.stream().filter(c -> c.key.equals(rsrc.getComment().key)).findFirst(); + if (!updatedComment.isPresent()) { + // This should not happen as this endpoint should not remove the whole comment. + throw new ResourceNotFoundException("comment not found: " + rsrc.getComment().key); + } + + return commentJson.get().newCommentFormatter().format(updatedComment.get()); + } + + private static String getCommentNewMessage(String name, String reason) { + StringBuilder stringBuilder = new StringBuilder("Comment removed by: ").append(name); + if (!Strings.isNullOrEmpty(reason)) { + stringBuilder.append("; Reason: ").append(reason); + } + return stringBuilder.toString(); + } + + private class DeleteCommentOp implements BatchUpdateOp { + private final CommentResource rsrc; + private final String newMessage; + + DeleteCommentOp(CommentResource rsrc, String newMessage) { + this.rsrc = rsrc; + this.newMessage = newMessage; + } + + @Override + public boolean updateChange(ChangeContext ctx) + throws ResourceConflictException, OrmException, ResourceNotFoundException { + PatchSet.Id psId = ctx.getChange().currentPatchSetId(); + commentsUtil.deleteCommentByRewritingHistory( + ctx.getDb(), + ctx.getUpdate(psId), + rsrc.getComment().key, + rsrc.getPatchSet().getId(), + newMessage); + return true; + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java index 7787260..021fd45 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftComment.java
@@ -21,7 +21,6 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.reviewdb.client.Comment; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.server.ReviewDb; @@ -32,6 +31,8 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -41,13 +42,13 @@ import java.util.Optional; @Singleton -public class DeleteDraftComment implements RestModifyView<DraftCommentResource, Input> { +public class DeleteDraftComment + extends RetryingRestModifyView<DraftCommentResource, Input, Response<CommentInfo>> { static class Input {} private final Provider<ReviewDb> db; private final CommentsUtil commentsUtil; private final PatchSetUtil psUtil; - private final BatchUpdate.Factory updateFactory; private final PatchListCache patchListCache; @Inject @@ -55,17 +56,18 @@ Provider<ReviewDb> db, CommentsUtil commentsUtil, PatchSetUtil psUtil, - BatchUpdate.Factory updateFactory, + RetryHelper retryHelper, PatchListCache patchListCache) { + super(retryHelper); this.db = db; this.commentsUtil = commentsUtil; this.psUtil = psUtil; - this.updateFactory = updateFactory; this.patchListCache = patchListCache; } @Override - public Response<CommentInfo> apply(DraftCommentResource rsrc, Input input) + protected Response<CommentInfo> applyImpl( + BatchUpdate.Factory updateFactory, DraftCommentResource rsrc, Input input) throws RestApiException, UpdateException { try (BatchUpdate bu = updateFactory.create( @@ -101,7 +103,7 @@ Comment c = maybeComment.get(); setCommentRevId(c, patchListCache, ctx.getChange(), ps); commentsUtil.deleteComments(ctx.getDb(), ctx.getUpdate(psId), Collections.singleton(c)); - ctx.bumpLastUpdatedOn(false); + ctx.dontBumpLastUpdatedOn(); return true; } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java index d452489..615c32b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraftPatchSet.java
@@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.collect.Iterables; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.restapi.AuthException; @@ -23,7 +24,6 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException; 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.extensions.webui.UiAction; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; @@ -34,12 +34,16 @@ import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Order; import com.google.gerrit.server.update.RepoContext; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -50,15 +54,14 @@ import java.util.Map; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.transport.ReceiveCommand; @Singleton public class DeleteDraftPatchSet - implements RestModifyView<RevisionResource, Input>, UiAction<RevisionResource> { + extends RetryingRestModifyView<RevisionResource, Input, Response<?>> + implements UiAction<RevisionResource> { public static class Input {} private final Provider<ReviewDb> db; - private final BatchUpdate.Factory updateFactory; private final PatchSetInfoFactory patchSetInfoFactory; private final PatchSetUtil psUtil; private final Provider<DeleteChangeOp> deleteChangeOpProvider; @@ -68,14 +71,14 @@ @Inject public DeleteDraftPatchSet( Provider<ReviewDb> db, - BatchUpdate.Factory updateFactory, + RetryHelper retryHelper, PatchSetInfoFactory patchSetInfoFactory, PatchSetUtil psUtil, Provider<DeleteChangeOp> deleteChangeOpProvider, DynamicItem<AccountPatchReviewStore> accountPatchReviewStore, @GerritServerConfig Config cfg) { + super(retryHelper); this.db = db; - this.updateFactory = updateFactory; this.patchSetInfoFactory = patchSetInfoFactory; this.psUtil = psUtil; this.deleteChangeOpProvider = deleteChangeOpProvider; @@ -84,8 +87,14 @@ } @Override - public Response<?> apply(RevisionResource rsrc, Input input) - throws RestApiException, UpdateException { + protected Response<?> applyImpl( + BatchUpdate.Factory updateFactory, RevisionResource rsrc, Input input) + throws RestApiException, UpdateException, OrmException, PermissionBackendException { + if (isDeletingOnlyPatchSet(rsrc)) { + // A change cannot have zero patch sets; the change is deleted instead. + rsrc.permissions().database(db).check(ChangePermission.DELETE); + } + try (BatchUpdate bu = updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { bu.setOrder(Order.DB_BEFORE_REPO); @@ -95,6 +104,12 @@ return Response.none(); } + private boolean isDeletingOnlyPatchSet(RevisionResource rsrc) throws OrmException { + Collection<PatchSet> patchSets = psUtil.byChange(db.get(), rsrc.getNotes()); + return patchSets.size() == 1 + && Iterables.getOnlyElement(patchSets).getId().equals(rsrc.getPatchSet().getId()); + } + private class Op implements BatchUpdateOp { private final PatchSet.Id psId; @@ -137,10 +152,9 @@ return; } ctx.addRefUpdate( - new ReceiveCommand( - ObjectId.fromString(patchSet.getRevision().get()), - ObjectId.zeroId(), - patchSet.getRefName())); + ObjectId.fromString(patchSet.getRevision().get()), + ObjectId.zeroId(), + patchSet.getRefName()); } private void deleteDraftPatchSet(PatchSet patchSet, ChangeContext ctx) throws OrmException { @@ -199,15 +213,14 @@ @Override public UiAction.Description getDescription(RevisionResource rsrc) { try { - int psCount = psUtil.byChange(db.get(), rsrc.getNotes()).size(); return new UiAction.Description() .setLabel("Delete") .setTitle(String.format("Delete draft revision %d", rsrc.getPatchSet().getPatchSetId())) .setVisible( allowDrafts && rsrc.getPatchSet().isDraft() - && rsrc.getControl().canDelete(db.get(), Change.Status.DRAFT) - && psCount > 1); + && psUtil.byChange(db.get(), rsrc.getNotes()).size() > 1 + && rsrc.getControl().canDelete(db.get(), Change.Status.DRAFT)); } catch (OrmException e) { throw new IllegalStateException(e); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java new file mode 100644 index 0000000..71c940b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
@@ -0,0 +1,86 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.TimeUtil; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; +import com.google.gerrit.server.update.UpdateException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +@Singleton +public class DeletePrivate + extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, Response<String>> { + private final ChangeMessagesUtil cmUtil; + private final Provider<ReviewDb> dbProvider; + private final PermissionBackend permissionBackend; + + @Inject + DeletePrivate( + Provider<ReviewDb> dbProvider, + RetryHelper retryHelper, + ChangeMessagesUtil cmUtil, + PermissionBackend permissionBackend) { + super(retryHelper); + this.dbProvider = dbProvider; + this.cmUtil = cmUtil; + this.permissionBackend = permissionBackend; + } + + @Override + protected Response<String> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, SetPrivateOp.Input input) + throws RestApiException, UpdateException { + if (!canDeletePrivate(rsrc)) { + throw new AuthException("not allowed to unmark private"); + } + + if (!rsrc.getChange().isPrivate()) { + throw new ResourceConflictException("change is not private"); + } + + ChangeControl control = rsrc.getControl(); + SetPrivateOp op = new SetPrivateOp(cmUtil, false, input); + try (BatchUpdate u = + updateFactory.create( + dbProvider.get(), + control.getProject().getNameKey(), + control.getUser(), + TimeUtil.nowTs())) { + u.addOp(control.getId(), op).execute(); + } + + return Response.none(); + } + + protected boolean canDeletePrivate(ChangeResource rsrc) { + PermissionBackend.WithUser user = permissionBackend.user(rsrc.getUser()); + return user.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER) + || (rsrc.isUserOwner() && rsrc.getChange().getStatus() != Change.Status.MERGED); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivateByPost.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivateByPost.java new file mode 100644 index 0000000..9cf85d1 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivateByPost.java
@@ -0,0 +1,44 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.update.RetryHelper; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +@Singleton +public class DeletePrivateByPost extends DeletePrivate implements UiAction<ChangeResource> { + @Inject + DeletePrivateByPost( + Provider<ReviewDb> dbProvider, + RetryHelper retryHelper, + ChangeMessagesUtil cmUtil, + PermissionBackend permissionBackend) { + super(dbProvider, retryHelper, cmUtil, permissionBackend); + } + + @Override + public Description getDescription(ChangeResource rsrc) { + return new UiAction.Description() + .setLabel("Unmark private") + .setTitle("Unmark change as private") + .setVisible(rsrc.getChange().isPrivate() && canDeletePrivate(rsrc)); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java index 1485d03..c2bcd69 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
@@ -14,255 +14,63 @@ package com.google.gerrit.server.change; -import com.google.common.collect.Iterables; import com.google.gerrit.common.TimeUtil; -import com.google.gerrit.common.data.LabelType; -import com.google.gerrit.common.data.LabelTypes; import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; -import com.google.gerrit.extensions.api.changes.NotifyHandling; -import com.google.gerrit.extensions.restapi.AuthException; -import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.ChangeMessage; -import com.google.gerrit.reviewdb.client.PatchSet; -import com.google.gerrit.reviewdb.client.PatchSetApproval; -import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.reviewdb.server.ReviewDbUtil; -import com.google.gerrit.server.ApprovalsUtil; -import com.google.gerrit.server.ChangeMessagesUtil; -import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.PatchSetUtil; -import com.google.gerrit.server.extensions.events.ReviewerDeleted; -import com.google.gerrit.server.mail.send.DeleteReviewerSender; -import com.google.gerrit.server.notedb.ChangeUpdate; -import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; -import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; -import com.google.gerrit.server.update.BatchUpdateReviewDb; -import com.google.gerrit.server.update.ChangeContext; -import com.google.gerrit.server.update.Context; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Singleton -public class DeleteReviewer implements RestModifyView<ReviewerResource, DeleteReviewerInput> { - private static final Logger log = LoggerFactory.getLogger(DeleteReviewer.class); +public class DeleteReviewer + extends RetryingRestModifyView<ReviewerResource, DeleteReviewerInput, Response<?>> { private final Provider<ReviewDb> dbProvider; - private final ApprovalsUtil approvalsUtil; - private final PatchSetUtil psUtil; - private final ChangeMessagesUtil cmUtil; - private final BatchUpdate.Factory batchUpdateFactory; - private final IdentifiedUser.GenericFactory userFactory; - private final ReviewerDeleted reviewerDeleted; - private final Provider<IdentifiedUser> user; - private final DeleteReviewerSender.Factory deleteReviewerSenderFactory; - private final NotesMigration migration; - private final NotifyUtil notifyUtil; + private final DeleteReviewerOp.Factory deleteReviewerOpFactory; + private final DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory; @Inject DeleteReviewer( Provider<ReviewDb> dbProvider, - ApprovalsUtil approvalsUtil, - PatchSetUtil psUtil, - ChangeMessagesUtil cmUtil, - BatchUpdate.Factory batchUpdateFactory, - IdentifiedUser.GenericFactory userFactory, - ReviewerDeleted reviewerDeleted, - Provider<IdentifiedUser> user, - DeleteReviewerSender.Factory deleteReviewerSenderFactory, - NotesMigration migration, - NotifyUtil notifyUtil) { + RetryHelper retryHelper, + DeleteReviewerOp.Factory deleteReviewerOpFactory, + DeleteReviewerByEmailOp.Factory deleteReviewerByEmailOpFactory) { + super(retryHelper); this.dbProvider = dbProvider; - this.approvalsUtil = approvalsUtil; - this.psUtil = psUtil; - this.cmUtil = cmUtil; - this.batchUpdateFactory = batchUpdateFactory; - this.userFactory = userFactory; - this.reviewerDeleted = reviewerDeleted; - this.user = user; - this.deleteReviewerSenderFactory = deleteReviewerSenderFactory; - this.migration = migration; - this.notifyUtil = notifyUtil; + this.deleteReviewerOpFactory = deleteReviewerOpFactory; + this.deleteReviewerByEmailOpFactory = deleteReviewerByEmailOpFactory; } @Override - public Response<?> apply(ReviewerResource rsrc, DeleteReviewerInput input) + protected Response<?> applyImpl( + BatchUpdate.Factory updateFactory, ReviewerResource rsrc, DeleteReviewerInput input) throws RestApiException, UpdateException { if (input == null) { input = new DeleteReviewerInput(); } - if (input.notify == null) { - input.notify = NotifyHandling.ALL; - } try (BatchUpdate bu = - batchUpdateFactory.create( + updateFactory.create( dbProvider.get(), rsrc.getChangeResource().getProject(), rsrc.getChangeResource().getUser(), TimeUtil.nowTs())) { - Op op = new Op(rsrc.getReviewerUser().getAccount(), input); + BatchUpdateOp op; + if (rsrc.isByEmail()) { + op = deleteReviewerByEmailOpFactory.create(rsrc.getReviewerByEmail(), input); + } else { + op = deleteReviewerOpFactory.create(rsrc.getReviewerUser().getAccount(), input); + } bu.addOp(rsrc.getChange().getId(), op); bu.execute(); } - return Response.none(); } - - private class Op implements BatchUpdateOp { - private final Account reviewer; - private final DeleteReviewerInput input; - ChangeMessage changeMessage; - Change currChange; - PatchSet currPs; - Map<String, Short> newApprovals = new HashMap<>(); - Map<String, Short> oldApprovals = new HashMap<>(); - - Op(Account reviewerAccount, DeleteReviewerInput input) { - this.reviewer = reviewerAccount; - this.input = input; - } - - @Override - public boolean updateChange(ChangeContext ctx) - throws AuthException, ResourceNotFoundException, OrmException { - Account.Id reviewerId = reviewer.getId(); - if (!approvalsUtil.getReviewers(ctx.getDb(), ctx.getNotes()).all().contains(reviewerId)) { - throw new ResourceNotFoundException(); - } - currChange = ctx.getChange(); - currPs = psUtil.current(ctx.getDb(), ctx.getNotes()); - - LabelTypes labelTypes = ctx.getControl().getLabelTypes(); - // removing a reviewer will remove all her votes - for (LabelType lt : labelTypes.getLabelTypes()) { - newApprovals.put(lt.getName(), (short) 0); - } - - StringBuilder msg = new StringBuilder(); - msg.append("Removed reviewer " + reviewer.getFullName()); - StringBuilder removedVotesMsg = new StringBuilder(); - removedVotesMsg.append(" with the following votes:\n\n"); - List<PatchSetApproval> del = new ArrayList<>(); - boolean votesRemoved = false; - for (PatchSetApproval a : approvals(ctx, reviewerId)) { - if (ctx.getControl().canRemoveReviewer(a)) { - del.add(a); - if (a.getPatchSetId().equals(currPs.getId()) && a.getValue() != 0) { - oldApprovals.put(a.getLabel(), a.getValue()); - removedVotesMsg - .append("* ") - .append(a.getLabel()) - .append(formatLabelValue(a.getValue())) - .append(" by ") - .append(userFactory.create(a.getAccountId()).getNameEmail()) - .append("\n"); - votesRemoved = true; - } - } else { - throw new AuthException("delete reviewer not permitted"); - } - } - - if (votesRemoved) { - msg.append(removedVotesMsg); - } else { - msg.append("."); - } - ctx.getDb().patchSetApprovals().delete(del); - ChangeUpdate update = ctx.getUpdate(currPs.getId()); - update.removeReviewer(reviewerId); - - changeMessage = - ChangeMessagesUtil.newMessage( - ctx, msg.toString(), ChangeMessagesUtil.TAG_DELETE_REVIEWER); - cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage); - - return true; - } - - @Override - public void postUpdate(Context ctx) { - if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) { - emailReviewers(ctx.getProject(), currChange, changeMessage); - } - reviewerDeleted.fire( - currChange, - currPs, - reviewer, - ctx.getAccount(), - changeMessage.getMessage(), - newApprovals, - oldApprovals, - input.notify, - ctx.getWhen()); - } - - private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId) - throws OrmException { - Change.Id changeId = ctx.getNotes().getChangeId(); - Iterable<PatchSetApproval> approvals; - PrimaryStorage r = PrimaryStorage.of(ctx.getChange()); - - if (migration.readChanges() && r == PrimaryStorage.REVIEW_DB) { - // Because NoteDb and ReviewDb have different semantics for zero-value - // approvals, we must fall back to ReviewDb as the source of truth here. - ReviewDb db = ctx.getDb(); - - if (db instanceof BatchUpdateReviewDb) { - db = ((BatchUpdateReviewDb) db).unsafeGetDelegate(); - } - db = ReviewDbUtil.unwrapDb(db); - approvals = db.patchSetApprovals().byChange(changeId); - } else { - approvals = approvalsUtil.byChange(ctx.getDb(), ctx.getNotes()).values(); - } - - return Iterables.filter(approvals, psa -> accountId.equals(psa.getAccountId())); - } - - private String formatLabelValue(short value) { - if (value > 0) { - return "+" + value; - } - return Short.toString(value); - } - - private void emailReviewers( - Project.NameKey projectName, Change change, ChangeMessage changeMessage) { - Account.Id userId = user.get().getAccountId(); - if (userId.equals(reviewer.getId())) { - // The user knows they removed themselves, don't bother emailing them. - return; - } - try { - DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId()); - cm.setFrom(userId); - cm.addReviewers(Collections.singleton(reviewer.getId())); - cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn()); - cm.setNotify(input.notify); - cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails)); - cm.send(); - } catch (Exception err) { - log.error("Cannot email update for change " + change.getId(), err); - } - } - } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java new file mode 100644 index 0000000..341ad4a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerByEmailOp.java
@@ -0,0 +1,105 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.extensions.api.changes.DeleteReviewerInput; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.ChangeMessage; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.send.DeleteReviewerSender; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.Context; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.util.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DeleteReviewerByEmailOp implements BatchUpdateOp { + private static final Logger log = LoggerFactory.getLogger(DeleteReviewer.class); + + public interface Factory { + DeleteReviewerByEmailOp create(Address reviewer, DeleteReviewerInput input); + } + + private final DeleteReviewerSender.Factory deleteReviewerSenderFactory; + private final NotifyUtil notifyUtil; + private final Address reviewer; + private final DeleteReviewerInput input; + + private ChangeMessage changeMessage; + private Change change; + + @Inject + DeleteReviewerByEmailOp( + DeleteReviewerSender.Factory deleteReviewerSenderFactory, + NotifyUtil notifyUtil, + @Assisted Address reviewer, + @Assisted DeleteReviewerInput input) { + this.deleteReviewerSenderFactory = deleteReviewerSenderFactory; + this.notifyUtil = notifyUtil; + this.reviewer = reviewer; + this.input = input; + } + + @Override + public boolean updateChange(ChangeContext ctx) throws OrmException { + change = ctx.getChange(); + PatchSet.Id psId = ctx.getChange().currentPatchSetId(); + String msg = "Removed reviewer " + reviewer; + changeMessage = + new ChangeMessage( + new ChangeMessage.Key(change.getId(), ChangeUtil.messageUuid()), + ctx.getAccountId(), + ctx.getWhen(), + psId); + changeMessage.setMessage(msg); + + ctx.getUpdate(psId).setChangeMessage(msg); + ctx.getUpdate(psId).removeReviewerByEmail(reviewer); + return true; + } + + @Override + public void postUpdate(Context ctx) { + if (input.notify == null) { + if (change.isWorkInProgress()) { + input.notify = NotifyHandling.NONE; + } else { + input.notify = NotifyHandling.ALL; + } + } + if (!NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) { + return; + } + try { + DeleteReviewerSender cm = + deleteReviewerSenderFactory.create(ctx.getProject(), change.getId()); + cm.setFrom(ctx.getAccountId()); + cm.addReviewersByEmail(Collections.singleton(reviewer)); + cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn()); + cm.setNotify(input.notify); + cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails)); + cm.send(); + } catch (Exception err) { + log.error("Cannot email update for change " + change.getId(), err); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerOp.java new file mode 100644 index 0000000..df4b435 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -0,0 +1,240 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.collect.Iterables; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.common.data.LabelTypes; +import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.ChangeMessage; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.PatchSetApproval; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.reviewdb.server.ReviewDbUtil; +import com.google.gerrit.server.ApprovalsUtil; +import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.extensions.events.ReviewerDeleted; +import com.google.gerrit.server.mail.send.DeleteReviewerSender; +import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; +import com.google.gerrit.server.notedb.NotesMigration; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.BatchUpdateReviewDb; +import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.Context; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.assistedinject.Assisted; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DeleteReviewerOp implements BatchUpdateOp { + private static final Logger log = LoggerFactory.getLogger(DeleteReviewer.class); + + public interface Factory { + DeleteReviewerOp create(Account reviewerAccount, DeleteReviewerInput input); + } + + private final ApprovalsUtil approvalsUtil; + private final PatchSetUtil psUtil; + private final ChangeMessagesUtil cmUtil; + private final IdentifiedUser.GenericFactory userFactory; + private final ReviewerDeleted reviewerDeleted; + private final Provider<IdentifiedUser> user; + private final DeleteReviewerSender.Factory deleteReviewerSenderFactory; + private final NotesMigration migration; + private final NotifyUtil notifyUtil; + + private final Account reviewer; + private final DeleteReviewerInput input; + + ChangeMessage changeMessage; + Change currChange; + PatchSet currPs; + Map<String, Short> newApprovals = new HashMap<>(); + Map<String, Short> oldApprovals = new HashMap<>(); + + @Inject + DeleteReviewerOp( + ApprovalsUtil approvalsUtil, + PatchSetUtil psUtil, + ChangeMessagesUtil cmUtil, + IdentifiedUser.GenericFactory userFactory, + ReviewerDeleted reviewerDeleted, + Provider<IdentifiedUser> user, + DeleteReviewerSender.Factory deleteReviewerSenderFactory, + NotesMigration migration, + NotifyUtil notifyUtil, + @Assisted Account reviewerAccount, + @Assisted DeleteReviewerInput input) { + this.approvalsUtil = approvalsUtil; + this.psUtil = psUtil; + this.cmUtil = cmUtil; + this.userFactory = userFactory; + this.reviewerDeleted = reviewerDeleted; + this.user = user; + this.deleteReviewerSenderFactory = deleteReviewerSenderFactory; + this.migration = migration; + this.notifyUtil = notifyUtil; + + this.reviewer = reviewerAccount; + this.input = input; + } + + @Override + public boolean updateChange(ChangeContext ctx) + throws AuthException, ResourceNotFoundException, OrmException { + Account.Id reviewerId = reviewer.getId(); + if (!approvalsUtil.getReviewers(ctx.getDb(), ctx.getNotes()).all().contains(reviewerId)) { + throw new ResourceNotFoundException(); + } + currChange = ctx.getChange(); + currPs = psUtil.current(ctx.getDb(), ctx.getNotes()); + + LabelTypes labelTypes = ctx.getControl().getLabelTypes(); + // removing a reviewer will remove all her votes + for (LabelType lt : labelTypes.getLabelTypes()) { + newApprovals.put(lt.getName(), (short) 0); + } + + StringBuilder msg = new StringBuilder(); + msg.append("Removed reviewer " + reviewer.getFullName()); + StringBuilder removedVotesMsg = new StringBuilder(); + removedVotesMsg.append(" with the following votes:\n\n"); + List<PatchSetApproval> del = new ArrayList<>(); + boolean votesRemoved = false; + for (PatchSetApproval a : approvals(ctx, reviewerId)) { + if (ctx.getControl().canRemoveReviewer(a)) { + del.add(a); + if (a.getPatchSetId().equals(currPs.getId()) && a.getValue() != 0) { + oldApprovals.put(a.getLabel(), a.getValue()); + removedVotesMsg + .append("* ") + .append(a.getLabel()) + .append(formatLabelValue(a.getValue())) + .append(" by ") + .append(userFactory.create(a.getAccountId()).getNameEmail()) + .append("\n"); + votesRemoved = true; + } + } else { + throw new AuthException("delete reviewer not permitted"); + } + } + + if (votesRemoved) { + msg.append(removedVotesMsg); + } else { + msg.append("."); + } + ctx.getDb().patchSetApprovals().delete(del); + ChangeUpdate update = ctx.getUpdate(currPs.getId()); + update.removeReviewer(reviewerId); + + changeMessage = + ChangeMessagesUtil.newMessage(ctx, msg.toString(), ChangeMessagesUtil.TAG_DELETE_REVIEWER); + cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage); + + return true; + } + + @Override + public void postUpdate(Context ctx) { + if (input.notify == null) { + if (currChange.isWorkInProgress()) { + input.notify = oldApprovals.isEmpty() ? NotifyHandling.NONE : NotifyHandling.OWNER; + } else { + input.notify = NotifyHandling.ALL; + } + } + if (NotifyUtil.shouldNotify(input.notify, input.notifyDetails)) { + emailReviewers(ctx.getProject(), currChange, changeMessage); + } + reviewerDeleted.fire( + currChange, + currPs, + reviewer, + ctx.getAccount(), + changeMessage.getMessage(), + newApprovals, + oldApprovals, + input.notify, + ctx.getWhen()); + } + + private Iterable<PatchSetApproval> approvals(ChangeContext ctx, Account.Id accountId) + throws OrmException { + Change.Id changeId = ctx.getNotes().getChangeId(); + Iterable<PatchSetApproval> approvals; + PrimaryStorage r = PrimaryStorage.of(ctx.getChange()); + + if (migration.readChanges() && r == PrimaryStorage.REVIEW_DB) { + // Because NoteDb and ReviewDb have different semantics for zero-value + // approvals, we must fall back to ReviewDb as the source of truth here. + ReviewDb db = ctx.getDb(); + + if (db instanceof BatchUpdateReviewDb) { + db = ((BatchUpdateReviewDb) db).unsafeGetDelegate(); + } + db = ReviewDbUtil.unwrapDb(db); + approvals = db.patchSetApprovals().byChange(changeId); + } else { + approvals = approvalsUtil.byChange(ctx.getDb(), ctx.getNotes()).values(); + } + + return Iterables.filter(approvals, psa -> accountId.equals(psa.getAccountId())); + } + + private String formatLabelValue(short value) { + if (value > 0) { + return "+" + value; + } + return Short.toString(value); + } + + private void emailReviewers( + Project.NameKey projectName, Change change, ChangeMessage changeMessage) { + Account.Id userId = user.get().getAccountId(); + if (userId.equals(reviewer.getId())) { + // The user knows they removed themselves, don't bother emailing them. + return; + } + try { + DeleteReviewerSender cm = deleteReviewerSenderFactory.create(projectName, change.getId()); + cm.setFrom(userId); + cm.addReviewers(Collections.singleton(reviewer.getId())); + cm.setChangeMessage(changeMessage.getMessage(), changeMessage.getWrittenOn()); + cm.setNotify(input.notify); + cm.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails)); + cm.send(); + } catch (Exception err) { + log.error("Cannot email update for change " + change.getId(), err); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java index 963e7b4..a09d22e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteVote.java
@@ -26,7 +26,6 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; @@ -46,12 +45,15 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.LabelVote; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -59,11 +61,10 @@ import org.slf4j.LoggerFactory; @Singleton -public class DeleteVote implements RestModifyView<VoteResource, DeleteVoteInput> { +public class DeleteVote extends RetryingRestModifyView<VoteResource, DeleteVoteInput, Response<?>> { private static final Logger log = LoggerFactory.getLogger(DeleteVote.class); private final Provider<ReviewDb> db; - private final BatchUpdate.Factory batchUpdateFactory; private final ApprovalsUtil approvalsUtil; private final PatchSetUtil psUtil; private final ChangeMessagesUtil cmUtil; @@ -75,7 +76,7 @@ @Inject DeleteVote( Provider<ReviewDb> db, - BatchUpdate.Factory batchUpdateFactory, + RetryHelper retryHelper, ApprovalsUtil approvalsUtil, PatchSetUtil psUtil, ChangeMessagesUtil cmUtil, @@ -83,8 +84,8 @@ VoteDeleted voteDeleted, DeleteVoteSender.Factory deleteVoteSenderFactory, NotifyUtil notifyUtil) { + super(retryHelper); this.db = db; - this.batchUpdateFactory = batchUpdateFactory; this.approvalsUtil = approvalsUtil; this.psUtil = psUtil; this.cmUtil = cmUtil; @@ -95,7 +96,8 @@ } @Override - public Response<?> apply(VoteResource rsrc, DeleteVoteInput input) + protected Response<?> applyImpl( + BatchUpdate.Factory updateFactory, VoteResource rsrc, DeleteVoteInput input) throws RestApiException, UpdateException { if (input == null) { input = new DeleteVoteInput(); @@ -114,7 +116,7 @@ } try (BatchUpdate bu = - batchUpdateFactory.create( + updateFactory.create( db.get(), change.getProject(), r.getControl().getUser(), TimeUtil.nowTs())) { bu.addOp(change.getId(), new Op(r.getReviewerUser().getAccountId(), rsrc.getLabel(), input)); bu.execute(); @@ -141,7 +143,7 @@ @Override public boolean updateChange(ChangeContext ctx) - throws OrmException, AuthException, ResourceNotFoundException { + throws OrmException, AuthException, ResourceNotFoundException, IOException { ChangeControl ctl = ctx.getControl(); change = ctl.getChange(); PatchSet.Id psId = change.currentPatchSetId(); @@ -150,7 +152,9 @@ boolean found = false; LabelTypes labelTypes = ctx.getControl().getLabelTypes(); - for (PatchSetApproval a : approvalsUtil.byPatchSetUser(ctx.getDb(), ctl, psId, accountId)) { + for (PatchSetApproval a : + approvalsUtil.byPatchSetUser( + ctx.getDb(), ctl, psId, accountId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) { if (labelTypes.byLabel(a.getLabelId()) == null) { continue; // Ignore undefined labels. } else if (!a.getLabel().equals(label)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java index 2e8fc2d..8e3ee9f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -50,6 +50,24 @@ private static final Logger log = LoggerFactory.getLogger(EmailReviewComments.class); public interface Factory { + // TODO(dborowitz/wyatta): Rationalize these arguments so HTML and text templates are operating + // on the same set of inputs. + /** + * @param notify setting for handling notification. + * @param accountsToNotify detailed map of accounts to notify. + * @param notes change notes. + * @param patchSet patch set corresponding to the top-level op + * @param user user the email should come from. + * @param message used by text template only: the full ChangeMessage that will go in the + * database. The contents of this message typically include the "Patch set N" header and "(M + * comments)". + * @param comments inline comments. + * @param patchSetComment used by HTML template only: some quasi-human-generated text. The + * contents should *not* include a "Patch set N" header or "(M comments)" footer, as these + * will be added automatically in soy in a structured way. + * @param labels labels applied as part of this review operation. + * @return handle for sending email. + */ EmailReviewComments create( NotifyHandling notify, ListMultimap<RecipientType, Account.Id> accountsToNotify,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java index 732848e..181505d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -22,6 +22,7 @@ import com.google.gerrit.common.Nullable; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.PatchScript.FileMode; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.reviewdb.client.Patch; @@ -67,10 +68,41 @@ this.registry = ftr; } - public BinaryResult getContent(ProjectState project, ObjectId revstr, String path) - throws ResourceNotFoundException, IOException { + /** + * Get the content of a file at a specific commit or one of it's parent commits. + * + * @param project A {@code Project} that this request refers to. + * @param revstr An {@code ObjectId} specifying the commit. + * @param path A string specifying the filepath. + * @param parent A 1-based parent index to get the content from instead. Null if the content + * should be obtained from {@code revstr} instead. + * @return Content of the file as {@code BinaryResult}. + * @throws ResourceNotFoundException + * @throws IOException + */ + public BinaryResult getContent( + ProjectState project, ObjectId revstr, String path, @Nullable Integer parent) + throws BadRequestException, ResourceNotFoundException, IOException { try (Repository repo = openRepository(project); RevWalk rw = new RevWalk(repo)) { + if (parent != null) { + RevCommit revCommit = rw.parseCommit(revstr); + if (revCommit == null) { + throw new ResourceNotFoundException("commit not found"); + } + if (parent > revCommit.getParentCount()) { + throw new BadRequestException("invalid parent"); + } + revstr = rw.parseCommit(revstr).getParent(Integer.max(0, parent - 1)).toObjectId(); + } + return getContent(repo, project, revstr, path); + } + } + + public BinaryResult getContent( + Repository repo, ProjectState project, ObjectId revstr, String path) + throws IOException, ResourceNotFoundException { + try (RevWalk rw = new RevWalk(repo)) { RevCommit commit = rw.parseCommit(revstr); ObjectReader reader = rw.getObjectReader(); TreeWalk tw = TreeWalk.forPath(reader, path, commit.getTree()); @@ -104,7 +136,7 @@ } } - private static BinaryResult asBinaryResult(byte[] raw, final ObjectLoader obj) { + private static BinaryResult asBinaryResult(byte[] raw, ObjectLoader obj) { if (raw != null) { return BinaryResult.create(raw); } @@ -174,7 +206,7 @@ @SuppressWarnings("resource") private BinaryResult zipBlob( - final String path, final ObjectLoader obj, RevCommit commit, @Nullable final String suffix) { + final String path, ObjectLoader obj, RevCommit commit, @Nullable final String suffix) { final String commitName = commit.getName(); final long when = commit.getCommitTime() * 1000L; return new BinaryResult() { @@ -241,7 +273,7 @@ // an attacker could upload a *.class file and have us send a ZIP // that can be invoked through an applet tag in the victim's browser. // - Hasher h = Hashing.md5().newHasher(); + Hasher h = Hashing.murmur3_128().newHasher(); byte[] buf = new byte[8]; NB.encodeInt64(buf, 0, TimeUtil.nowMs());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java index 60a4daf..6ccd460 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -48,9 +48,14 @@ Map<String, FileInfo> toFileInfoMap(Change change, RevId revision, @Nullable PatchSet base) throws PatchListNotAvailableException { + ObjectId objectId = ObjectId.fromString(revision.get()); + return toFileInfoMap(change, objectId, base); + } + + Map<String, FileInfo> toFileInfoMap(Change change, ObjectId objectId, @Nullable PatchSet base) + throws PatchListNotAvailableException { ObjectId a = (base == null) ? null : ObjectId.fromString(base.getRevision().get()); - ObjectId b = ObjectId.fromString(revision.get()); - return toFileInfoMap(change, new PatchListKey(a, b, Whitespace.IGNORE_NONE)); + return toFileInfoMap(change, PatchListKey.againstCommit(a, objectId, Whitespace.IGNORE_NONE)); } Map<String, FileInfo> toFileInfoMap(Change change, RevId revision, int parent)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java index 040b6de..d8ff050 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -15,6 +15,8 @@ package com.google.gerrit.server.change; import com.google.common.collect.Lists; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; import com.google.gerrit.extensions.common.FileInfo; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.registration.DynamicMap; @@ -22,10 +24,10 @@ import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.CacheControl; import com.google.gerrit.extensions.restapi.ChildCollection; +import com.google.gerrit.extensions.restapi.ETagView; import com.google.gerrit.extensions.restapi.IdString; 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.extensions.restapi.RestView; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; @@ -38,6 +40,7 @@ import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.patch.PatchList; import com.google.gerrit.server.patch.PatchListCache; +import com.google.gerrit.server.patch.PatchListKey; import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -90,7 +93,7 @@ return new FileResource(rev, id.get()); } - public static final class ListFiles implements RestReadView<RevisionResource> { + public static final class ListFiles implements ETagView<RevisionResource> { private static final Logger log = LoggerFactory.getLogger(ListFiles.class); @Option(name = "--base", metaVar = "revision-id") @@ -322,5 +325,15 @@ this.parentNum = parentNum; return this; } + + @Override + public String getETag(RevisionResource resource) { + Hasher h = Hashing.murmur3_128().newHasher(); + resource.prepareETag(h, resource.getUser()); + // File list comes from the PatchListCache, so any change to the key or value should + // invalidate ETag. + h.putLong(PatchListKey.serialVersionUID); + return h.hash().toString(); + } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FixResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FixResource.java new file mode 100644 index 0000000..08e2785 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FixResource.java
@@ -0,0 +1,42 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.extensions.restapi.RestResource; +import com.google.gerrit.extensions.restapi.RestView; +import com.google.gerrit.reviewdb.client.FixReplacement; +import com.google.inject.TypeLiteral; +import java.util.List; + +public class FixResource implements RestResource { + public static final TypeLiteral<RestView<FixResource>> FIX_KIND = + new TypeLiteral<RestView<FixResource>>() {}; + + private final List<FixReplacement> fixReplacements; + private final RevisionResource revisionResource; + + public FixResource(RevisionResource revisionResource, List<FixReplacement> fixReplacements) { + this.fixReplacements = fixReplacements; + this.revisionResource = revisionResource; + } + + public List<FixReplacement> getFixReplacements() { + return fixReplacements; + } + + public RevisionResource getRevisionResource() { + return revisionResource; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Fixes.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Fixes.java new file mode 100644 index 0000000..af9f60a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Fixes.java
@@ -0,0 +1,71 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.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.RestView; +import com.google.gerrit.reviewdb.client.FixSuggestion; +import com.google.gerrit.reviewdb.client.RobotComment; +import com.google.gerrit.server.CommentsUtil; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.util.List; +import java.util.Objects; + +@Singleton +public class Fixes implements ChildCollection<RevisionResource, FixResource> { + + private final DynamicMap<RestView<FixResource>> views; + private final CommentsUtil commentsUtil; + + @Inject + Fixes(DynamicMap<RestView<FixResource>> views, CommentsUtil commentsUtil) { + this.views = views; + this.commentsUtil = commentsUtil; + } + + @Override + public RestView<RevisionResource> list() throws ResourceNotFoundException { + throw new ResourceNotFoundException(); + } + + @Override + public FixResource parse(RevisionResource revisionResource, IdString id) + throws ResourceNotFoundException, OrmException { + String fixId = id.get(); + ChangeNotes changeNotes = revisionResource.getNotes(); + + List<RobotComment> robotComments = + commentsUtil.robotCommentsByPatchSet(changeNotes, revisionResource.getPatchSet().getId()); + for (RobotComment robotComment : robotComments) { + for (FixSuggestion fixSuggestion : robotComment.fixSuggestions) { + if (Objects.equals(fixId, fixSuggestion.fixId)) { + return new FixResource(revisionResource, fixSuggestion.replacements); + } + } + } + throw new ResourceNotFoundException(id); + } + + @Override + public DynamicMap<RestView<FixResource>> views() { + return views; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java index a874699..e33021ea 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetCommit.java
@@ -33,7 +33,6 @@ private final GitRepositoryManager repoManager; private final ChangeJson.Factory json; - @Option(name = "--links", usage = "Include weblinks") private boolean addLinks; @Inject @@ -42,6 +41,12 @@ this.json = json; } + @Option(name = "--links", usage = "Include weblinks") + public GetCommit setAddLinks(boolean addLinks) { + this.addLinks = addLinks; + return this; + } + @Override public Response<CommitInfo> apply(RevisionResource rsrc) throws IOException { Project.NameKey p = rsrc.getChange().getProject();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java index abb9e66..5433653 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetContent.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.change; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestReadView; @@ -30,21 +31,23 @@ import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; -import com.google.inject.Singleton; import java.io.IOException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.kohsuke.args4j.Option; -@Singleton public class GetContent implements RestReadView<FileResource> { private final Provider<ReviewDb> db; private final GitRepositoryManager gitManager; private final PatchSetUtil psUtil; private final FileContentUtil fileContentUtil; + @Option(name = "--parent") + private Integer parent; + @Inject GetContent( Provider<ReviewDb> db, @@ -59,7 +62,7 @@ @Override public BinaryResult apply(FileResource rsrc) - throws ResourceNotFoundException, IOException, NoSuchChangeException, OrmException { + throws ResourceNotFoundException, IOException, BadRequestException, OrmException { String path = rsrc.getPatchKey().get(); if (Patch.COMMIT_MSG.equals(path)) { String msg = getMessage(rsrc.getRevision().getChangeResource().getNotes()); @@ -75,7 +78,8 @@ return fileContentUtil.getContent( rsrc.getRevision().getControl().getProjectControl().getProjectState(), ObjectId.fromString(rsrc.getRevision().getPatchSet().getRevision().get()), - path); + path, + parent); } private String getMessage(ChangeNotes notes) throws OrmException, IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java index 5daf69a..1ac5a88f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -54,6 +54,7 @@ import com.google.inject.Inject; import java.io.IOException; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.diff.ReplaceEdit; @@ -168,6 +169,7 @@ psf.setLoadComments(context != DiffPreferencesInfo.WHOLE_FILE_CONTEXT); PatchScript ps = psf.call(); Content content = new Content(ps); + Set<Edit> editsDueToRebase = ps.getEditsDueToRebase(); for (Edit edit : ps.getEdits()) { if (edit.getType() == Edit.Type.EMPTY) { continue; @@ -190,7 +192,8 @@ case REPLACE: List<Edit> internalEdit = edit instanceof ReplaceEdit ? ((ReplaceEdit) edit).getInternalEdits() : null; - content.addDiff(edit.getEndA(), edit.getEndB(), internalEdit); + boolean dueToRebase = editsDueToRebase.contains(edit); + content.addDiff(edit.getEndA(), edit.getEndB(), internalEdit, dueToRebase); break; case EMPTY: default: @@ -372,7 +375,7 @@ } } - void addDiff(int endA, int endB, List<Edit> internalEdit) { + void addDiff(int endA, int endB, List<Edit> internalEdit, boolean dueToRebase) { int lenA = endA - nextA; int lenB = endB - nextB; checkState(lenA > 0 || lenB > 0); @@ -408,6 +411,7 @@ } } } + e.dueToRebase = dueToRebase ? true : null; } private ContentEntry entry() { @@ -437,7 +441,7 @@ } @Override - public final int parseArguments(final Parameters params) throws CmdLineException { + public final int parseArguments(Parameters params) throws CmdLineException { final String value = params.getParameter(0); short context; if ("all".equalsIgnoreCase(value)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java index aa0b339..db9af1d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
@@ -16,6 +16,7 @@ import com.google.gerrit.extensions.api.changes.ReviewerInfo; import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -31,7 +32,8 @@ } @Override - public List<ReviewerInfo> apply(ReviewerResource rsrc) throws OrmException { + public List<ReviewerInfo> apply(ReviewerResource rsrc) + throws OrmException, PermissionBackendException { return json.format(rsrc); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java index e476e73..8a6a1ab 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRevisionActions.java
@@ -63,7 +63,7 @@ @Override public String getETag(RevisionResource rsrc) { - Hasher h = Hashing.md5().newHasher(); + Hasher h = Hashing.murmur3_128().newHasher(); CurrentUser user = rsrc.getControl().getUser(); try { rsrc.getChangeResource().prepareETag(h, user);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Ignore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Ignore.java new file mode 100644 index 0000000..83ab811 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Ignore.java
@@ -0,0 +1,76 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.StarredChangesUtil; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class Ignore + implements RestModifyView<ChangeResource, Ignore.Input>, UiAction<ChangeResource> { + private static final Logger log = LoggerFactory.getLogger(Ignore.class); + + public static class Input {} + + private final Provider<IdentifiedUser> self; + private final StarredChangesUtil stars; + + @Inject + Ignore(Provider<IdentifiedUser> self, StarredChangesUtil stars) { + this.self = self; + this.stars = stars; + } + + @Override + public Description getDescription(ChangeResource rsrc) { + return new UiAction.Description() + .setLabel("Ignore") + .setTitle("Ignore the change") + .setVisible(!rsrc.isUserOwner() && !isIgnored(rsrc)); + } + + @Override + public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException { + try { + if (rsrc.isUserOwner() || isIgnored(rsrc)) { + // early exit for own changes and already ignored changes + return Response.ok(""); + } + stars.ignore(self.get().getAccountId(), rsrc.getProject(), rsrc.getChange().getId()); + } catch (OrmException e) { + throw new RestApiException("failed to ignore change", e); + } + return Response.ok(""); + } + + private boolean isIgnored(ChangeResource rsrc) { + try { + return stars.isIgnoredBy(rsrc.getChange().getId(), self.get().getAccountId()); + } catch (OrmException e) { + log.error("failed to check ignored star", e); + } + return false; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java index 843ef3c..6111dfb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/IncludedInResolver.java
@@ -42,8 +42,7 @@ private static final Logger log = LoggerFactory.getLogger(IncludedInResolver.class); - public static Result resolve(final Repository repo, final RevWalk rw, final RevCommit commit) - throws IOException { + public static Result resolve(Repository repo, RevWalk rw, RevCommit commit) throws IOException { RevFlag flag = newFlag(rw); try { return new IncludedInResolver(repo, rw, commit, flag).resolve(); @@ -53,7 +52,7 @@ } public static boolean includedInOne( - final Repository repo, final RevWalk rw, final RevCommit commit, final Collection<Ref> refs) + final Repository repo, RevWalk rw, RevCommit commit, Collection<Ref> refs) throws IOException { RevFlag flag = newFlag(rw); try { @@ -100,7 +99,7 @@ return detail; } - private boolean includedInOne(final Collection<Ref> refs) throws IOException { + private boolean includedInOne(Collection<Ref> refs) throws IOException { parseCommits(refs); List<RevCommit> before = new ArrayList<>(); List<RevCommit> after = new ArrayList<>(); @@ -112,7 +111,7 @@ } /** Resolves which tip refs include the target commit. */ - private Set<String> includedIn(final Collection<RevCommit> tips, int limit) + private Set<String> includedIn(Collection<RevCommit> tips, int limit) throws IOException, MissingObjectException, IncorrectObjectTypeException { Set<String> result = new HashSet<>(); for (RevCommit tip : tips) { @@ -149,7 +148,7 @@ * @param before * @param after */ - private void partition(final List<RevCommit> before, final List<RevCommit> after) { + private void partition(List<RevCommit> before, List<RevCommit> after) { int insertionPoint = Collections.binarySearch( tipsByCommitTime, @@ -187,7 +186,7 @@ } /** Parse commit of ref and store the relation between ref and commit. */ - private void parseCommits(final Collection<Ref> refs) throws IOException { + private void parseCommits(Collection<Ref> refs) throws IOException { if (commitToRef != null) { return; } @@ -219,7 +218,7 @@ sortOlderFirst(tipsByCommitTime); } - private void sortOlderFirst(final List<RevCommit> tips) { + private void sortOlderFirst(List<RevCommit> tips) { Collections.sort( tips, new Comparator<RevCommit>() { @@ -236,7 +235,7 @@ public Result() {} - public void setBranches(final List<String> b) { + public void setBranches(List<String> b) { Collections.sort(b); branches = b; } @@ -245,7 +244,7 @@ return branches; } - public void setTags(final List<String> t) { + public void setTags(List<String> t) { Collections.sort(t); tags = t; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java index 9257445..7c4d158 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Index.java
@@ -16,11 +16,16 @@ import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.Response; -import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.change.Index.Input; import com.google.gerrit.server.index.change.ChangeIndexer; -import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -28,25 +33,33 @@ import java.io.IOException; @Singleton -public class Index implements RestModifyView<ChangeResource, Input> { +public class Index extends RetryingRestModifyView<ChangeResource, Input, Response<?>> { public static class Input {} private final Provider<ReviewDb> db; + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; private final ChangeIndexer indexer; @Inject - Index(Provider<ReviewDb> db, ChangeIndexer indexer) { + Index( + Provider<ReviewDb> db, + RetryHelper retryHelper, + PermissionBackend permissionBackend, + Provider<CurrentUser> user, + ChangeIndexer indexer) { + super(retryHelper); this.db = db; + this.permissionBackend = permissionBackend; + this.user = user; this.indexer = indexer; } @Override - public Response<?> apply(ChangeResource rsrc, Input input) - throws IOException, AuthException, OrmException { - ChangeControl ctl = rsrc.getControl(); - if (!ctl.isOwner() && !ctl.getUser().getCapabilities().canMaintainServer()) { - throw new AuthException("Only change owner or server maintainer can reindex"); - } + protected Response<?> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input) + throws IOException, AuthException, OrmException, PermissionBackendException { + permissionBackend.user(user).check(GlobalPermission.MAINTAIN_SERVER); indexer.index(db.get(), rsrc.getChange()); return Response.none(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java index 27ec89d..ba2a10b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
@@ -19,6 +19,8 @@ import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -47,12 +49,18 @@ } @Override - public List<ReviewerInfo> apply(ChangeResource rsrc) throws OrmException { - Map<Account.Id, ReviewerResource> reviewers = new LinkedHashMap<>(); + public List<ReviewerInfo> apply(ChangeResource rsrc) + throws OrmException, PermissionBackendException { + Map<String, ReviewerResource> reviewers = new LinkedHashMap<>(); ReviewDb db = dbProvider.get(); for (Account.Id accountId : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) { - if (!reviewers.containsKey(accountId)) { - reviewers.put(accountId, resourceFactory.create(rsrc, accountId)); + if (!reviewers.containsKey(accountId.toString())) { + reviewers.put(accountId.toString(), resourceFactory.create(rsrc, accountId)); + } + } + for (Address adr : rsrc.getNotes().getReviewersByEmail().all()) { + if (!reviewers.containsKey(adr.toString())) { + reviewers.put(adr.toString(), new ReviewerResource(rsrc, adr)); } } return json.format(reviewers.values());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java index d0c8ca0..6d9dc79 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java
@@ -20,6 +20,8 @@ import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -49,16 +51,21 @@ @Override public List<ReviewerInfo> apply(RevisionResource rsrc) - throws OrmException, MethodNotAllowedException { + throws OrmException, MethodNotAllowedException, PermissionBackendException { if (!rsrc.isCurrent()) { throw new MethodNotAllowedException("Cannot list reviewers on non-current patch set"); } - Map<Account.Id, ReviewerResource> reviewers = new LinkedHashMap<>(); + Map<String, ReviewerResource> reviewers = new LinkedHashMap<>(); ReviewDb db = dbProvider.get(); for (Account.Id accountId : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) { - if (!reviewers.containsKey(accountId)) { - reviewers.put(accountId, resourceFactory.create(rsrc, accountId)); + if (!reviewers.containsKey(accountId.toString())) { + reviewers.put(accountId.toString(), resourceFactory.create(rsrc, accountId)); + } + } + for (Address address : rsrc.getNotes().getReviewersByEmail().all()) { + if (!reviewers.containsKey(address.toString())) { + reviewers.put(address.toString(), new ReviewerResource(rsrc, address)); } } return json.format(reviewers.values());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java index d67f8ce..119051e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -31,9 +31,7 @@ import com.google.gerrit.server.cache.CacheModule; import com.google.gerrit.server.git.CodeReviewCommit; import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; -import com.google.gerrit.server.git.IntegrationException; import com.google.gerrit.server.git.strategy.SubmitDryRun; -import com.google.gerrit.server.project.NoSuchProjectException; import com.google.inject.Inject; import com.google.inject.Module; import com.google.inject.Singleton; @@ -45,7 +43,6 @@ import java.util.Arrays; import java.util.Objects; import java.util.Set; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -173,31 +170,6 @@ } } - private class Loader implements Callable<Boolean> { - private final EntryKey key; - private final Branch.NameKey dest; - private final Repository repo; - - Loader(EntryKey key, Branch.NameKey dest, Repository repo) { - this.key = key; - this.dest = dest; - this.repo = repo; - } - - @Override - public Boolean call() throws NoSuchProjectException, IntegrationException, IOException { - if (key.into.equals(ObjectId.zeroId())) { - return true; // Assume yes on new branch. - } - try (CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) { - Set<RevCommit> accepted = SubmitDryRun.getAlreadyAccepted(repo, rw); - accepted.add(rw.parseCommit(key.into)); - accepted.addAll(Arrays.asList(rw.parseCommit(key.commit).getParents())); - return submitDryRun.run(key.submitType, repo, rw, dest, key.into, key.commit, accepted); - } - } - } - public static class MergeabilityWeigher implements Weigher<EntryKey, Boolean> { @Override public int weigh(EntryKey k, Boolean v) { @@ -229,7 +201,20 @@ ObjectId into = intoRef != null ? intoRef.getObjectId() : ObjectId.zeroId(); EntryKey key = new EntryKey(commit, into, submitType, mergeStrategy); try { - return cache.get(key, new Loader(key, dest, repo)); + return cache.get( + key, + () -> { + if (key.into.equals(ObjectId.zeroId())) { + return true; // Assume yes on new branch. + } + try (CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) { + Set<RevCommit> accepted = SubmitDryRun.getAlreadyAccepted(repo, rw); + accepted.add(rw.parseCommit(key.into)); + accepted.addAll(Arrays.asList(rw.parseCommit(key.commit).getParents())); + return submitDryRun.run( + key.submitType, repo, rw, dest, key.into, key.commit, accepted); + } + }); } catch (ExecutionException | UncheckedExecutionException e) { log.error( String.format(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java index e5f9352..06bb8d1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mergeable.java
@@ -25,6 +25,8 @@ import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDbUtil; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.git.BranchOrderSection; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MergeUtil; @@ -60,6 +62,7 @@ private boolean otherBranches; private final GitRepositoryManager gitManager; + private final AccountCache accountCache; private final ProjectCache projectCache; private final MergeUtil.Factory mergeUtilFactory; private final ChangeData.Factory changeDataFactory; @@ -70,6 +73,7 @@ @Inject Mergeable( GitRepositoryManager gitManager, + AccountCache accountCache, ProjectCache projectCache, MergeUtil.Factory mergeUtilFactory, ChangeData.Factory changeDataFactory, @@ -77,6 +81,7 @@ ChangeIndexer indexer, MergeabilityCache cache) { this.gitManager = gitManager; + this.accountCache = accountCache; this.projectCache = projectCache; this.mergeUtilFactory = mergeUtilFactory; this.changeDataFactory = changeDataFactory; @@ -98,7 +103,7 @@ MergeableInfo result = new MergeableInfo(); if (!change.getStatus().isOpen()) { - throw new ResourceConflictException("change is " + Submit.status(change)); + throw new ResourceConflictException("change is " + ChangeUtil.status(change)); } else if (!ps.getId().equals(change.currentPatchSetId())) { // Only the current revision is mergeable. Others always fail. return result; @@ -138,7 +143,8 @@ } private SubmitType getSubmitType(ChangeData cd, PatchSet patchSet) throws OrmException { - SubmitTypeRecord rec = new SubmitRuleEvaluator(cd).setPatchSet(patchSet).getSubmitType(); + SubmitTypeRecord rec = + new SubmitRuleEvaluator(accountCache, cd).setPatchSet(patchSet).getSubmitType(); if (rec.status != SubmitTypeRecord.Status.OK) { throw new OrmException("Submit type rule failed: " + rec); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java index aca6ef1..bf76af9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.server.change.CommentResource.COMMENT_KIND; import static com.google.gerrit.server.change.DraftCommentResource.DRAFT_COMMENT_KIND; import static com.google.gerrit.server.change.FileResource.FILE_KIND; +import static com.google.gerrit.server.change.FixResource.FIX_KIND; import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND; import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND; import static com.google.gerrit.server.change.RobotCommentResource.ROBOT_COMMENT_KIND; @@ -40,12 +41,14 @@ bind(DraftComments.class); bind(Comments.class); bind(RobotComments.class); + bind(Fixes.class); bind(Files.class); bind(Votes.class); DynamicMap.mapOf(binder(), CHANGE_KIND); DynamicMap.mapOf(binder(), COMMENT_KIND); DynamicMap.mapOf(binder(), ROBOT_COMMENT_KIND); + DynamicMap.mapOf(binder(), FIX_KIND); DynamicMap.mapOf(binder(), DRAFT_COMMENT_KIND); DynamicMap.mapOf(binder(), FILE_KIND); DynamicMap.mapOf(binder(), REVIEWER_KIND); @@ -82,6 +85,16 @@ post(CHANGE_KIND, "index").to(Index.class); post(CHANGE_KIND, "rebuild.notedb").to(Rebuild.class); post(CHANGE_KIND, "move").to(Move.class); + post(CHANGE_KIND, "private").to(PostPrivate.class); + post(CHANGE_KIND, "private.delete").to(DeletePrivateByPost.class); + delete(CHANGE_KIND, "private").to(DeletePrivate.class); + put(CHANGE_KIND, "ignore").to(Ignore.class); + put(CHANGE_KIND, "unignore").to(Unignore.class); + put(CHANGE_KIND, "mute").to(Mute.class); + put(CHANGE_KIND, "unmute").to(Unmute.class); + post(CHANGE_KIND, "wip").to(SetWorkInProgress.class); + post(CHANGE_KIND, "ready").to(SetReadyForReview.class); + put(CHANGE_KIND, "message").to(PutMessage.class); post(CHANGE_KIND, "reviewers").to(PostReviewers.class); get(CHANGE_KIND, "suggest_reviewers").to(SuggestChangeReviewers.class); @@ -125,9 +138,13 @@ child(REVISION_KIND, "comments").to(Comments.class); get(COMMENT_KIND).to(GetComment.class); + delete(COMMENT_KIND).to(DeleteComment.class); + post(COMMENT_KIND, "delete").to(DeleteComment.class); child(REVISION_KIND, "robotcomments").to(RobotComments.class); get(ROBOT_COMMENT_KIND).to(GetRobotComment.class); + child(REVISION_KIND, "fixes").to(Fixes.class); + post(FIX_KIND, "apply").to(ApplyFix.class); child(REVISION_KIND, "files").to(Files.class); put(FILE_KIND, "reviewed").to(PutReviewed.class); @@ -152,12 +169,16 @@ factory(ChangeEdits.Create.Factory.class); factory(ChangeEdits.DeleteFile.Factory.class); factory(ChangeInserter.Factory.class); + factory(ChangeResource.Factory.class); + factory(DeleteReviewerByEmailOp.Factory.class); + factory(DeleteReviewerOp.Factory.class); factory(EmailReviewComments.Factory.class); factory(PatchSetInserter.Factory.class); + factory(PostReviewersOp.Factory.class); factory(RebaseChangeOp.Factory.class); factory(ReviewerResource.Factory.class); factory(SetAssigneeOp.Factory.class); factory(SetHashtagsOp.Factory.class); - factory(ChangeResource.Factory.class); + factory(WorkInProgressOp.Factory.class); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java index fb6ae0b..ffc0dc4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
@@ -23,7 +23,6 @@ import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change.Status; @@ -33,14 +32,21 @@ import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.notedb.ChangeUpdate; -import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.permissions.ChangePermission; +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.query.change.InternalChangeQuery; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -54,50 +60,71 @@ import org.eclipse.jgit.revwalk.RevWalk; @Singleton -public class Move implements RestModifyView<ChangeResource, MoveInput> { +public class Move extends RetryingRestModifyView<ChangeResource, MoveInput, ChangeInfo> { + private final PermissionBackend permissionBackend; private final Provider<ReviewDb> dbProvider; private final ChangeJson.Factory json; private final GitRepositoryManager repoManager; private final Provider<InternalChangeQuery> queryProvider; private final ChangeMessagesUtil cmUtil; - private final BatchUpdate.Factory batchUpdateFactory; private final PatchSetUtil psUtil; @Inject Move( + PermissionBackend permissionBackend, Provider<ReviewDb> dbProvider, ChangeJson.Factory json, GitRepositoryManager repoManager, Provider<InternalChangeQuery> queryProvider, ChangeMessagesUtil cmUtil, - BatchUpdate.Factory batchUpdateFactory, + RetryHelper retryHelper, PatchSetUtil psUtil) { + super(retryHelper); + this.permissionBackend = permissionBackend; this.dbProvider = dbProvider; this.json = json; this.repoManager = repoManager; this.queryProvider = queryProvider; this.cmUtil = cmUtil; - this.batchUpdateFactory = batchUpdateFactory; this.psUtil = psUtil; } @Override - public ChangeInfo apply(ChangeResource req, MoveInput input) - throws RestApiException, OrmException, UpdateException { - ChangeControl control = req.getControl(); + protected ChangeInfo applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, MoveInput input) + throws RestApiException, OrmException, UpdateException, PermissionBackendException { + Change change = rsrc.getChange(); + Project.NameKey project = rsrc.getProject(); + IdentifiedUser caller = rsrc.getUser(); input.destinationBranch = RefNames.fullName(input.destinationBranch); - if (!control.canMoveTo(input.destinationBranch, dbProvider.get())) { - throw new AuthException("Move not permitted"); + + if (change.getStatus().isClosed()) { + throw new ResourceConflictException("Change is " + ChangeUtil.status(change)); + } + + Branch.NameKey newDest = new Branch.NameKey(project, input.destinationBranch); + if (change.getDest().equals(newDest)) { + throw new ResourceConflictException("Change is already destined for the specified branch"); + } + + // Move requires abandoning this change, and creating a new change. + try { + rsrc.permissions().database(dbProvider).check(ChangePermission.ABANDON); + permissionBackend + .user(caller) + .database(dbProvider) + .ref(newDest) + .check(RefPermission.CREATE_CHANGE); + } catch (AuthException denied) { + throw new AuthException("move not permitted", denied); } try (BatchUpdate u = - batchUpdateFactory.create( - dbProvider.get(), req.getChange().getProject(), control.getUser(), TimeUtil.nowTs())) { - u.addOp(req.getChange().getId(), new Op(input)); + updateFactory.create(dbProvider.get(), project, caller, TimeUtil.nowTs())) { + u.addOp(change.getId(), new Op(input)); u.execute(); } - - return json.noOptions().format(req.getChange()); + return json.noOptions().format(project, rsrc.getId()); } private class Op implements BatchUpdateOp { @@ -115,7 +142,7 @@ throws OrmException, ResourceConflictException, RepositoryNotFoundException, IOException { change = ctx.getChange(); if (change.getStatus() != Status.NEW && change.getStatus() != Status.DRAFT) { - throw new ResourceConflictException("Change is " + status(change)); + throw new ResourceConflictException("Change is " + ChangeUtil.status(change)); } Project.NameKey projectKey = change.getProject(); @@ -182,8 +209,4 @@ return true; } } - - private static String status(Change change) { - return change != null ? change.getStatus().name().toLowerCase() : "deleted"; - } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java new file mode 100644 index 0000000..d14fec8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Mute.java
@@ -0,0 +1,85 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.StarredChangesUtil; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class Mute implements RestModifyView<ChangeResource, Mute.Input>, UiAction<ChangeResource> { + private static final Logger log = LoggerFactory.getLogger(Mute.class); + + public static class Input {} + + private final Provider<IdentifiedUser> self; + private final StarredChangesUtil stars; + + @Inject + Mute(Provider<IdentifiedUser> self, StarredChangesUtil stars) { + this.self = self; + this.stars = stars; + } + + @Override + public Description getDescription(ChangeResource rsrc) { + return new UiAction.Description() + .setLabel("Mute") + .setTitle("Mute the change to unhighlight it in the dashboard") + .setVisible(!rsrc.isUserOwner() && isMuteable(rsrc.getChange())); + } + + @Override + public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException { + try { + if (rsrc.isUserOwner() || isMuted(rsrc.getChange())) { + // early exit for own changes and already muted changes + return Response.ok(""); + } + stars.mute(self.get().getAccountId(), rsrc.getProject(), rsrc.getChange()); + } catch (OrmException e) { + throw new RestApiException("failed to mute change", e); + } + return Response.ok(""); + } + + private boolean isMuted(Change change) { + try { + return stars.isMutedBy(change, self.get().getAccountId()); + } catch (OrmException e) { + log.error("failed to check muted star", e); + } + return false; + } + + private boolean isMuteable(Change change) { + try { + return !isMuted(change) && !stars.isIgnoredBy(change.getId(), self.get().getAccountId()); + } catch (OrmException e) { + log.error("failed to check ignored star", e); + } + return false; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java index 8516615..ccc7587 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/NotifyUtil.java
@@ -31,10 +31,12 @@ import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton public class NotifyUtil { @@ -76,7 +78,7 @@ public ListMultimap<RecipientType, Account.Id> resolveAccounts( @Nullable Map<RecipientType, NotifyInfo> notifyDetails) - throws OrmException, BadRequestException { + throws OrmException, BadRequestException, IOException, ConfigInvalidException { if (isNullOrEmpty(notifyDetails)) { return ImmutableListMultimap.of(); } @@ -96,7 +98,7 @@ } private List<Account.Id> find(ReviewDb db, List<String> nameOrEmails) - throws OrmException, BadRequestException { + throws OrmException, BadRequestException, IOException, ConfigInvalidException { List<String> missing = new ArrayList<>(nameOrEmails.size()); List<Account.Id> r = new ArrayList<>(nameOrEmails.size()); for (String nameOrEmail : nameOrEmails) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java index 7cf62a0..6463bed 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -34,6 +34,7 @@ import com.google.gerrit.server.ApprovalCopier; import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.events.CommitReceivedEvent; @@ -43,6 +44,9 @@ import com.google.gerrit.server.mail.send.ReplacePatchSetSender; import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.patch.PatchSetInfoFactory; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.ssh.NoSshInfo; import com.google.gerrit.server.update.BatchUpdateOp; @@ -50,13 +54,12 @@ import com.google.gerrit.server.update.Context; import com.google.gerrit.server.update.RepoContext; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.io.IOException; import java.util.Collections; import java.util.List; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.ReceiveCommand; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -65,10 +68,11 @@ private static final Logger log = LoggerFactory.getLogger(PatchSetInserter.class); public interface Factory { - PatchSetInserter create(ChangeControl ctl, PatchSet.Id psId, RevCommit commit); + PatchSetInserter create(ChangeControl ctl, PatchSet.Id psId, ObjectId commitId); } // Injected fields. + private final PermissionBackend permissionBackend; private final PatchSetInfoFactory patchSetInfoFactory; private final CommitValidators.Factory commitValidatorsFactory; private final ReplacePatchSetSender.Factory replacePatchSetFactory; @@ -80,7 +84,7 @@ // Assisted-injected fields. private final PatchSet.Id psId; - private final RevCommit commit; + private final ObjectId commitId; // Read prior to running the batch update, so must only be used during // updateRepo; updateChange and later must use the control from the // ChangeContext. @@ -89,7 +93,7 @@ // Fields exposed as setters. private String message; private String description; - private CommitValidators.Policy validatePolicy = CommitValidators.Policy.GERRIT; + private boolean validate = true; private boolean checkAddPatchSetPermission = true; private boolean draft; private List<String> groups = Collections.emptyList(); @@ -106,8 +110,9 @@ private ChangeMessage changeMessage; private ReviewerSet oldReviewers; - @AssistedInject + @Inject public PatchSetInserter( + PermissionBackend permissionBackend, ApprovalsUtil approvalsUtil, ApprovalCopier approvalCopier, ChangeMessagesUtil cmUtil, @@ -118,7 +123,8 @@ RevisionCreated revisionCreated, @Assisted ChangeControl ctl, @Assisted PatchSet.Id psId, - @Assisted RevCommit commit) { + @Assisted ObjectId commitId) { + this.permissionBackend = permissionBackend; this.approvalsUtil = approvalsUtil; this.approvalCopier = approvalCopier; this.cmUtil = cmUtil; @@ -130,7 +136,7 @@ this.origCtl = ctl; this.psId = psId; - this.commit = commit; + this.commitId = commitId.copy(); } public PatchSet.Id getPatchSetId() { @@ -147,8 +153,8 @@ return this; } - public PatchSetInserter setValidatePolicy(CommitValidators.Policy validate) { - this.validatePolicy = checkNotNull(validate); + public PatchSetInserter setValidate(boolean validate) { + this.validate = validate; return this; } @@ -206,11 +212,10 @@ @Override public void updateRepo(RepoContext ctx) - throws AuthException, ResourceConflictException, IOException, OrmException { + throws AuthException, ResourceConflictException, IOException, OrmException, + PermissionBackendException { validate(ctx); - ctx.addRefUpdate( - new ReceiveCommand( - ObjectId.zeroId(), commit, getPatchSetId().toRefName(), ReceiveCommand.Type.CREATE)); + ctx.addRefUpdate(ObjectId.zeroId(), commitId, getPatchSetId().toRefName()); } @Override @@ -227,7 +232,7 @@ throw new ResourceConflictException( String.format( "Cannot create new patch set of change %s because it is %s", - change.getId(), change.getStatus().name().toLowerCase())); + change.getId(), ChangeUtil.status(change))); } List<String> newGroups = groups; @@ -243,7 +248,7 @@ ctx.getRevWalk(), ctx.getUpdate(psId), psId, - commit, + commitId, draft, newGroups, null, @@ -264,13 +269,15 @@ changeMessage.setMessage(message); } - patchSetInfo = patchSetInfoFactory.get(ctx.getRevWalk(), commit, psId); + patchSetInfo = + patchSetInfoFactory.get(ctx.getRevWalk(), ctx.getRevWalk().parseCommit(commitId), psId); if (change.getStatus() != Change.Status.DRAFT && !allowClosed) { change.setStatus(Change.Status.NEW); } change.setCurrentPatchSet(patchSetInfo); if (copyApprovals) { - approvalCopier.copy(db, ctl, patchSet); + approvalCopier.copyInReviewDb( + db, ctl, patchSet, ctx.getRevWalk(), ctx.getRepoView().getConfig()); } if (changeMessage != null) { cmUtil.addChangeMessage(db, update, changeMessage); @@ -302,29 +309,35 @@ } private void validate(RepoContext ctx) - throws AuthException, ResourceConflictException, IOException, OrmException { - if (checkAddPatchSetPermission && !origCtl.canAddPatchSet(ctx.getDb())) { - throw new AuthException("cannot add patch set"); + throws AuthException, ResourceConflictException, IOException, PermissionBackendException { + if (checkAddPatchSetPermission) { + permissionBackend + .user(ctx.getUser()) + .database(ctx.getDb()) + .change(origCtl.getNotes()) + .check(ChangePermission.ADD_PATCH_SET); } - if (validatePolicy == CommitValidators.Policy.NONE) { + if (!validate) { return; } + PermissionBackend.ForRef perm = + permissionBackend.user(ctx.getUser()).ref(origCtl.getChange().getDest()); + String refName = getPatchSetId().toRefName(); - CommitReceivedEvent event = + try (CommitReceivedEvent event = new CommitReceivedEvent( new ReceiveCommand( ObjectId.zeroId(), - commit.getId(), + commitId, refName.substring(0, refName.lastIndexOf('/') + 1) + "new"), origCtl.getProjectControl().getProject(), origCtl.getRefControl().getRefName(), - commit, - ctx.getIdentifiedUser()); - - try { + ctx.getRevWalk().getObjectReader(), + commitId, + ctx.getIdentifiedUser())) { commitValidatorsFactory - .create(validatePolicy, origCtl.getRefControl(), new NoSshInfo(), ctx.getRepository()) + .forGerritCommits(perm, origCtl.getRefControl(), new NoSshInfo(), ctx.getRevWalk()) .validate(event); } catch (CommitValidationException e) { throw new ResourceConflictException(e.getFullMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java index 5aa41b1..0c8f010 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostHashtags.java
@@ -19,10 +19,13 @@ import com.google.gerrit.extensions.api.changes.HashtagsInput; 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.extensions.webui.UiAction; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -30,26 +33,28 @@ @Singleton public class PostHashtags - implements RestModifyView<ChangeResource, HashtagsInput>, UiAction<ChangeResource> { + extends RetryingRestModifyView< + ChangeResource, HashtagsInput, Response<ImmutableSortedSet<String>>> + implements UiAction<ChangeResource> { private final Provider<ReviewDb> db; - private final BatchUpdate.Factory batchUpdateFactory; private final SetHashtagsOp.Factory hashtagsFactory; @Inject PostHashtags( - Provider<ReviewDb> db, - BatchUpdate.Factory batchUpdateFactory, - SetHashtagsOp.Factory hashtagsFactory) { + Provider<ReviewDb> db, RetryHelper retryHelper, SetHashtagsOp.Factory hashtagsFactory) { + super(retryHelper); this.db = db; - this.batchUpdateFactory = batchUpdateFactory; this.hashtagsFactory = hashtagsFactory; } @Override - public Response<ImmutableSortedSet<String>> apply(ChangeResource req, HashtagsInput input) - throws RestApiException, UpdateException { + protected Response<ImmutableSortedSet<String>> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource req, HashtagsInput input) + throws RestApiException, UpdateException, PermissionBackendException { + req.permissions().check(ChangePermission.EDIT_HASHTAGS); + try (BatchUpdate bu = - batchUpdateFactory.create( + updateFactory.create( db.get(), req.getChange().getProject(), req.getControl().getUser(), TimeUtil.nowTs())) { SetHashtagsOp op = hashtagsFactory.create(input); bu.addOp(req.getId(), op); @@ -59,9 +64,9 @@ } @Override - public UiAction.Description getDescription(ChangeResource resource) { + public UiAction.Description getDescription(ChangeResource rsrc) { return new UiAction.Description() .setLabel("Edit Hashtags") - .setVisible(resource.getControl().canEditHashtags()); + .setVisible(rsrc.permissions().testOrFalse(ChangePermission.EDIT_HASHTAGS)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java new file mode 100644 index 0000000..771e669 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java
@@ -0,0 +1,96 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.TimeUtil; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; +import com.google.gerrit.server.update.UpdateException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +@Singleton +public class PostPrivate + extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, Response<String>> + implements UiAction<ChangeResource> { + private final ChangeMessagesUtil cmUtil; + private final Provider<ReviewDb> dbProvider; + private final PermissionBackend permissionBackend; + + @Inject + PostPrivate( + Provider<ReviewDb> dbProvider, + RetryHelper retryHelper, + ChangeMessagesUtil cmUtil, + PermissionBackend permissionBackend) { + super(retryHelper); + this.dbProvider = dbProvider; + this.cmUtil = cmUtil; + this.permissionBackend = permissionBackend; + } + + @Override + public Response<String> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, SetPrivateOp.Input input) + throws RestApiException, UpdateException { + if (!canSetPrivate(rsrc)) { + throw new AuthException("not allowed to mark private"); + } + + if (rsrc.getChange().isPrivate()) { + return Response.ok(""); + } + + ChangeControl control = rsrc.getControl(); + SetPrivateOp op = new SetPrivateOp(cmUtil, true, input); + try (BatchUpdate u = + updateFactory.create( + dbProvider.get(), + control.getProject().getNameKey(), + control.getUser(), + TimeUtil.nowTs())) { + u.addOp(control.getId(), op).execute(); + } + + return Response.created(""); + } + + @Override + public Description getDescription(ChangeResource rsrc) { + Change change = rsrc.getChange(); + return new UiAction.Description() + .setLabel("Mark private") + .setTitle("Mark change as private") + .setVisible(!change.isPrivate() && canSetPrivate(rsrc)); + } + + private boolean canSetPrivate(ChangeResource rsrc) { + PermissionBackend.WithUser user = permissionBackend.user(rsrc.getUser()); + return user.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER) + || (rsrc.isUserOwner() && rsrc.getChange().getStatus() != Change.Status.MERGED); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java index 76cc7e8..b11db15 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -18,7 +18,9 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.gerrit.server.CommentsUtil.setCommentRevId; import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER; +import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; @@ -27,7 +29,6 @@ import com.google.auto.value.AutoValue; import com.google.common.base.MoreObjects; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -38,8 +39,6 @@ import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; -import com.google.gerrit.common.data.Permission; -import com.google.gerrit.common.data.PermissionRange; import com.google.gerrit.extensions.api.changes.AddReviewerInput; import com.google.gerrit.extensions.api.changes.AddReviewerResult; import com.google.gerrit.extensions.api.changes.NotifyHandling; @@ -50,6 +49,7 @@ import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput; import com.google.gerrit.extensions.api.changes.ReviewResult; import com.google.gerrit.extensions.api.changes.ReviewerInfo; +import com.google.gerrit.extensions.client.Comment.Range; import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.client.Side; import com.google.gerrit.extensions.common.AccountInfo; @@ -61,7 +61,6 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException; 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.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.reviewdb.client.Account; @@ -81,28 +80,40 @@ import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.CommentsUtil; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.OutputFormat; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.account.AccountsCollection; +import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.extensions.events.CommentAdded; +import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.patch.PatchListCache; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.LabelPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.LabelVote; +import com.google.gson.Gson; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collection; @@ -113,16 +124,18 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.OptionalInt; import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; @Singleton -public class PostReview implements RestModifyView<RevisionResource, ReviewInput> { - private static final Logger log = LoggerFactory.getLogger(PostReview.class); +public class PostReview + extends RetryingRestModifyView<RevisionResource, ReviewInput, Response<ReviewResult>> { + private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson(); + private static final int DEFAULT_ROBOT_COMMENT_SIZE_LIMIT_IN_BYTES = 1024 * 1024; private final Provider<ReviewDb> db; - private final BatchUpdate.Factory batchUpdateFactory; private final ChangesCollection changes; private final ChangeData.Factory changeDataFactory; private final ApprovalsUtil approvalsUtil; @@ -136,11 +149,12 @@ private final PostReviewers postReviewers; private final NotesMigration migration; private final NotifyUtil notifyUtil; + private final Config gerritConfig; @Inject PostReview( Provider<ReviewDb> db, - BatchUpdate.Factory batchUpdateFactory, + RetryHelper retryHelper, ChangesCollection changes, ChangeData.Factory changeDataFactory, ApprovalsUtil approvalsUtil, @@ -153,9 +167,10 @@ CommentAdded commentAdded, PostReviewers postReviewers, NotesMigration migration, - NotifyUtil notifyUtil) { + NotifyUtil notifyUtil, + @GerritServerConfig Config gerritConfig) { + super(retryHelper); this.db = db; - this.batchUpdateFactory = batchUpdateFactory; this.changes = changes; this.changeDataFactory = changeDataFactory; this.commentsUtil = commentsUtil; @@ -169,16 +184,21 @@ this.postReviewers = postReviewers; this.migration = migration; this.notifyUtil = notifyUtil; + this.gerritConfig = gerritConfig; } @Override - public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input) - throws RestApiException, UpdateException, OrmException, IOException { - return apply(revision, input, TimeUtil.nowTs()); + protected Response<ReviewResult> applyImpl( + BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input) + throws RestApiException, UpdateException, OrmException, IOException, + PermissionBackendException, ConfigInvalidException { + return apply(updateFactory, revision, input, TimeUtil.nowTs()); } - public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input, Timestamp ts) - throws RestApiException, UpdateException, OrmException, IOException { + public Response<ReviewResult> apply( + BatchUpdate.Factory updateFactory, RevisionResource revision, ReviewInput input, Timestamp ts) + throws RestApiException, UpdateException, OrmException, IOException, + PermissionBackendException, ConfigInvalidException { // Respect timestamp, but truncate at change created-on time. ts = Ordering.natural().max(ts, revision.getChange().getCreatedOn()); if (revision.getEdit().isPresent()) { @@ -203,9 +223,9 @@ checkRobotComments(revision, input.robotComments); } + NotifyHandling reviewerNotify = input.notify; if (input.notify == null) { - log.warn("notify = null; assuming notify = NONE"); - input.notify = NotifyHandling.NONE; + input.notify = defaultNotify(revision.getChange(), input); } ListMultimap<RecipientType, Account.Id> accountsToNotify = @@ -244,8 +264,7 @@ output.labels = input.labels; try (BatchUpdate bu = - batchUpdateFactory.create( - db.get(), revision.getChange().getProject(), revision.getUser(), ts)) { + updateFactory.create(db.get(), revision.getChange().getProject(), revision.getUser(), ts)) { Account.Id id = revision.getUser().getAccountId(); boolean ccOrReviewer = false; if (input.labels != null && !input.labels.isEmpty()) { @@ -293,38 +312,63 @@ bu.addOp( revision.getChange().getId(), - new Op(revision.getPatchSet().getId(), input, accountsToNotify, reviewerResults)); + new Op(revision.getPatchSet().getId(), input, accountsToNotify)); bu.execute(); for (PostReviewers.Addition reviewerResult : reviewerResults) { reviewerResult.gatherResults(); } - emailReviewers(revision.getChange(), reviewerResults, input.notify, accountsToNotify); + emailReviewers(revision.getChange(), reviewerResults, reviewerNotify, accountsToNotify); } return Response.ok(output); } + private NotifyHandling defaultNotify(Change c, ReviewInput in) { + if (ChangeMessagesUtil.isAutogenerated(in.tag)) { + // Autogenerated comments default to lower notify levels. + return c.isWorkInProgress() ? NotifyHandling.OWNER : NotifyHandling.OWNER_REVIEWERS; + } + + if (c.isWorkInProgress() && !c.hasReviewStarted()) { + // If review hasn't started we want to minimize recipients, no matter who + // the author is. + return NotifyHandling.OWNER; + } + + return NotifyHandling.ALL; + } + private void emailReviewers( Change change, List<PostReviewers.Addition> reviewerAdditions, - NotifyHandling notify, + @Nullable NotifyHandling notify, ListMultimap<RecipientType, Account.Id> accountsToNotify) { List<Account.Id> to = new ArrayList<>(); List<Account.Id> cc = new ArrayList<>(); + List<Address> toByEmail = new ArrayList<>(); + List<Address> ccByEmail = new ArrayList<>(); for (PostReviewers.Addition addition : reviewerAdditions) { - if (addition.op.state == ReviewerState.REVIEWER) { - to.addAll(addition.op.reviewers.keySet()); - } else if (addition.op.state == ReviewerState.CC) { - cc.addAll(addition.op.reviewers.keySet()); + if (addition.state == ReviewerState.REVIEWER) { + to.addAll(addition.reviewers); + toByEmail.addAll(addition.reviewersByEmail); + } else if (addition.state == ReviewerState.CC) { + cc.addAll(addition.reviewers); + ccByEmail.addAll(addition.reviewersByEmail); } } - postReviewers.emailReviewers(change, to, cc, notify, accountsToNotify); + if (reviewerAdditions.size() > 0) { + reviewerAdditions + .get(0) + .op + .emailReviewers(change, to, cc, toByEmail, ccByEmail, notify, accountsToNotify); + } } private RevisionResource onBehalfOf(RevisionResource rev, ReviewInput in) - throws BadRequestException, AuthException, UnprocessableEntityException, OrmException { + throws BadRequestException, AuthException, UnprocessableEntityException, OrmException, + PermissionBackendException, IOException, ConfigInvalidException { if (in.labels == null || in.labels.isEmpty()) { throw new AuthException( String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf)); @@ -336,11 +380,13 @@ throw new AuthException("not allowed to modify other user's drafts"); } - ChangeControl caller = rev.getControl(); + CurrentUser caller = rev.getUser(); + PermissionBackend.ForChange perm = rev.permissions().database(db); + LabelTypes labelTypes = rev.getControl().getLabelTypes(); Iterator<Map.Entry<String, Short>> itr = in.labels.entrySet().iterator(); while (itr.hasNext()) { Map.Entry<String, Short> ent = itr.next(); - LabelType type = caller.getLabelTypes().byLabel(ent.getKey()); + LabelType type = labelTypes.byLabel(ent.getKey()); if (type == null && in.strictLabels) { throw new BadRequestException( String.format("label \"%s\" is not a configured label", ent.getKey())); @@ -349,16 +395,15 @@ continue; } - if (caller.getUser().isInternalUser()) { - continue; - } - - PermissionRange r = caller.getRange(Permission.forLabelAs(type.getName())); - if (r == null || r.isEmpty() || !r.contains(ent.getValue())) { - throw new AuthException( - String.format( - "not permitted to modify label \"%s\" on behalf of \"%s\"", - ent.getKey(), in.onBehalfOf)); + if (!caller.isInternalUser()) { + try { + perm.check(new LabelPermission.WithValue(ON_BEHALF_OF, type, ent.getValue())); + } catch (AuthException e) { + throw new AuthException( + String.format( + "not permitted to modify label \"%s\" on behalf of \"%s\"", + type.getName(), in.onBehalfOf)); + } } } if (in.labels.isEmpty()) { @@ -366,25 +411,26 @@ String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf)); } - ChangeControl target = - caller.forUser(accounts.parseOnBehalfOf(caller.getUser(), in.onBehalfOf)); - if (!target.getRefControl().isVisible()) { + IdentifiedUser reviewer = accounts.parseOnBehalfOf(caller, in.onBehalfOf); + try { + perm.user(reviewer).check(ChangePermission.READ); + } catch (AuthException e) { throw new UnprocessableEntityException( - String.format( - "on_behalf_of account %s cannot see destination ref", - target.getUser().getAccountId())); + String.format("on_behalf_of account %s cannot see change", reviewer.getAccountId())); } - return new RevisionResource(changes.parse(target), rev.getPatchSet()); + + ChangeControl ctl = rev.getControl().forUser(reviewer); + return new RevisionResource(changes.parse(ctl), rev.getPatchSet()); } - private void checkLabels(RevisionResource revision, boolean strict, Map<String, Short> labels) - throws BadRequestException, AuthException { - ChangeControl ctl = revision.getControl(); + private void checkLabels(RevisionResource rsrc, boolean strict, Map<String, Short> labels) + throws BadRequestException, AuthException, PermissionBackendException { + LabelTypes types = rsrc.getControl().getLabelTypes(); + PermissionBackend.ForChange perm = rsrc.permissions(); Iterator<Map.Entry<String, Short>> itr = labels.entrySet().iterator(); while (itr.hasNext()) { Map.Entry<String, Short> ent = itr.next(); - - LabelType lt = revision.getControl().getLabelTypes().byLabel(ent.getKey()); + LabelType lt = types.byLabel(ent.getKey()); if (lt == null) { if (strict) { throw new BadRequestException( @@ -409,23 +455,21 @@ continue; } - String name = lt.getName(); - PermissionRange range = ctl.getRange(Permission.forLabel(name)); - if (range == null || !range.contains(ent.getValue())) { + short val = ent.getValue(); + try { + perm.check(new LabelPermission.WithValue(lt, val)); + } catch (AuthException e) { if (strict) { throw new AuthException( - String.format( - "Applying label \"%s\": %d is restricted", ent.getKey(), ent.getValue())); - } else if (range == null || range.isEmpty()) { - ent.setValue((short) 0); - } else { - ent.setValue((short) range.squash(ent.getValue())); + String.format("Applying label \"%s\": %d is restricted", lt.getName(), val)); } + ent.setValue(perm.squashThenCheck(lt, val)); } } } - private <T extends CommentInput> void cleanUpComments(Map<String, List<T>> commentsPerPath) { + private static <T extends CommentInput> void cleanUpComments( + Map<String, List<T>> commentsPerPath) { Iterator<List<T>> mapValueIterator = commentsPerPath.values().iterator(); while (mapValueIterator.hasNext()) { List<T> comments = mapValueIterator.next(); @@ -441,7 +485,7 @@ } } - private <T extends CommentInput> void cleanUpComments(List<T> comments) { + private static <T extends CommentInput> void cleanUpComments(List<T> comments) { Iterator<T> commentsIterator = comments.iterator(); while (commentsIterator.hasNext()) { T comment = commentsIterator.next(); @@ -480,7 +524,7 @@ return new HashSet<>(changeData.filePaths(revision.getPatchSet())); } - private void ensurePathRefersToAvailableOrMagicFile( + private static void ensurePathRefersToAvailableOrMagicFile( String path, Set<String> availableFilePaths, PatchSet.Id patchSetId) throws BadRequestException { if (!availableFilePaths.contains(path) && !Patch.isMagic(path)) { @@ -489,14 +533,15 @@ } } - private void ensureLineIsNonNegative(Integer line, String path) throws BadRequestException { + 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 <T extends CommentInput> void ensureCommentNotOnMagicFilesOfAutoMerge( + private static <T extends CommentInput> 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)); @@ -510,6 +555,7 @@ for (Map.Entry<String, List<RobotCommentInput>> e : in.entrySet()) { String commentPath = e.getKey(); for (RobotCommentInput c : e.getValue()) { + ensureSizeOfJsonInputIsWithinBounds(c); ensureRobotIdIsSet(c.robotId, commentPath); ensureRobotRunIdIsSet(c.robotRunId, commentPath); ensureFixSuggestionsAreAddable(c.fixSuggestions, commentPath); @@ -518,14 +564,41 @@ checkComments(revision, in); } - private void ensureRobotIdIsSet(String robotId, String commentPath) throws BadRequestException { + private void ensureSizeOfJsonInputIsWithinBounds(RobotCommentInput robotCommentInput) + throws BadRequestException { + OptionalInt robotCommentSizeLimit = getRobotCommentSizeLimit(); + if (robotCommentSizeLimit.isPresent()) { + int sizeLimit = robotCommentSizeLimit.getAsInt(); + byte[] robotCommentBytes = GSON.toJson(robotCommentInput).getBytes(StandardCharsets.UTF_8); + int robotCommentSize = robotCommentBytes.length; + if (robotCommentSize > sizeLimit) { + throw new BadRequestException( + String.format( + "Size %d (bytes) of robot comment is greater than limit %d (bytes)", + robotCommentSize, sizeLimit)); + } + } + } + + private OptionalInt getRobotCommentSizeLimit() { + int robotCommentSizeLimit = + gerritConfig.getInt( + "change", "robotCommentSizeLimit", DEFAULT_ROBOT_COMMENT_SIZE_LIMIT_IN_BYTES); + if (robotCommentSizeLimit <= 0) { + return OptionalInt.empty(); + } + return OptionalInt.of(robotCommentSizeLimit); + } + + 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)); } } - private void ensureRobotRunIdIsSet(String robotRunId, String commentPath) + private static void ensureRobotRunIdIsSet(String robotRunId, String commentPath) throws BadRequestException { if (robotRunId == null) { throw new BadRequestException( @@ -533,7 +606,7 @@ } } - private void ensureFixSuggestionsAreAddable( + private static void ensureFixSuggestionsAreAddable( List<FixSuggestionInfo> fixSuggestionInfos, String commentPath) throws BadRequestException { if (fixSuggestionInfos == null) { return; @@ -545,7 +618,7 @@ } } - private void ensureDescriptionIsSet(String commentPath, String description) + private static void ensureDescriptionIsSet(String commentPath, String description) throws BadRequestException { if (description == null) { throw new BadRequestException( @@ -555,20 +628,25 @@ } } - private void ensureFixReplacementsAreAddable( + private static void ensureFixReplacementsAreAddable( String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException { ensureReplacementsArePresent(commentPath, fixReplacementInfos); for (FixReplacementInfo fixReplacementInfo : fixReplacementInfos) { ensureReplacementPathIsSet(commentPath, fixReplacementInfo.path); - ensureReplacementPathRefersToFileOfComment(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 void ensureReplacementsArePresent( + private static void ensureReplacementsArePresent( String commentPath, List<FixReplacementInfo> fixReplacementInfos) throws BadRequestException { if (fixReplacementInfos == null || fixReplacementInfos.isEmpty()) { throw new BadRequestException( @@ -579,7 +657,7 @@ } } - private void ensureReplacementPathIsSet(String commentPath, String replacementPath) + private static void ensureReplacementPathIsSet(String commentPath, String replacementPath) throws BadRequestException { if (replacementPath == null) { throw new BadRequestException( @@ -589,20 +667,7 @@ } } - private void ensureReplacementPathRefersToFileOfComment( - String commentPath, String replacementPath) throws BadRequestException { - if (!Objects.equals(commentPath, replacementPath)) { - throw new BadRequestException( - String.format( - "Replacements may only be " - + "specified for the file %s on which the robot comment was added", - commentPath)); - } - } - - private void ensureRangeIsSet( - String commentPath, com.google.gerrit.extensions.client.Comment.Range range) - throws BadRequestException { + private static void ensureRangeIsSet(String commentPath, Range range) throws BadRequestException { if (range == null) { throw new BadRequestException( String.format( @@ -610,8 +675,7 @@ } } - private void ensureRangeIsValid( - String commentPath, com.google.gerrit.extensions.client.Comment.Range range) + private static void ensureRangeIsValid(String commentPath, Range range) throws BadRequestException { if (range == null) { return; @@ -628,7 +692,7 @@ } } - private void ensureReplacementStringIsSet(String commentPath, String replacement) + private static void ensureReplacementStringIsSet(String commentPath, String replacement) throws BadRequestException { if (replacement == null) { throw new BadRequestException( @@ -639,6 +703,28 @@ } } + 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; + } + } + /** Used to compare Comments with CommentInput comments. */ @AutoValue abstract static class CommentSetEntry { @@ -659,7 +745,7 @@ comment.key.patchSetId, comment.lineNbr, Side.fromShort(comment.side), - Hashing.sha1().hashString(comment.message, UTF_8), + Hashing.murmur3_128().hashString(comment.message, UTF_8), comment.range); } @@ -682,7 +768,6 @@ private final PatchSet.Id psId; private final ReviewInput in; private final ListMultimap<RecipientType, Account.Id> accountsToNotify; - private final List<PostReviewers.Addition> reviewerResults; private IdentifiedUser user; private ChangeNotes notes; @@ -696,17 +781,15 @@ private Op( PatchSet.Id psId, ReviewInput in, - ListMultimap<RecipientType, Account.Id> accountsToNotify, - List<PostReviewers.Addition> reviewerResults) { + ListMultimap<RecipientType, Account.Id> accountsToNotify) { this.psId = psId; this.in = in; this.accountsToNotify = checkNotNull(accountsToNotify); - this.reviewerResults = reviewerResults; } @Override public boolean updateChange(ChangeContext ctx) - throws OrmException, ResourceConflictException, UnprocessableEntityException { + throws OrmException, ResourceConflictException, UnprocessableEntityException, IOException { user = ctx.getIdentifiedUser(); notes = ctx.getNotes(); ps = psUtil.get(ctx.getDb(), ctx.getNotes(), psId); @@ -801,12 +884,9 @@ toDel.addAll(drafts.values()); break; case PUBLISH: - for (Comment e : drafts.values()) { - toPublish.add(publishComment(ctx, e, ps)); - } - break; case PUBLISH_ALL_REVISIONS: - publishAllRevisions(ctx, drafts, toPublish); + commentsUtil.publish(ctx, psId, drafts.values(), in.tag); + comments.addAll(drafts.values()); break; } ChangeUpdate u = ctx.getUpdate(psId); @@ -939,37 +1019,6 @@ return labels; } - private Comment publishComment(ChangeContext ctx, Comment c, PatchSet ps) throws OrmException { - c.writtenOn = ctx.getWhen(); - c.tag = in.tag; - // Draft may have been created by a different real user; copy the current - // real user. (Only applies to X-Gerrit-RunAs, since modifying drafts via - // on_behalf_of is not allowed.) - ctx.getUser().updateRealAccountId(c::setRealAuthor); - setCommentRevId(c, patchListCache, ctx.getChange(), checkNotNull(ps)); - return c; - } - - private void publishAllRevisions( - ChangeContext ctx, Map<String, Comment> drafts, List<Comment> ups) throws OrmException { - boolean needOtherPatchSets = false; - for (Comment c : drafts.values()) { - if (c.key.patchSetId != psId.get()) { - needOtherPatchSets = true; - break; - } - } - Map<PatchSet.Id, PatchSet> patchSets = - needOtherPatchSets - ? psUtil.byChangeAsMap(ctx.getDb(), ctx.getNotes()) - : ImmutableMap.of(psId, ps); - for (Comment e : drafts.values()) { - ups.add( - publishComment( - ctx, e, patchSets.get(new PatchSet.Id(ctx.getChange().getId(), e.key.patchSetId)))); - } - } - private Map<String, Short> getAllApprovals( LabelTypes labelTypes, Map<String, Short> current, Map<String, Short> input) { Map<String, Short> allApprovals = new HashMap<>(); @@ -1005,16 +1054,6 @@ if (ctx.getAccountId().equals(ctx.getChange().getOwner())) { return true; } - for (PostReviewers.Addition addition : reviewerResults) { - if (addition.op.addedReviewers == null) { - continue; - } - for (PatchSetApproval psa : addition.op.addedReviewers) { - if (psa.getAccountId().equals(ctx.getAccountId())) { - return true; - } - } - } ChangeData cd = changeDataFactory.create(db.get(), ctx.getControl()); ReviewerSet reviewers = cd.reviewers(); if (reviewers.byState(REVIEWER).contains(ctx.getAccountId())) { @@ -1023,7 +1062,8 @@ return false; } - private boolean updateLabels(ChangeContext ctx) throws OrmException, ResourceConflictException { + private boolean updateLabels(ChangeContext ctx) + throws OrmException, ResourceConflictException, IOException { Map<String, Short> inLabels = MoreObjects.firstNonNull(in.labels, Collections.<String, Short>emptyMap()); @@ -1203,12 +1243,18 @@ } private Map<String, PatchSetApproval> scanLabels(ChangeContext ctx, List<PatchSetApproval> del) - throws OrmException { + throws OrmException, IOException { LabelTypes labelTypes = ctx.getControl().getLabelTypes(); Map<String, PatchSetApproval> current = new HashMap<>(); for (PatchSetApproval a : - approvalsUtil.byPatchSetUser(ctx.getDb(), ctx.getControl(), psId, user.getAccountId())) { + approvalsUtil.byPatchSetUser( + ctx.getDb(), + ctx.getControl(), + psId, + user.getAccountId(), + ctx.getRevWalk(), + ctx.getRepoView().getConfig())) { if (a.isLegacySubmit()) { continue; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java index 7031d51..a7711b6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -14,15 +14,16 @@ package com.google.gerrit.server.change; -import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.gerrit.extensions.client.ReviewerState.CC; import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER; 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.ListMultimap; import com.google.common.collect.Lists; +import com.google.gerrit.common.Nullable; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.GroupDescription; import com.google.gerrit.common.errors.NoSuchGroupException; @@ -32,36 +33,39 @@ import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.api.changes.ReviewerInfo; import com.google.gerrit.extensions.client.ReviewerState; +import com.google.gerrit.extensions.common.AccountInfo; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.ApprovalsUtil; +import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.PatchSetUtil; -import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountLoader; import com.google.gerrit.server.account.AccountsCollection; import com.google.gerrit.server.account.GroupMembers; import com.google.gerrit.server.config.GerritServerConfig; -import com.google.gerrit.server.extensions.events.ReviewerAdded; import com.google.gerrit.server.group.GroupsCollection; import com.google.gerrit.server.group.SystemGroupBackend; -import com.google.gerrit.server.mail.send.AddReviewerSender; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.send.OutgoingEmailValidator; +import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.NotesMigration; +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.ChangeControl; import com.google.gerrit.server.project.NoSuchProjectException; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.update.BatchUpdate; -import com.google.gerrit.server.update.BatchUpdateOp; -import com.google.gerrit.server.update.ChangeContext; -import com.google.gerrit.server.update.Context; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -69,85 +73,83 @@ import com.google.inject.Singleton; import java.io.IOException; import java.text.MessageFormat; -import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.HashSet; import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Singleton -public class PostReviewers implements RestModifyView<ChangeResource, AddReviewerInput> { - private static final Logger log = LoggerFactory.getLogger(PostReviewers.class); +public class PostReviewers + extends RetryingRestModifyView<ChangeResource, AddReviewerInput, AddReviewerResult> { public static final int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10; public static final int DEFAULT_MAX_REVIEWERS = 20; private final AccountsCollection accounts; private final ReviewerResource.Factory reviewerFactory; - private final ApprovalsUtil approvalsUtil; - private final PatchSetUtil psUtil; - private final AddReviewerSender.Factory addReviewerSenderFactory; + private final PermissionBackend permissionBackend; + private final GroupsCollection groupsCollection; private final GroupMembers.Factory groupMembersFactory; private final AccountLoader.Factory accountLoaderFactory; private final Provider<ReviewDb> dbProvider; - private final BatchUpdate.Factory batchUpdateFactory; - private final Provider<IdentifiedUser> user; + private final ChangeData.Factory changeDataFactory; private final IdentifiedUser.GenericFactory identifiedUserFactory; private final Config cfg; private final ReviewerJson json; - private final ReviewerAdded reviewerAdded; private final NotesMigration migration; - private final AccountCache accountCache; private final NotifyUtil notifyUtil; + private final ProjectCache projectCache; + private final Provider<AnonymousUser> anonymousProvider; + private final PostReviewersOp.Factory postReviewersOpFactory; + private final OutgoingEmailValidator validator; @Inject PostReviewers( AccountsCollection accounts, ReviewerResource.Factory reviewerFactory, - ApprovalsUtil approvalsUtil, - PatchSetUtil psUtil, - AddReviewerSender.Factory addReviewerSenderFactory, + PermissionBackend permissionBackend, GroupsCollection groupsCollection, GroupMembers.Factory groupMembersFactory, AccountLoader.Factory accountLoaderFactory, Provider<ReviewDb> db, - BatchUpdate.Factory batchUpdateFactory, - Provider<IdentifiedUser> user, + ChangeData.Factory changeDataFactory, + RetryHelper retryHelper, IdentifiedUser.GenericFactory identifiedUserFactory, @GerritServerConfig Config cfg, ReviewerJson json, - ReviewerAdded reviewerAdded, NotesMigration migration, - AccountCache accountCache, - NotifyUtil notifyUtil) { + NotifyUtil notifyUtil, + ProjectCache projectCache, + Provider<AnonymousUser> anonymousProvider, + PostReviewersOp.Factory postReviewersOpFactory, + OutgoingEmailValidator validator) { + super(retryHelper); this.accounts = accounts; this.reviewerFactory = reviewerFactory; - this.approvalsUtil = approvalsUtil; - this.psUtil = psUtil; - this.addReviewerSenderFactory = addReviewerSenderFactory; + this.permissionBackend = permissionBackend; this.groupsCollection = groupsCollection; this.groupMembersFactory = groupMembersFactory; this.accountLoaderFactory = accountLoaderFactory; this.dbProvider = db; - this.batchUpdateFactory = batchUpdateFactory; - this.user = user; + this.changeDataFactory = changeDataFactory; this.identifiedUserFactory = identifiedUserFactory; this.cfg = cfg; this.json = json; - this.reviewerAdded = reviewerAdded; this.migration = migration; - this.accountCache = accountCache; this.notifyUtil = notifyUtil; + this.projectCache = projectCache; + this.anonymousProvider = anonymousProvider; + this.postReviewersOpFactory = postReviewersOpFactory; + this.validator = validator; } @Override - public AddReviewerResult apply(ChangeResource rsrc, AddReviewerInput input) - throws IOException, OrmException, RestApiException, UpdateException { + protected AddReviewerResult applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, AddReviewerInput input) + throws IOException, OrmException, RestApiException, UpdateException, + PermissionBackendException, ConfigInvalidException { if (input.reviewer == null) { throw new BadRequestException("missing reviewer field"); } @@ -157,7 +159,7 @@ return addition.result; } try (BatchUpdate bu = - batchUpdateFactory.create( + updateFactory.create( dbProvider.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { Change.Id id = rsrc.getChange().getId(); bu.addOp(id, addition.op); @@ -169,75 +171,120 @@ public Addition prepareApplication( ChangeResource rsrc, AddReviewerInput input, boolean allowGroup) - throws OrmException, RestApiException, IOException { - Account.Id accountId; + throws OrmException, IOException, PermissionBackendException, ConfigInvalidException { + String reviewer = input.reviewer; + ReviewerState state = input.state(); + NotifyHandling notify = input.notify; + ListMultimap<RecipientType, Account.Id> accountsToNotify = null; try { - accountId = accounts.parse(input.reviewer).getAccountId(); - } catch (UnprocessableEntityException e) { - if (allowGroup) { - try { - return putGroup(rsrc, input); - } catch (UnprocessableEntityException e2) { - throw new UnprocessableEntityException( - MessageFormat.format( - ChangeMessages.get().reviewerNotFoundUserOrGroup, input.reviewer)); - } - } - throw new UnprocessableEntityException( - MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, input.reviewer)); + accountsToNotify = notifyUtil.resolveAccounts(input.notifyDetails); + } catch (BadRequestException e) { + return fail(reviewer, e.getMessage()); } - return putAccount( - input.reviewer, - reviewerFactory.create(rsrc, accountId), - input.state(), - input.notify, - notifyUtil.resolveAccounts(input.notifyDetails)); + boolean confirmed = input.confirmed(); + boolean allowByEmail = projectCache.checkedGet(rsrc.getProject()).isEnableReviewerByEmail(); + + Addition byAccountId = + addByAccountId(reviewer, rsrc, state, notify, accountsToNotify, allowGroup, allowByEmail); + if (byAccountId != null) { + return byAccountId; + } + + Addition wholeGroup = + addWholeGroup( + reviewer, rsrc, state, notify, accountsToNotify, confirmed, allowGroup, allowByEmail); + if (wholeGroup != null) { + return wholeGroup; + } + + return addByEmail(reviewer, rsrc, state, notify, accountsToNotify); } Addition ccCurrentUser(CurrentUser user, RevisionResource revision) { return new Addition( user.getUserName(), revision.getChangeResource(), - ImmutableMap.of(user.getAccountId(), revision.getControl()), + ImmutableSet.of(user.getAccountId()), + null, CC, NotifyHandling.NONE, ImmutableListMultimap.of()); } - private Addition putAccount( + @Nullable + private Addition addByAccountId( String reviewer, - ReviewerResource rsrc, + ChangeResource rsrc, ReviewerState state, NotifyHandling notify, - ListMultimap<RecipientType, Account.Id> accountsToNotify) - throws UnprocessableEntityException { - Account member = rsrc.getReviewerUser().getAccount(); - ChangeControl control = rsrc.getReviewerControl(); - if (isValidReviewer(member, control)) { + ListMultimap<RecipientType, Account.Id> accountsToNotify, + boolean allowGroup, + boolean allowByEmail) + throws OrmException, PermissionBackendException, IOException, ConfigInvalidException { + Account.Id accountId = null; + try { + accountId = accounts.parse(reviewer).getAccountId(); + } catch (UnprocessableEntityException | AuthException e) { + // AuthException won't occur since the user is authenticated at this point. + if (!allowGroup && !allowByEmail) { + // Only return failure if we aren't going to try other interpretations. + return fail( + reviewer, MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, reviewer)); + } + return null; + } + + ReviewerResource rrsrc = reviewerFactory.create(rsrc, accountId); + Account member = rrsrc.getReviewerUser().getAccount(); + PermissionBackend.ForRef perm = + permissionBackend.user(rrsrc.getReviewerUser()).ref(rrsrc.getChange().getDest()); + if (isValidReviewer(member, perm)) { return new Addition( - reviewer, - rsrc.getChangeResource(), - ImmutableMap.of(member.getId(), control), - state, - notify, - accountsToNotify); + reviewer, rsrc, ImmutableSet.of(member.getId()), null, state, notify, accountsToNotify); } - if (member.isActive()) { - throw new UnprocessableEntityException(String.format("Change not visible to %s", reviewer)); + if (!member.isActive()) { + if (allowByEmail && state == CC) { + return null; + } + return fail(reviewer, MessageFormat.format(ChangeMessages.get().reviewerInactive, reviewer)); } - throw new UnprocessableEntityException(String.format("Account of %s is inactive.", reviewer)); + return fail( + reviewer, MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, reviewer)); } - private Addition putGroup(ChangeResource rsrc, AddReviewerInput input) - throws RestApiException, OrmException, IOException { - GroupDescription.Basic group = groupsCollection.parseInternal(input.reviewer); - if (!isLegalReviewerGroup(group.getGroupUUID())) { - return fail( - input.reviewer, - MessageFormat.format(ChangeMessages.get().groupIsNotAllowed, group.getName())); + @Nullable + private Addition addWholeGroup( + String reviewer, + ChangeResource rsrc, + ReviewerState state, + NotifyHandling notify, + ListMultimap<RecipientType, Account.Id> accountsToNotify, + boolean confirmed, + boolean allowGroup, + boolean allowByEmail) + throws OrmException, IOException, PermissionBackendException { + if (!allowGroup) { + return null; } - Map<Account.Id, ChangeControl> reviewers = new HashMap<>(); + GroupDescription.Basic group = null; + try { + group = groupsCollection.parseInternal(reviewer); + } catch (UnprocessableEntityException e) { + if (!allowByEmail) { + return fail( + reviewer, + MessageFormat.format(ChangeMessages.get().reviewerNotFoundUserOrGroup, reviewer)); + } + return null; + } + + if (!isLegalReviewerGroup(group.getGroupUUID())) { + return fail( + reviewer, MessageFormat.format(ChangeMessages.get().groupIsNotAllowed, group.getName())); + } + + Set<Account.Id> reviewers = new HashSet<>(); ChangeControl control = rsrc.getControl(); Set<Account> members; try { @@ -246,9 +293,11 @@ .create(control.getUser()) .listAccounts(group.getGroupUUID(), control.getProject().getNameKey()); } catch (NoSuchGroupException e) { - throw new UnprocessableEntityException(e.getMessage()); + return fail( + reviewer, + MessageFormat.format(ChangeMessages.get().reviewerNotFoundUserOrGroup, group.getName())); } catch (NoSuchProjectException e) { - throw new BadRequestException(e.getMessage()); + return fail(reviewer, e.getMessage()); } // if maxAllowed is set to 0, it is allowed to add any number of @@ -256,44 +305,69 @@ int maxAllowed = cfg.getInt("addreviewer", "maxAllowed", DEFAULT_MAX_REVIEWERS); if (maxAllowed > 0 && members.size() > maxAllowed) { return fail( - input.reviewer, + reviewer, MessageFormat.format(ChangeMessages.get().groupHasTooManyMembers, group.getName())); } // if maxWithoutCheck is set to 0, we never ask for confirmation int maxWithoutConfirmation = cfg.getInt("addreviewer", "maxWithoutConfirmation", DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK); - if (!input.confirmed() - && maxWithoutConfirmation > 0 - && members.size() > maxWithoutConfirmation) { + if (!confirmed && maxWithoutConfirmation > 0 && members.size() > maxWithoutConfirmation) { return fail( - input.reviewer, + reviewer, true, MessageFormat.format( ChangeMessages.get().groupManyMembersConfirmation, group.getName(), members.size())); } + PermissionBackend.ForRef perm = + permissionBackend.user(rsrc.getUser()).ref(rsrc.getChange().getDest()); for (Account member : members) { - if (isValidReviewer(member, control)) { - reviewers.put(member.getId(), control); + if (isValidReviewer(member, perm)) { + reviewers.add(member.getId()); } } - return new Addition( - input.reviewer, - rsrc, - reviewers, - input.state(), - input.notify, - notifyUtil.resolveAccounts(input.notifyDetails)); + return new Addition(reviewer, rsrc, reviewers, null, state, notify, accountsToNotify); } - private boolean isValidReviewer(Account member, ChangeControl control) { + @Nullable + private Addition addByEmail( + String reviewer, + ChangeResource rsrc, + ReviewerState state, + NotifyHandling notify, + ListMultimap<RecipientType, Account.Id> accountsToNotify) + throws OrmException { + if (!rsrc.getControl().forUser(anonymousProvider.get()).isVisible(dbProvider.get())) { + return fail( + reviewer, MessageFormat.format(ChangeMessages.get().reviewerCantSeeChange, reviewer)); + } + if (!migration.readChanges()) { + // addByEmail depends on NoteDb. + return fail( + reviewer, MessageFormat.format(ChangeMessages.get().reviewerNotFoundUser, reviewer)); + } + Address adr = Address.tryParse(reviewer); + if (adr == null || !validator.isValid(adr.getEmail())) { + return fail(reviewer, MessageFormat.format(ChangeMessages.get().reviewerInvalid, reviewer)); + } + return new Addition( + reviewer, rsrc, null, ImmutableList.of(adr), state, notify, accountsToNotify); + } + + private boolean isValidReviewer(Account member, PermissionBackend.ForRef perm) + throws PermissionBackendException { if (member.isActive()) { IdentifiedUser user = identifiedUserFactory.create(member.getId()); // Does not account for draft status as a user might want to let a // reviewer see a draft. - return control.forUser(user).isRefVisible(); + try { + perm.user(user).check(RefPermission.READ); + return true; + } catch (AuthException e) { + return false; + } } return false; } @@ -311,177 +385,87 @@ public class Addition { final AddReviewerResult result; - final Op op; + final PostReviewersOp op; + final Set<Account.Id> reviewers; + final Collection<Address> reviewersByEmail; + final ReviewerState state; + final ChangeNotes notes; + final IdentifiedUser caller; - private final Map<Account.Id, ChangeControl> reviewers; - - protected Addition(String reviewer) { - this(reviewer, null, null, REVIEWER, null, ImmutableListMultimap.of()); + Addition(String reviewer) { + result = new AddReviewerResult(reviewer); + op = null; + reviewers = ImmutableSet.of(); + reviewersByEmail = ImmutableSet.of(); + state = REVIEWER; + notes = null; + caller = null; } protected Addition( String reviewer, ChangeResource rsrc, - Map<Account.Id, ChangeControl> reviewers, + @Nullable Set<Account.Id> reviewers, + @Nullable Collection<Address> reviewersByEmail, ReviewerState state, - NotifyHandling notify, + @Nullable NotifyHandling notify, ListMultimap<RecipientType, Account.Id> accountsToNotify) { + checkArgument( + reviewers != null || reviewersByEmail != null, + "must have either reviewers or reviewersByEmail"); + result = new AddReviewerResult(reviewer); - if (reviewers == null) { - this.reviewers = ImmutableMap.of(); - op = null; - return; - } - this.reviewers = reviewers; - op = new Op(rsrc, reviewers, state, notify, accountsToNotify); + this.reviewers = reviewers == null ? ImmutableSet.of() : reviewers; + this.reviewersByEmail = reviewersByEmail == null ? ImmutableList.of() : reviewersByEmail; + this.state = state; + notes = rsrc.getNotes(); + caller = rsrc.getUser(); + op = + postReviewersOpFactory.create( + rsrc, this.reviewers, this.reviewersByEmail, state, notify, accountsToNotify); } - void gatherResults() throws OrmException { + void gatherResults() throws OrmException, PermissionBackendException { + if (notes == null || caller == null) { + // When notes or caller is missing this is likely just carrying an error message + // in the contained AddReviewerResult. + return; + } + + ChangeData cd = changeDataFactory.create(dbProvider.get(), notes); + PermissionBackend.ForChange perm = + permissionBackend.user(caller).database(dbProvider).change(cd); + // Generate result details and fill AccountLoader. This occurs outside // the Op because the accounts are in a different table. - if (migration.readChanges() && op.state == CC) { - result.ccs = Lists.newArrayListWithCapacity(op.addedCCs.size()); - for (Account.Id accountId : op.addedCCs) { - result.ccs.add(json.format(new ReviewerInfo(accountId.get()), reviewers.get(accountId))); + PostReviewersOp.Result opResult = op.getResult(); + if (migration.readChanges() && state == CC) { + result.ccs = Lists.newArrayListWithCapacity(opResult.addedCCs().size()); + for (Account.Id accountId : opResult.addedCCs()) { + IdentifiedUser u = identifiedUserFactory.create(accountId); + result.ccs.add(json.format(new ReviewerInfo(accountId.get()), perm.user(u), cd)); } accountLoaderFactory.create(true).fill(result.ccs); + for (Address a : reviewersByEmail) { + result.ccs.add(new AccountInfo(a.getName(), a.getEmail())); + } } else { - result.reviewers = Lists.newArrayListWithCapacity(op.addedReviewers.size()); - for (PatchSetApproval psa : op.addedReviewers) { + result.reviewers = Lists.newArrayListWithCapacity(opResult.addedReviewers().size()); + for (PatchSetApproval psa : opResult.addedReviewers()) { // New reviewers have value 0, don't bother normalizing. + IdentifiedUser u = identifiedUserFactory.create(psa.getAccountId()); result.reviewers.add( json.format( new ReviewerInfo(psa.getAccountId().get()), - reviewers.get(psa.getAccountId()), + perm.user(u), + cd, ImmutableList.of(psa))); } accountLoaderFactory.create(true).fill(result.reviewers); - } - } - } - - public class Op implements BatchUpdateOp { - final Map<Account.Id, ChangeControl> reviewers; - final ReviewerState state; - final NotifyHandling notify; - final ListMultimap<RecipientType, Account.Id> accountsToNotify; - List<PatchSetApproval> addedReviewers; - Collection<Account.Id> addedCCs; - - private final ChangeResource rsrc; - private PatchSet patchSet; - - Op( - ChangeResource rsrc, - Map<Account.Id, ChangeControl> reviewers, - ReviewerState state, - NotifyHandling notify, - ListMultimap<RecipientType, Account.Id> accountsToNotify) { - this.rsrc = rsrc; - this.reviewers = reviewers; - this.state = state; - this.notify = notify; - this.accountsToNotify = checkNotNull(accountsToNotify); - } - - @Override - public boolean updateChange(ChangeContext ctx) - throws RestApiException, OrmException, IOException { - if (migration.readChanges() && state == CC) { - addedCCs = - approvalsUtil.addCcs( - ctx.getNotes(), - ctx.getUpdate(ctx.getChange().currentPatchSetId()), - reviewers.keySet()); - if (addedCCs.isEmpty()) { - return false; - } - } else { - addedReviewers = - approvalsUtil.addReviewers( - ctx.getDb(), - ctx.getNotes(), - ctx.getUpdate(ctx.getChange().currentPatchSetId()), - rsrc.getControl().getLabelTypes(), - rsrc.getChange(), - reviewers.keySet()); - if (addedReviewers.isEmpty()) { - return false; + for (Address a : reviewersByEmail) { + result.reviewers.add(ReviewerInfo.byEmail(a.getName(), a.getEmail())); } } - - patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes()); - return true; - } - - @Override - public void postUpdate(Context ctx) throws Exception { - if (addedReviewers != null || addedCCs != null) { - if (addedReviewers == null) { - addedReviewers = new ArrayList<>(); - } - if (addedCCs == null) { - addedCCs = new ArrayList<>(); - } - emailReviewers( - rsrc.getChange(), - Lists.transform(addedReviewers, r -> r.getAccountId()), - addedCCs, - notify, - accountsToNotify); - if (!addedReviewers.isEmpty()) { - List<Account> reviewers = - Lists.transform( - addedReviewers, psa -> accountCache.get(psa.getAccountId()).getAccount()); - reviewerAdded.fire( - rsrc.getChange(), patchSet, reviewers, ctx.getAccount(), ctx.getWhen()); - } - } - } - } - - public void emailReviewers( - Change change, - Collection<Account.Id> added, - Collection<Account.Id> copied, - NotifyHandling notify, - ListMultimap<RecipientType, Account.Id> accountsToNotify) { - if (added.isEmpty() && copied.isEmpty()) { - return; - } - - // Email the reviewers - // - // The user knows they added themselves, don't bother emailing them. - List<Account.Id> toMail = Lists.newArrayListWithCapacity(added.size()); - Account.Id userId = user.get().getAccountId(); - for (Account.Id id : added) { - if (!id.equals(userId)) { - toMail.add(id); - } - } - List<Account.Id> toCopy = Lists.newArrayListWithCapacity(copied.size()); - for (Account.Id id : copied) { - if (!id.equals(userId)) { - toCopy.add(id); - } - } - if (toMail.isEmpty() && toCopy.isEmpty()) { - return; - } - - try { - AddReviewerSender cm = addReviewerSenderFactory.create(change.getProject(), change.getId()); - if (notify != null) { - cm.setNotify(notify); - } - cm.setAccountsToNotify(accountsToNotify); - cm.setFrom(userId); - cm.addReviewers(toMail); - cm.addExtraCC(toCopy); - cm.send(); - } catch (Exception err) { - log.error("Cannot send email to new reviewers of change " + change.getId(), err); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewersOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewersOp.java new file mode 100644 index 0000000..df6d389 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewersOp.java
@@ -0,0 +1,261 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.base.Preconditions.checkState; +import static com.google.gerrit.extensions.client.ReviewerState.CC; +import static java.util.stream.Collectors.toList; + +import com.google.auto.value.AutoValue; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.extensions.client.ReviewerState; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.PatchSetApproval; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ApprovalsUtil; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.extensions.events.ReviewerAdded; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.mail.send.AddReviewerSender; +import com.google.gerrit.server.notedb.NotesMigration; +import com.google.gerrit.server.notedb.ReviewerStateInternal; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.Context; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PostReviewersOp implements BatchUpdateOp { + private static final Logger log = LoggerFactory.getLogger(PostReviewersOp.class); + + public interface Factory { + PostReviewersOp create( + ChangeResource rsrc, + Set<Account.Id> reviewers, + Collection<Address> reviewersByEmail, + ReviewerState state, + @Nullable NotifyHandling notify, + ListMultimap<RecipientType, Account.Id> accountsToNotify); + } + + @AutoValue + public abstract static class Result { + public abstract ImmutableList<PatchSetApproval> addedReviewers(); + + public abstract ImmutableList<Account.Id> addedCCs(); + + static Builder builder() { + return new AutoValue_PostReviewersOp_Result.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setAddedReviewers(ImmutableList<PatchSetApproval> addedReviewers); + + abstract Builder setAddedCCs(ImmutableList<Account.Id> addedCCs); + + abstract Result build(); + } + } + + private final ApprovalsUtil approvalsUtil; + private final PatchSetUtil psUtil; + private final ReviewerAdded reviewerAdded; + private final AccountCache accountCache; + private final AddReviewerSender.Factory addReviewerSenderFactory; + private final NotesMigration migration; + private final Provider<IdentifiedUser> user; + private final Provider<ReviewDb> dbProvider; + private final ChangeResource rsrc; + private final Set<Account.Id> reviewers; + private final Collection<Address> reviewersByEmail; + private final ReviewerState state; + private final NotifyHandling notify; + private final ListMultimap<RecipientType, Account.Id> accountsToNotify; + + private List<PatchSetApproval> addedReviewers = new ArrayList<>(); + private Collection<Account.Id> addedCCs = new ArrayList<>(); + private Collection<Address> addedCCsByEmail = new ArrayList<>(); + private PatchSet patchSet; + private Result opResult; + + @Inject + PostReviewersOp( + ApprovalsUtil approvalsUtil, + PatchSetUtil psUtil, + ReviewerAdded reviewerAdded, + AccountCache accountCache, + AddReviewerSender.Factory addReviewerSenderFactory, + NotesMigration migration, + Provider<IdentifiedUser> user, + Provider<ReviewDb> dbProvider, + @Assisted ChangeResource rsrc, + @Assisted Set<Account.Id> reviewers, + @Assisted Collection<Address> reviewersByEmail, + @Assisted ReviewerState state, + @Assisted @Nullable NotifyHandling notify, + @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify) { + this.approvalsUtil = approvalsUtil; + this.psUtil = psUtil; + this.reviewerAdded = reviewerAdded; + this.accountCache = accountCache; + this.addReviewerSenderFactory = addReviewerSenderFactory; + this.migration = migration; + this.user = user; + this.dbProvider = dbProvider; + + this.rsrc = rsrc; + this.reviewers = reviewers; + this.reviewersByEmail = reviewersByEmail; + this.state = state; + this.notify = notify; + this.accountsToNotify = accountsToNotify; + } + + @Override + public boolean updateChange(ChangeContext ctx) + throws RestApiException, OrmException, IOException { + if (!reviewers.isEmpty()) { + if (migration.readChanges() && state == CC) { + addedCCs = + approvalsUtil.addCcs( + ctx.getNotes(), ctx.getUpdate(ctx.getChange().currentPatchSetId()), reviewers); + if (addedCCs.isEmpty()) { + return false; + } + } else { + addedReviewers = + approvalsUtil.addReviewers( + ctx.getDb(), + ctx.getNotes(), + ctx.getUpdate(ctx.getChange().currentPatchSetId()), + rsrc.getControl().getLabelTypes(), + rsrc.getChange(), + reviewers); + if (addedReviewers.isEmpty()) { + return false; + } + } + } + + for (Address a : reviewersByEmail) { + ctx.getUpdate(ctx.getChange().currentPatchSetId()) + .putReviewerByEmail(a, ReviewerStateInternal.fromReviewerState(state)); + } + + patchSet = psUtil.current(dbProvider.get(), rsrc.getNotes()); + return true; + } + + @Override + public void postUpdate(Context ctx) throws Exception { + opResult = + Result.builder() + .setAddedReviewers(ImmutableList.copyOf(addedReviewers)) + .setAddedCCs(ImmutableList.copyOf(addedCCs)) + .build(); + emailReviewers( + rsrc.getChange(), + Lists.transform(addedReviewers, r -> r.getAccountId()), + addedCCs == null ? ImmutableList.of() : addedCCs, + reviewersByEmail, + addedCCsByEmail, + notify, + accountsToNotify); + if (!addedReviewers.isEmpty()) { + List<Account> reviewers = + addedReviewers + .stream() + .map(r -> accountCache.get(r.getAccountId()).getAccount()) + .collect(toList()); + reviewerAdded.fire(rsrc.getChange(), patchSet, reviewers, ctx.getAccount(), ctx.getWhen()); + } + } + + public void emailReviewers( + Change change, + Collection<Account.Id> added, + Collection<Account.Id> copied, + Collection<Address> addedByEmail, + Collection<Address> copiedByEmail, + NotifyHandling notify, + ListMultimap<RecipientType, Account.Id> accountsToNotify) { + if (added.isEmpty() && copied.isEmpty() && addedByEmail.isEmpty() && copiedByEmail.isEmpty()) { + return; + } + + // Email the reviewers + // + // The user knows they added themselves, don't bother emailing them. + List<Account.Id> toMail = Lists.newArrayListWithCapacity(added.size()); + Account.Id userId = user.get().getAccountId(); + for (Account.Id id : added) { + if (!id.equals(userId)) { + toMail.add(id); + } + } + List<Account.Id> toCopy = Lists.newArrayListWithCapacity(copied.size()); + for (Account.Id id : copied) { + if (!id.equals(userId)) { + toCopy.add(id); + } + } + if (toMail.isEmpty() && toCopy.isEmpty() && addedByEmail.isEmpty() && copiedByEmail.isEmpty()) { + return; + } + + try { + AddReviewerSender cm = addReviewerSenderFactory.create(change.getProject(), change.getId()); + // Default to silent operation on WIP changes. + NotifyHandling defaultNotifyHandling = + change.isWorkInProgress() ? NotifyHandling.NONE : NotifyHandling.ALL; + cm.setNotify(MoreObjects.firstNonNull(notify, defaultNotifyHandling)); + cm.setAccountsToNotify(accountsToNotify); + cm.setFrom(userId); + cm.addReviewers(toMail); + cm.addReviewersByEmail(addedByEmail); + cm.addExtraCC(toCopy); + cm.addExtraCCByEmail(copiedByEmail); + cm.send(); + } catch (Exception err) { + log.error("Cannot send email to new reviewers of change " + change.getId(), err); + } + } + + public Result getResult() { + checkState(opResult != null, "Batch update wasn't executed yet"); + return opResult; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java index f4356db..9ef445d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
@@ -25,7 +25,9 @@ import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.change.LimitedByteArrayOutputStream.LimitExceededException; import com.google.gerrit.server.config.GerritServerConfig; @@ -34,6 +36,7 @@ import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchProjectException; +import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -42,9 +45,11 @@ import java.io.OutputStream; import java.util.Collection; import org.apache.commons.compress.archivers.ArchiveOutputStream; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.storage.pack.PackConfig; import org.eclipse.jgit.transport.BundleWriter; import org.eclipse.jgit.transport.ReceiveCommand; import org.kohsuke.args4j.Option; @@ -77,7 +82,8 @@ } @Override - public BinaryResult apply(RevisionResource rsrc) throws OrmException, RestApiException { + public BinaryResult apply(RevisionResource rsrc) + throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException { if (Strings.isNullOrEmpty(format)) { throw new BadRequestException("format is not specified"); } @@ -94,7 +100,7 @@ Change change = rsrc.getChange(); if (!change.getStatus().isOpen()) { - throw new PreconditionFailedException("change is " + Submit.status(change)); + throw new PreconditionFailedException("change is " + ChangeUtil.status(change)); } ChangeControl control = rsrc.getControl(); if (!control.getUser().isIdentifiedUser()) { @@ -105,7 +111,7 @@ } private BinaryResult getBundles(RevisionResource rsrc, ArchiveFormat f) - throws OrmException, RestApiException { + throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException { ReviewDb db = dbProvider.get(); ChangeControl control = rsrc.getControl(); IdentifiedUser caller = control.getUser().asIdentifiedUser(); @@ -120,7 +126,12 @@ .setContentType(f.getMimeType()) .setAttachmentName("submit-preview-" + change.getChangeId() + "." + format); return bin; - } catch (OrmException | RestApiException | RuntimeException e) { + } catch (OrmException + | RestApiException + | UpdateException + | IOException + | ConfigInvalidException + | RuntimeException e) { op.close(); throw e; } @@ -144,14 +155,16 @@ MergeOpRepoManager orm = mergeOp.getMergeOpRepoManager(); for (Project.NameKey p : mergeOp.getAllProjects()) { OpenRepo or = orm.getRepo(p); - BundleWriter bw = new BundleWriter(or.getRepo()); + BundleWriter bw = new BundleWriter(or.getCodeReviewRevWalk().getObjectReader()); bw.setObjectCountCallback(null); - bw.setPackConfig(null); - Collection<ReceiveCommand> refs = or.getUpdate().getRefUpdates(); + bw.setPackConfig(new PackConfig(or.getRepo())); + Collection<ReceiveCommand> refs = or.getUpdate().getRefUpdates().values(); for (ReceiveCommand r : refs) { bw.include(r.getRefName(), r.getNewId()); ObjectId oldId = r.getOldId(); - if (!oldId.equals(ObjectId.zeroId())) { + if (!oldId.equals(ObjectId.zeroId()) + // Probably the client doesn't already have NoteDb data. + && !RefNames.isNoteDbMetaRef(r.getRefName())) { bw.assume(or.getCodeReviewRevWalk().parseCommit(oldId)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java index 0e72979..cbb5fa3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishChangeEdit.java
@@ -14,27 +14,28 @@ package com.google.gerrit.server.change; -import com.google.gerrit.common.data.Capable; import com.google.gerrit.extensions.api.changes.PublishChangeEditInput; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.AcceptsPost; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ChildCollection; import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.NotImplementedException; import com.google.gerrit.extensions.restapi.ResourceConflictException; 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.extensions.restapi.RestView; import com.google.gerrit.server.edit.ChangeEdit; import com.google.gerrit.server.edit.ChangeEditUtil; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; import java.util.Optional; +import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton public class PublishChangeEdit @@ -69,25 +70,25 @@ } @Singleton - public static class Publish implements RestModifyView<ChangeResource, PublishChangeEditInput> { + public static class Publish + extends RetryingRestModifyView<ChangeResource, PublishChangeEditInput, Response<?>> { private final ChangeEditUtil editUtil; private final NotifyUtil notifyUtil; @Inject - Publish(ChangeEditUtil editUtil, NotifyUtil notifyUtil) { + Publish(RetryHelper retryHelper, ChangeEditUtil editUtil, NotifyUtil notifyUtil) { + super(retryHelper); this.editUtil = editUtil; this.notifyUtil = notifyUtil; } @Override - public Response<?> apply(ChangeResource rsrc, PublishChangeEditInput in) - throws IOException, OrmException, RestApiException, UpdateException { - Capable r = rsrc.getControl().getProjectControl().canPushToAtLeastOneRef(); - if (r != Capable.OK) { - throw new AuthException(r.getMessage()); - } - + protected Response<?> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, PublishChangeEditInput in) + throws IOException, OrmException, RestApiException, UpdateException, + ConfigInvalidException { + CreateChange.checkValidCLA(rsrc.getControl().getProjectControl()); Optional<ChangeEdit> edit = editUtil.byChange(rsrc.getChange()); if (!edit.isPresent()) { throw new ResourceConflictException( @@ -96,7 +97,12 @@ if (in == null) { in = new PublishChangeEditInput(); } - editUtil.publish(edit.get(), in.notify, notifyUtil.resolveAccounts(in.notifyDetails)); + editUtil.publish( + updateFactory, + rsrc.getControl(), + edit.get(), + in.notify, + notifyUtil.resolveAccounts(in.notifyDetails)); return Response.none(); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java index 4cbeaf63c..3a614a3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -25,7 +25,6 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.extensions.webui.UiAction; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; @@ -49,6 +48,8 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -65,14 +66,14 @@ @Singleton public class PublishDraftPatchSet - implements RestModifyView<RevisionResource, Input>, UiAction<RevisionResource> { + extends RetryingRestModifyView<RevisionResource, Input, Response<?>> + implements UiAction<RevisionResource> { private static final Logger log = LoggerFactory.getLogger(PublishDraftPatchSet.class); public static class Input {} private final AccountResolver accountResolver; private final ApprovalsUtil approvalsUtil; - private final BatchUpdate.Factory updateFactory; private final CreateChangeSender.Factory createChangeSenderFactory; private final PatchSetInfoFactory patchSetInfoFactory; private final PatchSetUtil psUtil; @@ -84,16 +85,16 @@ public PublishDraftPatchSet( AccountResolver accountResolver, ApprovalsUtil approvalsUtil, - BatchUpdate.Factory updateFactory, + RetryHelper retryHelper, CreateChangeSender.Factory createChangeSenderFactory, PatchSetInfoFactory patchSetInfoFactory, PatchSetUtil psUtil, Provider<ReviewDb> dbProvider, ReplacePatchSetSender.Factory replacePatchSetFactory, DraftPublished draftPublished) { + super(retryHelper); this.accountResolver = accountResolver; this.approvalsUtil = approvalsUtil; - this.updateFactory = updateFactory; this.createChangeSenderFactory = createChangeSenderFactory; this.patchSetInfoFactory = patchSetInfoFactory; this.psUtil = psUtil; @@ -103,12 +104,19 @@ } @Override - public Response<?> apply(RevisionResource rsrc, Input input) + protected Response<?> applyImpl( + BatchUpdate.Factory updateFactory, RevisionResource rsrc, Input input) throws RestApiException, UpdateException { - return apply(rsrc.getUser(), rsrc.getChange(), rsrc.getPatchSet().getId(), rsrc.getPatchSet()); + return apply( + updateFactory, + rsrc.getUser(), + rsrc.getChange(), + rsrc.getPatchSet().getId(), + rsrc.getPatchSet()); } - private Response<?> apply(CurrentUser u, Change c, PatchSet.Id psId, PatchSet ps) + private Response<?> apply( + BatchUpdate.Factory updateFactory, CurrentUser u, Change c, PatchSet.Id psId, PatchSet ps) throws RestApiException, UpdateException { try (BatchUpdate bu = updateFactory.create(dbProvider.get(), c.getProject(), u, TimeUtil.nowTs())) { @@ -131,18 +139,22 @@ } } - public static class CurrentRevision implements RestModifyView<ChangeResource, Input> { + public static class CurrentRevision + extends RetryingRestModifyView<ChangeResource, Input, Response<?>> { private final PublishDraftPatchSet publish; @Inject - CurrentRevision(PublishDraftPatchSet publish) { + CurrentRevision(RetryHelper retryHelper, PublishDraftPatchSet publish) { + super(retryHelper); this.publish = publish; } @Override - public Response<?> apply(ChangeResource rsrc, Input input) + protected Response<?> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input) throws RestApiException, UpdateException { return publish.apply( + updateFactory, rsrc.getControl().getUser(), rsrc.getChange(), rsrc.getChange().currentPatchSetId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java index e64abaa..a0862d6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutAssignee.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.change; +import com.google.common.base.Strings; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.api.changes.AddReviewerInput; import com.google.gerrit.extensions.api.changes.AssigneeInput; @@ -22,74 +23,94 @@ import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.restapi.AuthException; 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.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.extensions.webui.UiAction; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountLoader; +import com.google.gerrit.server.account.AccountsCollection; import com.google.gerrit.server.change.PostReviewers.Addition; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; 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; @Singleton -public class PutAssignee - implements RestModifyView<ChangeResource, AssigneeInput>, UiAction<ChangeResource> { +public class PutAssignee extends RetryingRestModifyView<ChangeResource, AssigneeInput, AccountInfo> + implements UiAction<ChangeResource> { + private final AccountsCollection accounts; private final SetAssigneeOp.Factory assigneeFactory; - private final BatchUpdate.Factory batchUpdateFactory; private final Provider<ReviewDb> db; private final PostReviewers postReviewers; private final AccountLoader.Factory accountLoaderFactory; @Inject PutAssignee( + AccountsCollection accounts, SetAssigneeOp.Factory assigneeFactory, - BatchUpdate.Factory batchUpdateFactory, + RetryHelper retryHelper, Provider<ReviewDb> db, PostReviewers postReviewers, AccountLoader.Factory accountLoaderFactory) { + super(retryHelper); + this.accounts = accounts; this.assigneeFactory = assigneeFactory; - this.batchUpdateFactory = batchUpdateFactory; this.db = db; this.postReviewers = postReviewers; this.accountLoaderFactory = accountLoaderFactory; } @Override - public Response<AccountInfo> apply(ChangeResource rsrc, AssigneeInput input) - throws RestApiException, UpdateException, OrmException, IOException { - if (!rsrc.getControl().canEditAssignee()) { - throw new AuthException("Changing Assignee not permitted"); - } - if (input.assignee == null || input.assignee.trim().isEmpty()) { + protected AccountInfo applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, AssigneeInput input) + throws RestApiException, UpdateException, OrmException, IOException, + PermissionBackendException, ConfigInvalidException { + rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE); + + input.assignee = Strings.nullToEmpty(input.assignee).trim(); + if (input.assignee.isEmpty()) { throw new BadRequestException("missing assignee field"); } + IdentifiedUser assignee = accounts.parse(input.assignee); + if (!assignee.getAccount().isActive()) { + throw new UnprocessableEntityException(input.assignee + " is not active"); + } + try { + rsrc.permissions().database(db).user(assignee).check(ChangePermission.READ); + } catch (AuthException e) { + throw new AuthException("read not permitted for " + input.assignee); + } + try (BatchUpdate bu = - batchUpdateFactory.create( + updateFactory.create( db.get(), rsrc.getChange().getProject(), rsrc.getControl().getUser(), TimeUtil.nowTs())) { - SetAssigneeOp op = assigneeFactory.create(input.assignee); + SetAssigneeOp op = assigneeFactory.create(assignee); bu.addOp(rsrc.getId(), op); PostReviewers.Addition reviewersAddition = addAssigneeAsCC(rsrc, input.assignee); bu.addOp(rsrc.getId(), reviewersAddition.op); bu.execute(); - return Response.ok(accountLoaderFactory.create(true).fillOne(op.getNewAssignee())); + return accountLoaderFactory.create(true).fillOne(assignee.getAccountId()); } } private Addition addAssigneeAsCC(ChangeResource rsrc, String assignee) - throws OrmException, RestApiException, IOException { + throws OrmException, IOException, PermissionBackendException, ConfigInvalidException { AddReviewerInput reviewerInput = new AddReviewerInput(); reviewerInput.reviewer = assignee; reviewerInput.state = ReviewerState.CC; @@ -99,9 +120,9 @@ } @Override - public UiAction.Description getDescription(ChangeResource resource) { + public UiAction.Description getDescription(ChangeResource rsrc) { return new UiAction.Description() .setLabel("Edit Assignee") - .setVisible(resource.getControl().canEditAssignee()); + .setVisible(rsrc.permissions().testOrFalse(ChangePermission.EDIT_ASSIGNEE)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java index 3c2633e..f2614c3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDescription.java
@@ -16,11 +16,9 @@ import com.google.common.base.Strings; import com.google.gerrit.common.TimeUtil; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.DefaultInput; 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.extensions.webui.UiAction; import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.PatchSet; @@ -28,10 +26,14 @@ import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -41,10 +43,10 @@ @Singleton public class PutDescription - implements RestModifyView<RevisionResource, PutDescription.Input>, UiAction<RevisionResource> { + extends RetryingRestModifyView<RevisionResource, PutDescription.Input, Response<String>> + implements UiAction<RevisionResource> { private final Provider<ReviewDb> dbProvider; private final ChangeMessagesUtil cmUtil; - private final BatchUpdate.Factory batchUpdateFactory; private final PatchSetUtil psUtil; public static class Input { @@ -55,24 +57,24 @@ PutDescription( Provider<ReviewDb> dbProvider, ChangeMessagesUtil cmUtil, - BatchUpdate.Factory batchUpdateFactory, + RetryHelper retryHelper, PatchSetUtil psUtil) { + super(retryHelper); this.dbProvider = dbProvider; this.cmUtil = cmUtil; - this.batchUpdateFactory = batchUpdateFactory; this.psUtil = psUtil; } @Override - public Response<String> apply(RevisionResource rsrc, Input input) - throws UpdateException, RestApiException { + protected Response<String> applyImpl( + BatchUpdate.Factory updateFactory, RevisionResource rsrc, Input input) + throws UpdateException, RestApiException, PermissionBackendException { + rsrc.permissions().check(ChangePermission.EDIT_DESCRIPTION); + ChangeControl ctl = rsrc.getControl(); - if (!ctl.canEditDescription()) { - throw new AuthException("changing description not permitted"); - } Op op = new Op(input != null ? input : new Input(), rsrc.getPatchSet().getId()); try (BatchUpdate u = - batchUpdateFactory.create( + updateFactory.create( dbProvider.get(), rsrc.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) { u.addOp(rsrc.getChange().getId(), op); u.execute(); @@ -129,6 +131,6 @@ public UiAction.Description getDescription(RevisionResource rsrc) { return new UiAction.Description() .setLabel("Edit Description") - .setVisible(rsrc.getControl().canEditDescription()); + .setVisible(rsrc.permissions().testOrFalse(ChangePermission.EDIT_DESCRIPTION)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java index b289da8..90716ce 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraftComment.java
@@ -23,7 +23,6 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; 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.extensions.restapi.Url; import com.google.gerrit.reviewdb.client.Comment; import com.google.gerrit.reviewdb.client.PatchLineComment.Status; @@ -36,6 +35,8 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -46,13 +47,13 @@ import java.util.Optional; @Singleton -public class PutDraftComment implements RestModifyView<DraftCommentResource, DraftInput> { +public class PutDraftComment + extends RetryingRestModifyView<DraftCommentResource, DraftInput, Response<CommentInfo>> { private final Provider<ReviewDb> db; private final DeleteDraftComment delete; private final CommentsUtil commentsUtil; private final PatchSetUtil psUtil; - private final BatchUpdate.Factory updateFactory; private final Provider<CommentJson> commentJson; private final PatchListCache patchListCache; @@ -62,23 +63,24 @@ DeleteDraftComment delete, CommentsUtil commentsUtil, PatchSetUtil psUtil, - BatchUpdate.Factory updateFactory, + RetryHelper retryHelper, Provider<CommentJson> commentJson, PatchListCache patchListCache) { + super(retryHelper); this.db = db; this.delete = delete; this.commentsUtil = commentsUtil; this.psUtil = psUtil; - this.updateFactory = updateFactory; this.commentJson = commentJson; this.patchListCache = patchListCache; } @Override - public Response<CommentInfo> apply(DraftCommentResource rsrc, DraftInput in) + protected Response<CommentInfo> applyImpl( + BatchUpdate.Factory updateFactory, DraftCommentResource rsrc, DraftInput in) throws RestApiException, UpdateException, OrmException { if (in == null || in.message == null || in.message.trim().isEmpty()) { - return delete.apply(rsrc, null); + return delete.applyImpl(updateFactory, rsrc, null); } else if (in.id != null && !rsrc.getId().equals(in.id)) { throw new BadRequestException("id must match URL"); } else if (in.line != null && in.line < 0) { @@ -146,7 +148,7 @@ update, Status.DRAFT, Collections.singleton(update(comment, in, ctx.getWhen()))); - ctx.bumpLastUpdatedOn(false); + ctx.dontBumpLastUpdatedOn(); return true; } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java new file mode 100644 index 0000000..95e29ea --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutMessage.java
@@ -0,0 +1,219 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.FooterConstants; +import com.google.gerrit.common.TimeUtil; +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.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.DefaultInput; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.edit.UnchangedCommitMessageException; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; +import com.google.gerrit.server.update.UpdateException; +import com.google.gerrit.server.util.CommitMessageUtil; +import com.google.gwtorm.server.OrmException; +import java.io.IOException; +import java.sql.Timestamp; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +@Singleton +public class PutMessage + extends RetryingRestModifyView<ChangeResource, PutMessage.Input, Response<?>> { + + public static class Input { + @DefaultInput public String message; + + public NotifyHandling notify = NotifyHandling.ALL; + + public Map<RecipientType, NotifyInfo> notifyDetails; + } + + private final GitRepositoryManager repositoryManager; + private final Provider<CurrentUser> currentUserProvider; + private final Provider<ReviewDb> db; + private final TimeZone tz; + private final PatchSetInserter.Factory psInserterFactory; + private final PermissionBackend permissionBackend; + private final PatchSetUtil psUtil; + private final NotifyUtil notifyUtil; + + @Inject + PutMessage( + RetryHelper retryHelper, + GitRepositoryManager repositoryManager, + Provider<CurrentUser> currentUserProvider, + Provider<ReviewDb> db, + PatchSetInserter.Factory psInserterFactory, + PermissionBackend permissionBackend, + @GerritPersonIdent PersonIdent gerritIdent, + PatchSetUtil psUtil, + NotifyUtil notifyUtil) { + super(retryHelper); + this.repositoryManager = repositoryManager; + this.currentUserProvider = currentUserProvider; + this.db = db; + this.psInserterFactory = psInserterFactory; + this.tz = gerritIdent.getTimeZone(); + this.permissionBackend = permissionBackend; + this.psUtil = psUtil; + this.notifyUtil = notifyUtil; + } + + @Override + protected Response<String> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource resource, Input input) + throws IOException, UnchangedCommitMessageException, RestApiException, UpdateException, + PermissionBackendException, OrmException, ConfigInvalidException { + PatchSet ps = psUtil.current(db.get(), resource.getNotes()); + if (ps == null) { + throw new ResourceConflictException("current revision is missing"); + } else if (!resource.getControl().isPatchVisible(ps, db.get())) { + throw new AuthException("current revision not accessible"); + } + + if (input == null) { + throw new BadRequestException("input cannot be null"); + } + String sanitizedCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(input.message); + + ensureCanEditCommitMessage(resource.getControl().getNotes()); + ensureChangeIdIsCorrect( + resource.getControl().getProjectControl().getProjectState().isRequireChangeID(), + resource.getChange().getKey().get(), + sanitizedCommitMessage); + + try (Repository repository = repositoryManager.openRepository(resource.getProject()); + RevWalk revWalk = new RevWalk(repository); + ObjectInserter objectInserter = repository.newObjectInserter()) { + RevCommit patchSetCommit = revWalk.parseCommit(ObjectId.fromString(ps.getRevision().get())); + + String currentCommitMessage = patchSetCommit.getFullMessage(); + if (input.message.equals(currentCommitMessage)) { + throw new ResourceConflictException("new and existing commit message are the same"); + } + + Timestamp ts = TimeUtil.nowTs(); + try (BatchUpdate bu = + updateFactory.create( + db.get(), resource.getChange().getProject(), currentUserProvider.get(), ts)) { + // Ensure that BatchUpdate will update the same repo + bu.setRepository(repository, new RevWalk(objectInserter.newReader()), objectInserter); + + PatchSet.Id psId = ChangeUtil.nextPatchSetId(repository, ps.getId()); + ObjectId newCommit = + createCommit(objectInserter, patchSetCommit, sanitizedCommitMessage, ts); + PatchSetInserter inserter = + psInserterFactory.create(resource.getControl(), psId, newCommit); + inserter.setMessage( + String.format("Patch Set %s: Commit message was updated.", psId.getId())); + inserter.setDescription("Edit commit message"); + inserter.setNotify(input.notify); + inserter.setAccountsToNotify(notifyUtil.resolveAccounts(input.notifyDetails)); + bu.addOp(resource.getChange().getId(), inserter); + bu.execute(); + } + } + return Response.ok("ok"); + } + + private ObjectId createCommit( + ObjectInserter objectInserter, + RevCommit basePatchSetCommit, + String commitMessage, + Timestamp timestamp) + throws IOException { + CommitBuilder builder = new CommitBuilder(); + builder.setTreeId(basePatchSetCommit.getTree()); + builder.setParentIds(basePatchSetCommit.getParents()); + builder.setAuthor(basePatchSetCommit.getAuthorIdent()); + builder.setCommitter( + currentUserProvider.get().asIdentifiedUser().newCommitterIdent(timestamp, tz)); + builder.setMessage(commitMessage); + ObjectId newCommitId = objectInserter.insert(builder); + objectInserter.flush(); + return newCommitId; + } + + private void ensureCanEditCommitMessage(ChangeNotes changeNotes) + throws AuthException, PermissionBackendException { + if (!currentUserProvider.get().isIdentifiedUser()) { + throw new AuthException("Authentication required"); + } + try { + permissionBackend + .user(currentUserProvider.get()) + .database(db.get()) + .change(changeNotes) + .check(ChangePermission.ADD_PATCH_SET); + } catch (AuthException denied) { + throw new AuthException("modifying commit message not permitted", denied); + } + } + + private static void ensureChangeIdIsCorrect( + boolean requireChangeId, String currentChangeId, String newCommitMessage) + throws ResourceConflictException, BadRequestException { + RevCommit revCommit = + RevCommit.parse( + Constants.encode("tree " + ObjectId.zeroId().name() + "\n\n" + newCommitMessage)); + + // Check that the commit message without footers is not empty + CommitMessageUtil.checkAndSanitizeCommitMessage(revCommit.getShortMessage()); + + List<String> changeIdFooters = revCommit.getFooterLines(FooterConstants.CHANGE_ID); + if (requireChangeId && changeIdFooters.isEmpty()) { + throw new ResourceConflictException("missing Change-Id footer"); + } + if (!changeIdFooters.isEmpty() && !changeIdFooters.get(0).equals(currentChangeId)) { + throw new ResourceConflictException("wrong Change-Id footer"); + } + if (changeIdFooters.size() > 1) { + throw new ResourceConflictException("multiple Change-Id footers"); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java index 783ab9d..5d9f7b9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -16,11 +16,9 @@ import com.google.common.base.Strings; import com.google.gerrit.common.TimeUtil; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.DefaultInput; 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.extensions.webui.UiAction; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; @@ -29,11 +27,14 @@ import com.google.gerrit.server.change.PutTopic.Input; import com.google.gerrit.server.extensions.events.TopicEdited; import com.google.gerrit.server.notedb.ChangeUpdate; -import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -41,10 +42,10 @@ import com.google.inject.Singleton; @Singleton -public class PutTopic implements RestModifyView<ChangeResource, Input>, UiAction<ChangeResource> { +public class PutTopic extends RetryingRestModifyView<ChangeResource, Input, Response<String>> + implements UiAction<ChangeResource> { private final Provider<ReviewDb> dbProvider; private final ChangeMessagesUtil cmUtil; - private final BatchUpdate.Factory batchUpdateFactory; private final TopicEdited topicEdited; public static class Input { @@ -55,32 +56,27 @@ PutTopic( Provider<ReviewDb> dbProvider, ChangeMessagesUtil cmUtil, - BatchUpdate.Factory batchUpdateFactory, + RetryHelper retryHelper, TopicEdited topicEdited) { + super(retryHelper); this.dbProvider = dbProvider; this.cmUtil = cmUtil; - this.batchUpdateFactory = batchUpdateFactory; this.topicEdited = topicEdited; } @Override - public Response<String> apply(ChangeResource req, Input input) - throws UpdateException, RestApiException { - ChangeControl ctl = req.getControl(); - if (!ctl.canEditTopicName()) { - throw new AuthException("changing topic not permitted"); - } - + protected Response<String> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource req, Input input) + throws UpdateException, RestApiException, PermissionBackendException { + req.permissions().check(ChangePermission.EDIT_TOPIC_NAME); Op op = new Op(input != null ? input : new Input()); try (BatchUpdate u = - batchUpdateFactory.create( - dbProvider.get(), req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) { + updateFactory.create( + dbProvider.get(), req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) { u.addOp(req.getId(), op); u.execute(); } - return Strings.isNullOrEmpty(op.newTopicName) - ? Response.<String>none() - : Response.ok(op.newTopicName); + return Strings.isNullOrEmpty(op.newTopicName) ? Response.none() : Response.ok(op.newTopicName); } private class Op implements BatchUpdateOp { @@ -129,9 +125,9 @@ } @Override - public UiAction.Description getDescription(ChangeResource resource) { + public UiAction.Description getDescription(ChangeResource rsrc) { return new UiAction.Description() .setLabel("Edit Topic") - .setVisible(resource.getControl().canEditTopicName()); + .setVisible(rsrc.permissions().testOrFalse(ChangePermission.EDIT_TOPIC_NAME)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java index 93e1e4e..7131e20 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Rebase.java
@@ -31,13 +31,17 @@ import com.google.gerrit.reviewdb.client.Change.Status; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.change.RebaseUtil.Base; import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.git.validators.CommitValidators; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -46,6 +50,8 @@ import java.io.IOException; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; @@ -53,13 +59,12 @@ import org.slf4j.LoggerFactory; @Singleton -public class Rebase +public class Rebase extends RetryingRestModifyView<RevisionResource, RebaseInput, ChangeInfo> implements RestModifyView<RevisionResource, RebaseInput>, UiAction<RevisionResource> { private static final Logger log = LoggerFactory.getLogger(Rebase.class); private static final ImmutableSet<ListChangesOption> OPTIONS = Sets.immutableEnumSet(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT); - private final BatchUpdate.Factory updateFactory; private final GitRepositoryManager repoManager; private final RebaseChangeOp.Factory rebaseFactory; private final RebaseUtil rebaseUtil; @@ -68,13 +73,13 @@ @Inject public Rebase( - BatchUpdate.Factory updateFactory, + RetryHelper retryHelper, GitRepositoryManager repoManager, RebaseChangeOp.Factory rebaseFactory, RebaseUtil rebaseUtil, ChangeJson.Factory json, Provider<ReviewDb> dbProvider) { - this.updateFactory = updateFactory; + super(retryHelper); this.repoManager = repoManager; this.rebaseFactory = rebaseFactory; this.rebaseUtil = rebaseUtil; @@ -83,21 +88,23 @@ } @Override - public ChangeInfo apply(RevisionResource rsrc, RebaseInput input) + protected ChangeInfo applyImpl( + BatchUpdate.Factory updateFactory, RevisionResource rsrc, RebaseInput input) throws EmailException, OrmException, UpdateException, RestApiException, IOException, - NoSuchChangeException { + NoSuchChangeException, PermissionBackendException { + rsrc.permissions().database(dbProvider).check(ChangePermission.REBASE); + ChangeControl control = rsrc.getControl(); Change change = rsrc.getChange(); try (Repository repo = repoManager.openRepository(change.getProject()); - RevWalk rw = new RevWalk(repo); ObjectInserter oi = repo.newObjectInserter(); + ObjectReader reader = oi.newReader(); + RevWalk rw = new RevWalk(reader); BatchUpdate bu = updateFactory.create( dbProvider.get(), change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { - if (!control.canRebase(dbProvider.get())) { - throw new AuthException("rebase not permitted"); - } else if (!change.getStatus().isOpen()) { - throw new ResourceConflictException("change is " + change.getStatus().name().toLowerCase()); + if (!change.getStatus().isOpen()) { + throw new ResourceConflictException("change is " + ChangeUtil.status(change)); } else if (!hasOneParent(rw, rsrc.getPatchSet())) { throw new ResourceConflictException( "cannot rebase merge commits or commit with no ancestor"); @@ -106,27 +113,32 @@ bu.addOp( change.getId(), rebaseFactory - .create(control, rsrc.getPatchSet(), findBaseRev(rw, rsrc, input)) + .create(control, rsrc.getPatchSet(), findBaseRev(repo, rw, rsrc, input)) .setForceContentMerge(true) - .setFireRevisionCreated(true) - .setValidatePolicy(CommitValidators.Policy.GERRIT)); + .setFireRevisionCreated(true)); bu.execute(); } return json.create(OPTIONS).format(change.getProject(), change.getId()); } - private String findBaseRev(RevWalk rw, RevisionResource rsrc, RebaseInput input) - throws AuthException, ResourceConflictException, OrmException, IOException, - NoSuchChangeException { + private ObjectId findBaseRev( + Repository repo, RevWalk rw, RevisionResource rsrc, RebaseInput input) + throws RestApiException, OrmException, IOException, NoSuchChangeException { + Branch.NameKey destRefKey = rsrc.getChange().getDest(); if (input == null || input.base == null) { - return null; + return rebaseUtil.findBaseRevision(rsrc.getPatchSet(), destRefKey, repo, rw); } Change change = rsrc.getChange(); String str = input.base.trim(); if (str.equals("")) { - // remove existing dependency to other patch set - return change.getDest().get(); + // Remove existing dependency to other patch set. + Ref destRef = repo.exactRef(destRefKey.get()); + if (destRef == null) { + throw new ResourceConflictException( + "can't rebase onto tip of branch " + destRefKey.get() + "; branch doesn't exist"); + } + return destRef.getObjectId(); } @SuppressWarnings("resource") @@ -157,7 +169,7 @@ + baseChange.getKey() + " is a descendant of the current change - recursion not allowed"); } - return base.patchSet().getRevision().get(); + return ObjectId.fromString(base.patchSet().getRevision().get()); } private boolean isMergedInto(RevWalk rw, PatchSet base, PatchSet tip) throws IOException { @@ -175,15 +187,12 @@ @Override public UiAction.Description getDescription(RevisionResource resource) { PatchSet patchSet = resource.getPatchSet(); - Branch.NameKey dest = resource.getChange().getDest(); - boolean canRebase = false; - try { - canRebase = resource.getControl().canRebase(dbProvider.get()); - } catch (OrmException e) { - log.error("Cannot check canRebase status. Assuming false.", e); - } + Change change = resource.getChange(); + Branch.NameKey dest = change.getDest(); boolean visible = - resource.getChange().getStatus().isOpen() && resource.isCurrent() && canRebase; + change.getStatus().isOpen() + && resource.isCurrent() + && resource.permissions().database(dbProvider).testOrFalse(ChangePermission.REBASE); boolean enabled = true; if (visible) { @@ -196,35 +205,37 @@ visible = false; } } - UiAction.Description descr = - new UiAction.Description() - .setLabel("Rebase") - .setTitle("Rebase onto tip of branch or parent change") - .setVisible(visible) - .setEnabled(enabled); - return descr; + return new UiAction.Description() + .setLabel("Rebase") + .setTitle("Rebase onto tip of branch or parent change") + .setVisible(visible) + .setEnabled(enabled); } - public static class CurrentRevision implements RestModifyView<ChangeResource, RebaseInput> { + public static class CurrentRevision + extends RetryingRestModifyView<ChangeResource, RebaseInput, ChangeInfo> { private final PatchSetUtil psUtil; private final Rebase rebase; @Inject - CurrentRevision(PatchSetUtil psUtil, Rebase rebase) { + CurrentRevision(RetryHelper retryHelper, PatchSetUtil psUtil, Rebase rebase) { + super(retryHelper); this.psUtil = psUtil; this.rebase = rebase; } @Override - public ChangeInfo apply(ChangeResource rsrc, RebaseInput input) - throws EmailException, OrmException, UpdateException, RestApiException, IOException { + protected ChangeInfo applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, RebaseInput input) + throws EmailException, OrmException, UpdateException, RestApiException, IOException, + PermissionBackendException { PatchSet ps = psUtil.current(rebase.dbProvider.get(), rsrc.getNotes()); if (ps == null) { throw new ResourceConflictException("current revision is missing"); } else if (!rsrc.getControl().isPatchVisible(ps, rebase.dbProvider.get())) { throw new AuthException("current revision not accessible"); } - return rebase.apply(new RevisionResource(rsrc, ps), input); + return rebase.applyImpl(updateFactory, new RevisionResource(rsrc, ps), input); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java index 7b673dd..9e840c3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeEdit.java
@@ -28,6 +28,7 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.edit.ChangeEditModifier; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -82,7 +83,8 @@ @Override public Response<?> apply(ChangeResource rsrc, Rebase.Input in) - throws AuthException, ResourceConflictException, IOException, OrmException { + throws AuthException, ResourceConflictException, IOException, OrmException, + PermissionBackendException { Project.NameKey project = rsrc.getProject(); try (Repository repository = repositoryManager.openRepository(project)) { editModifier.rebaseEdit(repository, rsrc.getControl());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java index c03bb6f..34d239c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -16,7 +16,6 @@ import static com.google.common.base.Preconditions.checkState; -import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.restapi.MergeConflictException; import com.google.gerrit.extensions.restapi.ResourceConflictException; @@ -26,7 +25,7 @@ import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.change.RebaseUtil.Base; import com.google.gerrit.server.git.MergeUtil; -import com.google.gerrit.server.git.validators.CommitValidators; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gerrit.server.project.NoSuchChangeException; @@ -36,8 +35,8 @@ import com.google.gerrit.server.update.Context; import com.google.gerrit.server.update.RepoContext; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.io.IOException; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.ObjectId; @@ -48,8 +47,7 @@ public class RebaseChangeOp implements BatchUpdateOp { public interface Factory { - RebaseChangeOp create( - ChangeControl ctl, PatchSet originalPatchSet, @Nullable String baseCommitish); + RebaseChangeOp create(ChangeControl ctl, PatchSet originalPatchSet, ObjectId baseCommitId); } private final PatchSetInserter.Factory patchSetInserterFactory; @@ -60,22 +58,23 @@ private final ChangeControl ctl; private final PatchSet originalPatchSet; - private String baseCommitish; + private ObjectId baseCommitId; private PersonIdent committerIdent; private boolean fireRevisionCreated = true; - private CommitValidators.Policy validate; + private boolean validate = true; private boolean checkAddPatchSetPermission = true; private boolean forceContentMerge; private boolean copyApprovals = true; private boolean detailedCommitMessage; private boolean postMessage = true; + private boolean matchAuthorToCommitterDate = false; private RevCommit rebasedCommit; private PatchSet.Id rebasedPatchSetId; private PatchSetInserter patchSetInserter; private PatchSet rebasedPatchSet; - @AssistedInject + @Inject RebaseChangeOp( PatchSetInserter.Factory patchSetInserterFactory, MergeUtil.Factory mergeUtilFactory, @@ -83,14 +82,14 @@ ChangeResource.Factory changeResourceFactory, @Assisted ChangeControl ctl, @Assisted PatchSet originalPatchSet, - @Assisted @Nullable String baseCommitish) { + @Assisted ObjectId baseCommitId) { this.patchSetInserterFactory = patchSetInserterFactory; this.mergeUtilFactory = mergeUtilFactory; this.rebaseUtil = rebaseUtil; this.changeResourceFactory = changeResourceFactory; this.ctl = ctl; this.originalPatchSet = originalPatchSet; - this.baseCommitish = baseCommitish; + this.baseCommitId = baseCommitId; } public RebaseChangeOp setCommitterIdent(PersonIdent committerIdent) { @@ -98,7 +97,7 @@ return this; } - public RebaseChangeOp setValidatePolicy(CommitValidators.Policy validate) { + public RebaseChangeOp setValidate(boolean validate) { this.validate = validate; return this; } @@ -133,10 +132,15 @@ return this; } + public RebaseChangeOp setMatchAuthorToCommitterDate(boolean matchAuthorToCommitterDate) { + this.matchAuthorToCommitterDate = matchAuthorToCommitterDate; + return this; + } + @Override public void updateRepo(RepoContext ctx) throws MergeConflictException, InvalidChangeOperationException, RestApiException, IOException, - OrmException, NoSuchChangeException { + OrmException, NoSuchChangeException, PermissionBackendException { // Ok that originalPatchSet was not read in a transaction, since we just // need its revision. RevId oldRev = originalPatchSet.getRevision(); @@ -144,19 +148,7 @@ RevWalk rw = ctx.getRevWalk(); RevCommit original = rw.parseCommit(ObjectId.fromString(oldRev.get())); rw.parseBody(original); - - RevCommit baseCommit; - if (baseCommitish != null) { - baseCommit = rw.parseCommit(ctx.getRepository().resolve(baseCommitish)); - } else { - baseCommit = - rw.parseCommit( - rebaseUtil.findBaseRevision( - originalPatchSet, - ctl.getChange().getDest(), - ctx.getRepository(), - ctx.getRevWalk())); - } + RevCommit baseCommit = rw.parseCommit(baseCommitId); String newCommitMessage; if (detailedCommitMessage) { @@ -169,16 +161,15 @@ } rebasedCommit = rebaseCommit(ctx, original, baseCommit, newCommitMessage); - - RevId baseRevId = - new RevId((baseCommitish != null) ? baseCommitish : ObjectId.toString(baseCommit.getId())); Base base = rebaseUtil.parseBase( new RevisionResource(changeResourceFactory.create(ctl), originalPatchSet), - baseRevId.get()); + baseCommitId.name()); rebasedPatchSetId = - ChangeUtil.nextPatchSetId(ctx.getRepository(), ctl.getChange().currentPatchSetId()); + ChangeUtil.nextPatchSetIdFromChangeRefsMap( + ctx.getRepoView().getRefs(originalPatchSet.getId().getParentKey().toRefPrefix()), + ctl.getChange().currentPatchSetId()); patchSetInserter = patchSetInserterFactory .create(ctl, rebasedPatchSetId, rebasedCommit) @@ -187,7 +178,8 @@ .setNotify(NotifyHandling.NONE) .setFireRevisionCreated(fireRevisionCreated) .setCopyApprovals(copyApprovals) - .setCheckAddPatchSetPermission(checkAddPatchSetPermission); + .setCheckAddPatchSetPermission(checkAddPatchSetPermission) + .setValidate(validate); if (postMessage) { patchSetInserter.setMessage( "Patch Set " @@ -200,9 +192,6 @@ if (base != null) { patchSetInserter.setGroups(base.patchSet().getGroups()); } - if (validate != null) { - patchSetInserter.setValidatePolicy(validate); - } patchSetInserter.updateRepo(ctx); } @@ -261,7 +250,7 @@ } ThreeWayMerger merger = - newMergeUtil().newThreeWayMerger(ctx.getRepository(), ctx.getInserter()); + newMergeUtil().newThreeWayMerger(ctx.getInserter(), ctx.getRepoView().getConfig()); merger.setBase(parentCommit); merger.merge(original, base); @@ -280,6 +269,11 @@ } else { cb.setCommitter(ctx.getIdentifiedUser().newCommitterIdent(ctx.getWhen(), ctx.getTimeZone())); } + if (matchAuthorToCommitterDate) { + cb.setAuthor( + new PersonIdent( + cb.getAuthor(), cb.getCommitter().getWhen(), cb.getCommitter().getTimeZone())); + } ObjectId objectId = ctx.getInserter().insert(cb); ctx.getInserter().flush(); return ctx.getRevWalk().parseCommit(objectId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java index b6c4d02..6f74ddd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -18,10 +18,8 @@ import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.api.changes.RestoreInput; import com.google.gerrit.extensions.common.ChangeInfo; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.webui.UiAction; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change.Status; @@ -29,16 +27,21 @@ import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.extensions.events.ChangeRestored; import com.google.gerrit.server.mail.send.ReplyToChangeSender; import com.google.gerrit.server.mail.send.RestoredSender; import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -48,8 +51,8 @@ import org.slf4j.LoggerFactory; @Singleton -public class Restore - implements RestModifyView<ChangeResource, RestoreInput>, UiAction<ChangeResource> { +public class Restore extends RetryingRestModifyView<ChangeResource, RestoreInput, ChangeInfo> + implements UiAction<ChangeResource> { private static final Logger log = LoggerFactory.getLogger(Restore.class); private final RestoredSender.Factory restoredSenderFactory; @@ -57,7 +60,6 @@ private final ChangeJson.Factory json; private final ChangeMessagesUtil cmUtil; private final PatchSetUtil psUtil; - private final BatchUpdate.Factory batchUpdateFactory; private final ChangeRestored changeRestored; @Inject @@ -67,28 +69,27 @@ ChangeJson.Factory json, ChangeMessagesUtil cmUtil, PatchSetUtil psUtil, - BatchUpdate.Factory batchUpdateFactory, + RetryHelper retryHelper, ChangeRestored changeRestored) { + super(retryHelper); this.restoredSenderFactory = restoredSenderFactory; this.dbProvider = dbProvider; this.json = json; this.cmUtil = cmUtil; this.psUtil = psUtil; - this.batchUpdateFactory = batchUpdateFactory; this.changeRestored = changeRestored; } @Override - public ChangeInfo apply(ChangeResource req, RestoreInput input) - throws RestApiException, UpdateException, OrmException { - ChangeControl ctl = req.getControl(); - if (!ctl.canRestore(dbProvider.get())) { - throw new AuthException("restore not permitted"); - } + protected ChangeInfo applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource req, RestoreInput input) + throws RestApiException, UpdateException, OrmException, PermissionBackendException { + req.permissions().database(dbProvider).check(ChangePermission.RESTORE); + ChangeControl ctl = req.getControl(); Op op = new Op(input); try (BatchUpdate u = - batchUpdateFactory.create( + updateFactory.create( dbProvider.get(), req.getChange().getProject(), ctl.getUser(), TimeUtil.nowTs())) { u.addOp(req.getId(), op).execute(); } @@ -110,7 +111,7 @@ public boolean updateChange(ChangeContext ctx) throws OrmException, ResourceConflictException { change = ctx.getChange(); if (change == null || change.getStatus() != Status.ABANDONED) { - throw new ResourceConflictException("change is " + status(change)); + throw new ResourceConflictException("change is " + ChangeUtil.status(change)); } PatchSet.Id psId = change.currentPatchSetId(); ChangeUpdate update = ctx.getUpdate(psId); @@ -150,20 +151,12 @@ } @Override - public UiAction.Description getDescription(ChangeResource resource) { - boolean canRestore = false; - try { - canRestore = resource.getControl().canRestore(dbProvider.get()); - } catch (OrmException e) { - log.error("Cannot check canRestore status. Assuming false.", e); - } + public UiAction.Description getDescription(ChangeResource rsrc) { return new UiAction.Description() .setLabel("Restore") .setTitle("Restore the change") - .setVisible(resource.getChange().getStatus() == Status.ABANDONED && canRestore); - } - - private static String status(Change change) { - return change != null ? change.getStatus().name().toLowerCase() : "deleted"; + .setVisible( + rsrc.getChange().getStatus() == Status.ABANDONED + && rsrc.permissions().database(dbProvider).testOrFalse(ChangePermission.RESTORE)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java index 14ba111..af06054 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -14,42 +14,44 @@ package com.google.gerrit.server.change; +import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE; + import com.google.common.base.Strings; import com.google.gerrit.common.TimeUtil; -import com.google.gerrit.common.data.Capable; import com.google.gerrit.extensions.api.changes.RevertInput; import com.google.gerrit.extensions.common.ChangeInfo; -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.RestApiException; -import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.webui.UiAction; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; -import com.google.gerrit.reviewdb.client.Change.Status; import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.Sequences; import com.google.gerrit.server.extensions.events.ChangeReverted; import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.git.validators.CommitValidators; import com.google.gerrit.server.mail.send.RevertedSender; +import com.google.gerrit.server.notedb.ReviewerStateInternal; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; -import com.google.gerrit.server.project.ProjectControl; -import com.google.gerrit.server.project.RefControl; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -74,15 +76,16 @@ import org.slf4j.LoggerFactory; @Singleton -public class Revert - implements RestModifyView<ChangeResource, RevertInput>, UiAction<ChangeResource> { +public class Revert extends RetryingRestModifyView<ChangeResource, RevertInput, ChangeInfo> + implements UiAction<ChangeResource> { private static final Logger log = LoggerFactory.getLogger(Revert.class); private final Provider<ReviewDb> db; + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; private final GitRepositoryManager repoManager; private final ChangeInserter.Factory changeInserterFactory; private final ChangeMessagesUtil cmUtil; - private final BatchUpdate.Factory updateFactory; private final Sequences seq; private final PatchSetUtil psUtil; private final RevertedSender.Factory revertedSenderFactory; @@ -94,10 +97,12 @@ @Inject Revert( Provider<ReviewDb> db, + PermissionBackend permissionBackend, + Provider<CurrentUser> user, GitRepositoryManager repoManager, ChangeInserter.Factory changeInserterFactory, ChangeMessagesUtil cmUtil, - BatchUpdate.Factory updateFactory, + RetryHelper retryHelper, Sequences seq, PatchSetUtil psUtil, RevertedSender.Factory revertedSenderFactory, @@ -105,11 +110,13 @@ @GerritPersonIdent PersonIdent serverIdent, ApprovalsUtil approvalsUtil, ChangeReverted changeReverted) { + super(retryHelper); this.db = db; + this.permissionBackend = permissionBackend; + this.user = user; this.repoManager = repoManager; this.changeInserterFactory = changeInserterFactory; this.cmUtil = cmUtil; - this.updateFactory = updateFactory; this.seq = seq; this.psUtil = psUtil; this.revertedSenderFactory = revertedSenderFactory; @@ -120,28 +127,24 @@ } @Override - public ChangeInfo apply(ChangeResource req, RevertInput input) - throws IOException, OrmException, RestApiException, UpdateException, NoSuchChangeException { - RefControl refControl = req.getControl().getRefControl(); - ProjectControl projectControl = req.getControl().getProjectControl(); - - Capable capable = projectControl.canPushToAtLeastOneRef(); - if (capable != Capable.OK) { - throw new AuthException(capable.getMessage()); + public ChangeInfo applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, RevertInput input) + throws IOException, OrmException, RestApiException, UpdateException, NoSuchChangeException, + PermissionBackendException { + Change change = rsrc.getChange(); + if (change.getStatus() != Change.Status.MERGED) { + throw new ResourceConflictException("change is " + ChangeUtil.status(change)); } - Change change = req.getChange(); - if (!refControl.canUpload()) { - throw new AuthException("revert not permitted"); - } else if (change.getStatus() != Status.MERGED) { - throw new ResourceConflictException("change is " + status(change)); - } + CreateChange.checkValidCLA(rsrc.getControl().getProjectControl()); + permissionBackend.user(user).ref(change.getDest()).check(CREATE_CHANGE); - Change.Id revertedChangeId = revert(req.getControl(), Strings.emptyToNull(input.message)); - return json.noOptions().format(req.getProject(), revertedChangeId); + Change.Id revertId = + revert(updateFactory, rsrc.getControl(), Strings.emptyToNull(input.message)); + return json.noOptions().format(rsrc.getProject(), revertId); } - private Change.Id revert(ChangeControl ctl, String message) + private Change.Id revert(BatchUpdate.Factory updateFactory, ChangeControl ctl, String message) throws OrmException, IOException, RestApiException, UpdateException { Change.Id changeIdToRevert = ctl.getChange().getId(); PatchSet.Id patchSetId = ctl.getChange().currentPatchSetId(); @@ -201,16 +204,21 @@ ChangeInserter ins = changeInserterFactory .create(changeId, revertCommit, ctl.getChange().getDest().get()) - .setValidatePolicy(CommitValidators.Policy.GERRIT) .setTopic(changeToRevert.getTopic()); ins.setMessage("Uploaded patch set 1."); + ReviewerSet reviewerSet = approvalsUtil.getReviewers(db.get(), ctl.getNotes()); + Set<Account.Id> reviewers = new HashSet<>(); reviewers.add(changeToRevert.getOwner()); - reviewers.addAll(approvalsUtil.getReviewers(db.get(), ctl.getNotes()).all()); + reviewers.addAll(reviewerSet.byState(ReviewerStateInternal.REVIEWER)); reviewers.remove(user.getAccountId()); ins.setReviewers(reviewers); + Set<Account.Id> ccs = new HashSet<>(reviewerSet.byState(ReviewerStateInternal.CC)); + ccs.remove(user.getAccountId()); + ins.setExtraCC(ccs); + try (BatchUpdate bu = updateFactory.create(db.get(), project, user, now)) { bu.setRepository(git, revWalk, oi); bu.insertChange(ins); @@ -225,17 +233,14 @@ } @Override - public UiAction.Description getDescription(ChangeResource resource) { + public UiAction.Description getDescription(ChangeResource rsrc) { + Change change = rsrc.getChange(); return new UiAction.Description() .setLabel("Revert") .setTitle("Revert the change") .setVisible( - resource.getChange().getStatus() == Status.MERGED - && resource.getControl().getRefControl().canUpload()); - } - - private static String status(Change change) { - return change != null ? change.getStatus().name().toLowerCase() : "deleted"; + change.getStatus() == Change.Status.MERGED + && permissionBackend.user(user).ref(change.getDest()).testOrFalse(CREATE_CHANGE)); } private class NotifyOp implements BatchUpdateOp {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java index ac7f15e..1473472 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -20,7 +20,6 @@ import com.google.common.collect.Lists; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; -import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.data.PermissionRange; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.extensions.api.changes.ReviewerInfo; @@ -29,7 +28,11 @@ import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; +import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountLoader; +import com.google.gerrit.server.permissions.LabelPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.SubmitRuleEvaluator; import com.google.gerrit.server.query.change.ChangeData; @@ -44,30 +47,42 @@ @Singleton public class ReviewerJson { private final Provider<ReviewDb> db; + private final PermissionBackend permissionBackend; private final ChangeData.Factory changeDataFactory; + private final AccountCache accountCache; private final ApprovalsUtil approvalsUtil; private final AccountLoader.Factory accountLoaderFactory; @Inject ReviewerJson( Provider<ReviewDb> db, + PermissionBackend permissionBackend, ChangeData.Factory changeDataFactory, + AccountCache accountCache, ApprovalsUtil approvalsUtil, AccountLoader.Factory accountLoaderFactory) { this.db = db; + this.permissionBackend = permissionBackend; this.changeDataFactory = changeDataFactory; + this.accountCache = accountCache; this.approvalsUtil = approvalsUtil; this.accountLoaderFactory = accountLoaderFactory; } - public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs) throws OrmException { + public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs) + throws OrmException, PermissionBackendException { List<ReviewerInfo> infos = Lists.newArrayListWithCapacity(rsrcs.size()); AccountLoader loader = accountLoaderFactory.create(true); + ChangeData cd = null; for (ReviewerResource rsrc : rsrcs) { + if (cd == null || !cd.getId().equals(rsrc.getChangeId())) { + cd = changeDataFactory.create(db.get(), rsrc.getControl().getNotes()); + } ReviewerInfo info = format( new ReviewerInfo(rsrc.getReviewerUser().getAccountId().get()), - rsrc.getReviewerControl()); + permissionBackend.user(rsrc.getReviewerUser()).database(db).change(cd), + cd); loader.put(info); infos.add(info); } @@ -75,27 +90,35 @@ return infos; } - public List<ReviewerInfo> format(ReviewerResource rsrc) throws OrmException { + public List<ReviewerInfo> format(ReviewerResource rsrc) + throws OrmException, PermissionBackendException { return format(ImmutableList.<ReviewerResource>of(rsrc)); } - public ReviewerInfo format(ReviewerInfo out, ChangeControl ctl) throws OrmException { - PatchSet.Id psId = ctl.getChange().currentPatchSetId(); + public ReviewerInfo format(ReviewerInfo out, PermissionBackend.ForChange perm, ChangeData cd) + throws OrmException, PermissionBackendException { + PatchSet.Id psId = cd.change().currentPatchSetId(); + ChangeControl ctl = cd.changeControl().forUser(perm.user()); return format( out, - ctl, - approvalsUtil.byPatchSetUser(db.get(), ctl, psId, new Account.Id(out._accountId))); + perm, + cd, + approvalsUtil.byPatchSetUser( + db.get(), ctl, psId, new Account.Id(out._accountId), null, null)); } public ReviewerInfo format( - ReviewerInfo out, ChangeControl ctl, Iterable<PatchSetApproval> approvals) - throws OrmException { - LabelTypes labelTypes = ctl.getLabelTypes(); + ReviewerInfo out, + PermissionBackend.ForChange perm, + ChangeData cd, + Iterable<PatchSetApproval> approvals) + throws OrmException, PermissionBackendException { + LabelTypes labelTypes = cd.getLabelTypes(); // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167. out.approvals = new TreeMap<>(labelTypes.nameComparator()); for (PatchSetApproval ca : approvals) { - for (PermissionRange pr : ctl.getLabelRanges()) { + for (PermissionRange pr : cd.changeControl().getLabelRanges()) { if (!pr.isEmpty()) { LabelType at = labelTypes.byLabel(ca.getLabelId()); if (at != null) { @@ -107,18 +130,22 @@ // Add dummy approvals for all permitted labels for the user even if they // do not exist in the DB. - ChangeData cd = changeDataFactory.create(db.get(), ctl); PatchSet ps = cd.currentPatchSet(); if (ps != null) { for (SubmitRecord rec : - new SubmitRuleEvaluator(cd).setFastEvalLabels(true).setAllowDraft(true).evaluate()) { + new SubmitRuleEvaluator(accountCache, cd) + .setFastEvalLabels(true) + .setAllowDraft(true) + .evaluate()) { if (rec.labels == null) { continue; } for (SubmitRecord.Label label : rec.labels) { String name = label.label; + LabelType type = labelTypes.byLabel(name); if (!out.approvals.containsKey(name) - && !ctl.getRange(Permission.forLabel(name)).isEmpty()) { + && type != null + && perm.test(new LabelPermission(type))) { out.approvals.put(name, formatValue((short) 0)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java index 6ff4a50..f6f7919 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -14,11 +14,15 @@ package com.google.gerrit.server.change; +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.restapi.RestResource; import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.project.ChangeControl; import com.google.inject.TypeLiteral; import com.google.inject.assistedinject.Assisted; @@ -36,7 +40,8 @@ private final ChangeResource change; private final RevisionResource revision; - private final IdentifiedUser user; + @Nullable private final IdentifiedUser user; + @Nullable private final Address address; @AssistedInject ReviewerResource( @@ -44,8 +49,9 @@ @Assisted ChangeResource change, @Assisted Account.Id id) { this.change = change; - this.revision = null; this.user = userFactory.create(id); + this.revision = null; + this.address = null; } @AssistedInject @@ -56,6 +62,21 @@ this.revision = revision; this.change = revision.getChangeResource(); this.user = userFactory.create(id); + this.address = null; + } + + ReviewerResource(ChangeResource change, Address address) { + this.change = change; + this.address = address; + this.revision = null; + this.user = null; + } + + ReviewerResource(RevisionResource revision, Address address) { + this.revision = revision; + this.change = revision.getChangeResource(); + this.address = address; + this.user = null; } public ChangeResource getChangeResource() { @@ -75,10 +96,28 @@ } public IdentifiedUser getReviewerUser() { + checkArgument(user != null, "no user provided"); return user; } + public Address getReviewerByEmail() { + checkArgument(address != null, "no address provided"); + return address; + } + /** + * Check if this resource was constructed by email or by {@code Account.Id}. + * + * @return true if the resource was constructed by providing an {@code Address}; false if the + * resource was constructed by providing an {@code Account.Id}. + */ + public boolean isByEmail() { + return user == null; + } + + /** + * Get the control for the caller's user. + * * @return the control for the caller's user (as opposed to the reviewer's user as returned by * {@link #getReviewerControl()}). */ @@ -87,10 +126,13 @@ } /** + * Get the control for the reviewer's user. + * * @return the control for the reviewer's user (as opposed to the caller's user as returned by * {@link #getControl()}). */ public ChangeControl getReviewerControl() { + checkArgument(user != null, "no user provided"); return change.getControl().forUser(user); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java index 14c74bc..8794083 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
@@ -25,11 +25,14 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.account.AccountsCollection; +import com.google.gerrit.server.mail.Address; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; +import java.io.IOException; import java.util.Collection; +import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton public class Reviewers implements ChildCollection<ChangeResource, ReviewerResource> { @@ -68,13 +71,28 @@ @Override public ReviewerResource parse(ChangeResource rsrc, IdString id) - throws OrmException, ResourceNotFoundException, AuthException { - Account.Id accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId(); + throws OrmException, ResourceNotFoundException, AuthException, IOException, + ConfigInvalidException { + Address address = Address.tryParse(id.get()); + Account.Id accountId = null; + try { + accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId(); + } catch (ResourceNotFoundException e) { + if (address == null) { + throw e; + } + } // See if the id exists as a reviewer for this change - if (fetchAccountIds(rsrc).contains(accountId)) { + if (accountId != null && fetchAccountIds(rsrc).contains(accountId)) { return resourceFactory.create(rsrc, accountId); } + + // See if the address exists as a reviewer on the change + if (address != null && rsrc.getNotes().getReviewersByEmail().all().contains(address)) { + return new ReviewerResource(rsrc, address); + } + throw new ResourceNotFoundException(id); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java index 4d35f9e..a582e2c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
@@ -14,6 +14,8 @@ package com.google.gerrit.server.change; +import com.google.common.hash.Hasher; +import com.google.common.hash.Hashing; import com.google.gerrit.extensions.restapi.RestResource; import com.google.gerrit.extensions.restapi.RestResource.HasETag; import com.google.gerrit.extensions.restapi.RestView; @@ -24,6 +26,7 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.edit.ChangeEdit; import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.project.ChangeControl; import com.google.inject.TypeLiteral; import java.util.Optional; @@ -51,6 +54,10 @@ return cacheable; } + public PermissionBackend.ForChange permissions() { + return change.permissions(); + } + public ChangeResource getChangeResource() { return change; } @@ -77,10 +84,15 @@ @Override public String getETag() { - // 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. - return change.getETag(); + Hasher h = Hashing.murmur3_128().newHasher(); + prepareETag(h, getUser()); + return h.hash().toString(); + } + + 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); } Account.Id getAccountId() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java index d3623cf..be8bce0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionReviewers.java
@@ -26,11 +26,14 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.account.AccountsCollection; +import com.google.gerrit.server.mail.Address; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; +import java.io.IOException; import java.util.Collection; +import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton public class RevisionReviewers implements ChildCollection<RevisionResource, ReviewerResource> { @@ -69,18 +72,33 @@ @Override public ReviewerResource parse(RevisionResource rsrc, IdString id) - throws OrmException, ResourceNotFoundException, AuthException, MethodNotAllowedException { + throws OrmException, ResourceNotFoundException, AuthException, MethodNotAllowedException, + IOException, ConfigInvalidException { if (!rsrc.isCurrent()) { throw new MethodNotAllowedException("Cannot access on non-current patch set"); } + Address address = Address.tryParse(id.get()); - Account.Id accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId(); - + Account.Id accountId = null; + try { + accountId = accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId(); + } catch (ResourceNotFoundException e) { + if (address == null) { + throw e; + } + } Collection<Account.Id> reviewers = approvalsUtil.getReviewers(dbProvider.get(), rsrc.getNotes()).all(); + // See if the id exists as a reviewer for this change if (reviewers.contains(accountId)) { return resourceFactory.create(rsrc, accountId); } + + // See if the address exists as a reviewer on the change + if (address != null && rsrc.getNotes().getReviewersByEmail().all().contains(address)) { + return new ReviewerResource(rsrc, address); + } + throw new ResourceNotFoundException(id); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java index a16f2f9..851202b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
@@ -37,6 +37,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import org.eclipse.jgit.lib.ObjectId; @Singleton public class Revisions implements ChildCollection<ChangeResource, RevisionResource> { @@ -70,7 +71,7 @@ @Override public RevisionResource parse(ChangeResource change, IdString id) throws ResourceNotFoundException, AuthException, OrmException, IOException { - if (id.equals("current")) { + if (id.get().equals("current")) { PatchSet ps = psUtil.current(dbProvider.get(), change.getNotes()); if (ps != null && visible(change, ps)) { return new RevisionResource(change, ps).doNotCache(); @@ -143,8 +144,9 @@ Optional<ChangeEdit> edit = editUtil.byChange(change.getChange()); if (edit.isPresent()) { PatchSet ps = new PatchSet(new PatchSet.Id(change.getId(), 0)); - ps.setRevision(edit.get().getRevision()); - if (revid == null || edit.get().getRevision().equals(revid)) { + RevId editRevId = new RevId(ObjectId.toString(edit.get().getEditCommit())); + ps.setRevision(editRevId); + if (revid == null || editRevId.equals(revid)) { return Collections.singletonList(new RevisionResource(change, ps, edit)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java index 409be9d..73a6c60 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetAssigneeOp.java
@@ -17,16 +17,12 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.gerrit.extensions.registration.DynamicSet; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.extensions.restapi.UnprocessableEntityException; -import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.account.AccountsCollection; import com.google.gerrit.server.extensions.events.AssigneeChanged; import com.google.gerrit.server.mail.send.SetAssigneeSender; import com.google.gerrit.server.notedb.ChangeUpdate; @@ -36,9 +32,9 @@ import com.google.gerrit.server.validators.AssigneeValidationListener; import com.google.gerrit.server.validators.ValidationException; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,93 +42,74 @@ private static final Logger log = LoggerFactory.getLogger(SetAssigneeOp.class); public interface Factory { - SetAssigneeOp create(String assignee); + SetAssigneeOp create(IdentifiedUser assignee); } - private final AccountsCollection accounts; private final ChangeMessagesUtil cmUtil; private final DynamicSet<AssigneeValidationListener> validationListeners; - private final String assignee; + private final IdentifiedUser newAssignee; private final AssigneeChanged assigneeChanged; private final SetAssigneeSender.Factory setAssigneeSenderFactory; private final Provider<IdentifiedUser> user; private final IdentifiedUser.GenericFactory userFactory; private Change change; - private Account newAssignee; - private Account oldAssignee; + private IdentifiedUser oldAssignee; - @AssistedInject + @Inject SetAssigneeOp( - AccountsCollection accounts, ChangeMessagesUtil cmUtil, DynamicSet<AssigneeValidationListener> validationListeners, AssigneeChanged assigneeChanged, SetAssigneeSender.Factory setAssigneeSenderFactory, Provider<IdentifiedUser> user, IdentifiedUser.GenericFactory userFactory, - @Assisted String assignee) { - this.accounts = accounts; + @Assisted IdentifiedUser newAssignee) { this.cmUtil = cmUtil; this.validationListeners = validationListeners; this.assigneeChanged = assigneeChanged; this.setAssigneeSenderFactory = setAssigneeSenderFactory; this.user = user; this.userFactory = userFactory; - this.assignee = checkNotNull(assignee); + this.newAssignee = checkNotNull(newAssignee, "assignee"); } @Override public boolean updateChange(ChangeContext ctx) throws OrmException, RestApiException { change = ctx.getChange(); - ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); - IdentifiedUser newAssigneeUser = accounts.parse(assignee); - newAssignee = newAssigneeUser.getAccount(); - IdentifiedUser oldAssigneeUser = null; - if (change.getAssignee() != null) { - oldAssigneeUser = userFactory.create(change.getAssignee()); - oldAssignee = oldAssigneeUser.getAccount(); - if (newAssignee.equals(oldAssignee)) { - return false; - } - } - if (!newAssignee.isActive()) { - throw new UnprocessableEntityException( - String.format("Account of %s is not active", assignee)); - } - if (!ctx.getControl().forUser(newAssigneeUser).isRefVisible()) { - throw new AuthException( - String.format("Change %s is not visible to %s.", change.getChangeId(), assignee)); + if (newAssignee.getAccountId().equals(change.getAssignee())) { + return false; } try { for (AssigneeValidationListener validator : validationListeners) { - validator.validateAssignee(change, newAssignee); + validator.validateAssignee(change, newAssignee.getAccount()); } } catch (ValidationException e) { throw new ResourceConflictException(e.getMessage()); } + + if (change.getAssignee() != null) { + oldAssignee = userFactory.create(change.getAssignee()); + } + + ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); // notedb - update.setAssignee(newAssignee.getId()); + update.setAssignee(newAssignee.getAccountId()); // reviewdb - change.setAssignee(newAssignee.getId()); - addMessage(ctx, update, oldAssigneeUser, newAssigneeUser); + change.setAssignee(newAssignee.getAccountId()); + addMessage(ctx, update); return true; } - private void addMessage( - ChangeContext ctx, - ChangeUpdate update, - IdentifiedUser previousAssignee, - IdentifiedUser newAssignee) - throws OrmException { + private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException { StringBuilder msg = new StringBuilder(); msg.append("Assignee "); - if (previousAssignee == null) { + if (oldAssignee == null) { msg.append("added: "); msg.append(newAssignee.getNameEmail()); } else { msg.append("changed from: "); - msg.append(previousAssignee.getNameEmail()); + msg.append(oldAssignee.getNameEmail()); msg.append(" to: "); msg.append(newAssignee.getNameEmail()); } @@ -145,16 +122,17 @@ public void postUpdate(Context ctx) throws OrmException { try { SetAssigneeSender cm = - setAssigneeSenderFactory.create(change.getProject(), change.getId(), newAssignee.getId()); + setAssigneeSenderFactory.create( + change.getProject(), change.getId(), newAssignee.getAccountId()); cm.setFrom(user.get().getAccountId()); cm.send(); } catch (Exception err) { log.error("Cannot send email to new assignee of change " + change.getId(), err); } - assigneeChanged.fire(change, ctx.getAccount(), oldAssignee, ctx.getWhen()); - } - - public Account.Id getNewAssignee() { - return newAssignee != null ? newAssignee.getId() : null; + assigneeChanged.fire( + change, + ctx.getAccount(), + oldAssignee != null ? oldAssignee.getAccount() : null, + ctx.getWhen()); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java index 0e78c18..724598a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetHashtagsOp.java
@@ -39,8 +39,8 @@ import com.google.gerrit.server.validators.HashtagValidationListener; import com.google.gerrit.server.validators.ValidationException; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.io.IOException; import java.util.Collection; import java.util.HashSet; @@ -64,7 +64,7 @@ private Set<String> toRemove; private ImmutableSortedSet<String> updatedHashtags; - @AssistedInject + @Inject SetHashtagsOp( NotesMigration notesMigration, ChangeMessagesUtil cmUtil, @@ -94,9 +94,7 @@ updatedHashtags = ImmutableSortedSet.of(); return false; } - if (!ctx.getControl().canEditHashtags()) { - throw new AuthException("Editing hashtags not permitted"); - } + change = ctx.getChange(); ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); ChangeNotes notes = update.getNotes().load();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java new file mode 100644 index 0000000..d0bb70b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java
@@ -0,0 +1,78 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.base.Strings; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.ChangeMessage; +import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.ChangeContext; +import com.google.gwtorm.server.OrmException; + +public class SetPrivateOp implements BatchUpdateOp { + public static class Input { + String message; + + public Input() {} + + public Input(String message) { + this.message = message; + } + } + + private final ChangeMessagesUtil cmUtil; + private final boolean isPrivate; + private final Input input; + + SetPrivateOp(ChangeMessagesUtil cmUtil, boolean isPrivate, Input input) { + this.cmUtil = cmUtil; + this.isPrivate = isPrivate; + this.input = input; + } + + @Override + public boolean updateChange(ChangeContext ctx) throws ResourceConflictException, OrmException { + Change change = ctx.getChange(); + ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); + change.setPrivate(isPrivate); + change.setLastUpdatedOn(ctx.getWhen()); + update.setPrivate(isPrivate); + addMessage(ctx, update); + return true; + } + + private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException { + Change c = ctx.getChange(); + StringBuilder buf = new StringBuilder(c.isPrivate() ? "Set private" : "Unset private"); + + String m = Strings.nullToEmpty(input == null ? null : input.message).trim(); + if (!m.isEmpty()) { + buf.append("\n\n"); + buf.append(m); + } + + ChangeMessage cmsg = + ChangeMessagesUtil.newMessage( + ctx, + buf.toString(), + c.isPrivate() + ? ChangeMessagesUtil.TAG_SET_PRIVATE + : ChangeMessagesUtil.TAG_UNSET_PRIVATE); + cmUtil.addChangeMessage(ctx.getDb(), update, cmsg); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetReadyForReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetReadyForReview.java new file mode 100644 index 0000000..eb61d3c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetReadyForReview.java
@@ -0,0 +1,85 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.TimeUtil; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Change.Status; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.change.WorkInProgressOp.Input; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; +import com.google.gerrit.server.update.UpdateException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +@Singleton +public class SetReadyForReview extends RetryingRestModifyView<ChangeResource, Input, Response<?>> + implements UiAction<ChangeResource> { + private final WorkInProgressOp.Factory opFactory; + private final Provider<ReviewDb> db; + + @Inject + SetReadyForReview( + RetryHelper retryHelper, WorkInProgressOp.Factory opFactory, Provider<ReviewDb> db) { + super(retryHelper); + this.opFactory = opFactory; + this.db = db; + } + + @Override + protected Response<?> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input) + throws RestApiException, UpdateException { + Change change = rsrc.getChange(); + if (!rsrc.isUserOwner()) { + throw new AuthException("not allowed to set ready for review"); + } + + if (change.getStatus() != Status.NEW) { + throw new ResourceConflictException("change is " + ChangeUtil.status(change)); + } + + if (!change.isWorkInProgress()) { + throw new ResourceConflictException("change is not work in progress"); + } + + try (BatchUpdate bu = + updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { + bu.addOp(rsrc.getChange().getId(), opFactory.create(false, input)); + bu.execute(); + return Response.ok(""); + } + } + + @Override + public Description getDescription(ChangeResource rsrc) { + return new Description() + .setLabel("Ready") + .setTitle("Set Ready For Review") + .setVisible( + rsrc.isUserOwner() + && rsrc.getChange().getStatus() == Status.NEW + && rsrc.getChange().isWorkInProgress()); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetWorkInProgress.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetWorkInProgress.java new file mode 100644 index 0000000..565f67f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetWorkInProgress.java
@@ -0,0 +1,85 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.TimeUtil; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Change.Status; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.ChangeUtil; +import com.google.gerrit.server.change.WorkInProgressOp.Input; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; +import com.google.gerrit.server.update.UpdateException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +@Singleton +public class SetWorkInProgress extends RetryingRestModifyView<ChangeResource, Input, Response<?>> + implements UiAction<ChangeResource> { + private final WorkInProgressOp.Factory opFactory; + private final Provider<ReviewDb> db; + + @Inject + SetWorkInProgress( + WorkInProgressOp.Factory opFactory, RetryHelper retryHelper, Provider<ReviewDb> db) { + super(retryHelper); + this.opFactory = opFactory; + this.db = db; + } + + @Override + protected Response<?> applyImpl( + BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input) + throws RestApiException, UpdateException { + Change change = rsrc.getChange(); + if (!rsrc.isUserOwner()) { + throw new AuthException("not allowed to set work in progress"); + } + + if (change.getStatus() != Status.NEW) { + throw new ResourceConflictException("change is " + ChangeUtil.status(change)); + } + + if (change.isWorkInProgress()) { + throw new ResourceConflictException("change is already work in progress"); + } + + try (BatchUpdate bu = + updateFactory.create(db.get(), rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) { + bu.addOp(rsrc.getChange().getId(), opFactory.create(true, input)); + bu.execute(); + return Response.ok(""); + } + } + + @Override + public Description getDescription(ChangeResource rsrc) { + return new Description() + .setLabel("WIP") + .setTitle("Set Work In Progress") + .setVisible( + rsrc.isUserOwner() + && rsrc.getChange().getStatus() == Status.NEW + && !rsrc.getChange().isWorkInProgress()); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java index 01020fa..3dd467a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -40,6 +40,7 @@ import com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PatchSetUtil; @@ -51,10 +52,13 @@ import com.google.gerrit.server.git.MergeOp; import com.google.gerrit.server.git.MergeSuperSet; import com.google.gerrit.server.notedb.ChangeNotes; -import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.InternalChangeQuery; +import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmRuntimeException; import com.google.inject.Inject; @@ -62,11 +66,14 @@ import com.google.inject.Singleton; import java.io.IOException; import java.util.Collection; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.ObjectId; @@ -93,6 +100,7 @@ "This change depends on other changes which are not ready"; private static final String BLOCKED_HIDDEN_SUBMIT_TOOLTIP = "This change depends on other hidden changes which are not ready"; + private static final String BLOCKED_WORK_IN_PROGRESS = "This change is marked work in progress"; private static final String CLICK_FAILURE_TOOLTIP = "Clicking the button would fail"; private static final String CHANGE_UNMERGEABLE = "Problems with integrating this change"; private static final String CHANGES_NOT_MERGEABLE = "Problems with change(s): "; @@ -111,24 +119,25 @@ */ @VisibleForTesting public static class TestSubmitInput extends SubmitInput { - public final boolean failAfterRefUpdates; + public boolean failAfterRefUpdates; - public TestSubmitInput(SubmitInput base, boolean failAfterRefUpdates) { - this.onBehalfOf = base.onBehalfOf; - this.notify = base.notify; - this.failAfterRefUpdates = failAfterRefUpdates; - } + /** + * For each change being submitted, an element is removed from this queue and, if the value is + * true, a bogus ref update is added to the batch, in order to generate a lock failure during + * execution. + */ + public Queue<Boolean> generateLockFailures; } private final Provider<ReviewDb> dbProvider; private final GitRepositoryManager repoManager; + private final PermissionBackend permissionBackend; private final ChangeData.Factory changeDataFactory; private final ChangeMessagesUtil cmUtil; private final ChangeNotes.Factory changeNotesFactory; private final Provider<MergeOp> mergeOpProvider; private final Provider<MergeSuperSet> mergeSuperSet; private final AccountsCollection accounts; - private final ChangesCollection changes; private final String label; private final String labelWithParents; private final ParameterizedString titlePattern; @@ -143,25 +152,25 @@ Submit( Provider<ReviewDb> dbProvider, GitRepositoryManager repoManager, + PermissionBackend permissionBackend, ChangeData.Factory changeDataFactory, ChangeMessagesUtil cmUtil, ChangeNotes.Factory changeNotesFactory, Provider<MergeOp> mergeOpProvider, Provider<MergeSuperSet> mergeSuperSet, AccountsCollection accounts, - ChangesCollection changes, @GerritServerConfig Config cfg, Provider<InternalChangeQuery> queryProvider, PatchSetUtil psUtil) { this.dbProvider = dbProvider; this.repoManager = repoManager; + this.permissionBackend = permissionBackend; this.changeDataFactory = changeDataFactory; this.cmUtil = cmUtil; this.changeNotesFactory = changeNotesFactory; this.mergeOpProvider = mergeOpProvider; this.mergeSuperSet = mergeSuperSet; this.accounts = accounts; - this.changes = changes; this.label = MoreObjects.firstNonNull( Strings.emptyToNull(cfg.getString("change", null, "submitLabel")), "Submit"); @@ -193,18 +202,25 @@ @Override public Output apply(RevisionResource rsrc, SubmitInput input) - throws RestApiException, RepositoryNotFoundException, IOException, OrmException { + throws RestApiException, RepositoryNotFoundException, IOException, OrmException, + PermissionBackendException, UpdateException, ConfigInvalidException { input.onBehalfOf = Strings.emptyToNull(input.onBehalfOf); + IdentifiedUser submitter; if (input.onBehalfOf != null) { - rsrc = onBehalfOf(rsrc, input); + submitter = onBehalfOf(rsrc, input); + } else { + rsrc.permissions().check(ChangePermission.SUBMIT); + submitter = rsrc.getUser().asIdentifiedUser(); } - ChangeControl control = rsrc.getControl(); - IdentifiedUser caller = control.getUser().asIdentifiedUser(); + + return new Output(mergeChange(rsrc, submitter, input)); + } + + public Change mergeChange(RevisionResource rsrc, IdentifiedUser submitter, SubmitInput input) + throws OrmException, RestApiException, IOException, UpdateException, ConfigInvalidException { Change change = rsrc.getChange(); - if (input.onBehalfOf == null && !control.canSubmit()) { - throw new AuthException("submit not permitted"); - } else if (!change.getStatus().isOpen()) { - throw new ResourceConflictException("change is " + status(change)); + if (!change.getStatus().isOpen()) { + throw new ResourceConflictException("change is " + ChangeUtil.status(change)); } else if (!ProjectUtil.branchExists(repoManager, change.getDest())) { throw new ResourceConflictException( String.format("destination branch \"%s\" not found.", change.getDest().get())); @@ -217,7 +233,7 @@ try (MergeOp op = mergeOpProvider.get()) { ReviewDb db = dbProvider.get(); - op.merge(db, change, caller, true, input, false); + op.merge(db, change, submitter, true, input, false); try { change = changeNotesFactory.createChecked(db, change.getProject(), change.getId()).getChange(); @@ -228,17 +244,17 @@ switch (change.getStatus()) { case MERGED: - return new Output(change); + return change; case NEW: ChangeMessage msg = getConflictMessage(rsrc); if (msg != null) { throw new ResourceConflictException(msg.getMessage()); } - //$FALL-THROUGH$ + // $FALL-THROUGH$ case ABANDONED: case DRAFT: default: - throw new ResourceConflictException("change is " + status(change)); + throw new ResourceConflictException("change is " + ChangeUtil.status(change)); } } @@ -250,21 +266,26 @@ */ private String problemsForSubmittingChangeset(ChangeData cd, ChangeSet cs, CurrentUser user) { try { - @SuppressWarnings("resource") - ReviewDb db = dbProvider.get(); if (cs.furtherHiddenChanges()) { return BLOCKED_HIDDEN_SUBMIT_TOOLTIP; } for (ChangeData c : cs.changes()) { - ChangeControl changeControl = c.changeControl(user); - - if (!changeControl.isVisible(db)) { + Set<ChangePermission> can = + permissionBackend + .user(user) + .database(dbProvider) + .change(c) + .test(EnumSet.of(ChangePermission.READ, ChangePermission.SUBMIT)); + if (!can.contains(ChangePermission.READ)) { return BLOCKED_HIDDEN_SUBMIT_TOOLTIP; } - if (!changeControl.canSubmit()) { + if (!can.contains(ChangePermission.SUBMIT)) { return BLOCKED_SUBMIT_TOOLTIP; } - MergeOp.checkSubmitRule(c); + if (c.change().isWorkInProgress()) { + return BLOCKED_WORK_IN_PROGRESS; + } + MergeOp.checkSubmitRule(c, false); } Collection<ChangeData> unmergeable = unmergeableChanges(cs); @@ -281,7 +302,7 @@ } } catch (ResourceConflictException e) { return BLOCKED_SUBMIT_TOOLTIP; - } catch (OrmException | IOException e) { + } catch (PermissionBackendException | OrmException | IOException e) { log.error("Error checking if change is submittable", e); throw new OrmRuntimeException("Could not determine problems for the change", e); } @@ -290,20 +311,23 @@ @Override public UiAction.Description getDescription(RevisionResource resource) { - PatchSet.Id current = resource.getChange().currentPatchSetId(); - String topic = resource.getChange().getTopic(); - boolean visible = - !resource.getPatchSet().isDraft() - && resource.getChange().getStatus().isOpen() - && resource.getPatchSet().getId().equals(current) - && resource.getControl().canSubmit(); + Change change = resource.getChange(); + String topic = change.getTopic(); ReviewDb db = dbProvider.get(); ChangeData cd = changeDataFactory.create(db, resource.getControl()); - + boolean visible; try { - MergeOp.checkSubmitRule(cd); + visible = + change.getStatus().isOpen() + && resource.isCurrent() + && !resource.getPatchSet().isDraft() + && resource.permissions().test(ChangePermission.SUBMIT); + MergeOp.checkSubmitRule(cd, false); } catch (ResourceConflictException e) { visible = false; + } catch (PermissionBackendException e) { + log.error("Error checking if change is submittable", e); + throw new OrmRuntimeException("Could not check submit permission", e); } catch (OrmException e) { log.error("Error checking if change is submittable", e); throw new OrmRuntimeException("Could not determine problems for the change", e); @@ -367,7 +391,7 @@ Map<String, String> params = ImmutableMap.of( "patchSet", String.valueOf(resource.getPatchSet().getPatchSetId()), - "branch", resource.getChange().getDest().getShortName(), + "branch", change.getDest().getShortName(), "commit", ObjectId.fromString(revId.get()).abbreviate(7).name(), "submitSize", String.valueOf(cs.size())); ParameterizedString tp = cs.size() > 1 ? titlePatternWithAncestors : titlePattern; @@ -390,10 +414,6 @@ .orNull(); } - static String status(Change change) { - return change != null ? change.getStatus().name().toLowerCase() : "deleted"; - } - public Collection<ChangeData> unmergeableChanges(ChangeSet cs) throws OrmException, IOException { Set<ChangeData> mergeabilityMap = new HashSet<>(); for (ChangeData change : cs.changes()) { @@ -458,24 +478,22 @@ return commits; } - private RevisionResource onBehalfOf(RevisionResource rsrc, SubmitInput in) - throws AuthException, UnprocessableEntityException, OrmException { - ChangeControl caller = rsrc.getControl(); - if (!caller.canSubmit()) { - throw new AuthException("submit not permitted"); - } - if (!caller.canSubmitAs()) { - throw new AuthException("submit on behalf of not permitted"); - } - ChangeControl target = - caller.forUser(accounts.parseOnBehalfOf(caller.getUser(), in.onBehalfOf)); - if (!target.getRefControl().isVisible()) { + private IdentifiedUser onBehalfOf(RevisionResource rsrc, SubmitInput in) + throws AuthException, UnprocessableEntityException, OrmException, PermissionBackendException, + IOException, ConfigInvalidException { + PermissionBackend.ForChange perm = rsrc.permissions().database(dbProvider); + perm.check(ChangePermission.SUBMIT); + perm.check(ChangePermission.SUBMIT_AS); + + CurrentUser caller = rsrc.getUser(); + IdentifiedUser submitter = accounts.parseOnBehalfOf(caller, in.onBehalfOf); + try { + perm.user(submitter).check(ChangePermission.READ); + } catch (AuthException e) { throw new UnprocessableEntityException( - String.format( - "on_behalf_of account %s cannot see destination ref", - target.getUser().getAccountId())); + String.format("on_behalf_of account %s cannot see change", submitter.getAccountId())); } - return new RevisionResource(changes.parse(target), rsrc.getPatchSet()); + return submitter; } public static boolean wholeTopicEnabled(Config config) { @@ -510,7 +528,8 @@ @Override public ChangeInfo apply(ChangeResource rsrc, SubmitInput input) - throws RestApiException, RepositoryNotFoundException, IOException, OrmException { + throws RestApiException, RepositoryNotFoundException, IOException, OrmException, + PermissionBackendException, UpdateException, ConfigInvalidException { PatchSet ps = psUtil.current(dbProvider.get(), rsrc.getNotes()); if (ps == null) { throw new ResourceConflictException("current revision is missing");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java index 5260730..178aeea 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
@@ -27,11 +27,14 @@ import com.google.gerrit.server.ReviewersUtil.VisibilityControl; import com.google.gerrit.server.account.AccountVisibility; import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.RefPermission; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import java.io.IOException; import java.util.List; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; import org.kohsuke.args4j.Option; @@ -45,6 +48,7 @@ ) boolean excludeGroups; + private final PermissionBackend permissionBackend; private final Provider<CurrentUser> self; @Inject @@ -52,16 +56,18 @@ AccountVisibility av, GenericFactory identifiedUserFactory, Provider<ReviewDb> dbProvider, + PermissionBackend permissionBackend, Provider<CurrentUser> self, @GerritServerConfig Config cfg, ReviewersUtil reviewersUtil) { super(av, identifiedUserFactory, dbProvider, cfg, reviewersUtil); + this.permissionBackend = permissionBackend; this.self = self; } @Override public List<SuggestedReviewerInfo> apply(ChangeResource rsrc) - throws AuthException, BadRequestException, OrmException, IOException { + throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException { if (!self.get().isIdentifiedUser()) { throw new AuthException("Authentication required"); } @@ -73,7 +79,7 @@ excludeGroups); } - private VisibilityControl getVisibility(final ChangeResource rsrc) { + private VisibilityControl getVisibility(ChangeResource rsrc) { if (rsrc.getControl().getRefControl().isVisibleByRegisteredUsers()) { return new VisibilityControl() { @Override @@ -82,13 +88,15 @@ } }; } + + // Use the destination reference, not the change, as drafts may deny + // anyone who is not already a reviewer. + PermissionBackend.ForRef perm = permissionBackend.user(self).ref(rsrc.getChange().getDest()); return new VisibilityControl() { @Override public boolean isVisibleTo(Account.Id account) throws OrmException { IdentifiedUser who = identifiedUserFactory.create(account); - // we can't use changeControl directly as it won't suggest reviewers - // to drafts - return rsrc.getControl().forUser(who).isRefVisible(); + return perm.user(who).testOrFalse(RefPermission.READ); } }; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java index 524f4d6..0aecd86 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
@@ -24,6 +24,7 @@ import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.rules.RulesCache; +import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountLoader; import com.google.gerrit.server.project.SubmitRuleEvaluator; import com.google.gerrit.server.query.change.ChangeData; @@ -39,6 +40,7 @@ private final Provider<ReviewDb> db; private final ChangeData.Factory changeDataFactory; private final RulesCache rules; + private final AccountCache accountCache; private final AccountLoader.Factory accountInfoFactory; @Option(name = "--filters", usage = "impact of filters in parent projects") @@ -49,10 +51,12 @@ Provider<ReviewDb> db, ChangeData.Factory changeDataFactory, RulesCache rules, + AccountCache accountCache, AccountLoader.Factory infoFactory) { this.db = db; this.changeDataFactory = changeDataFactory; this.rules = rules; + this.accountCache = accountCache; this.accountInfoFactory = infoFactory; } @@ -67,7 +71,8 @@ } input.filters = MoreObjects.firstNonNull(input.filters, filters); SubmitRuleEvaluator evaluator = - new SubmitRuleEvaluator(changeDataFactory.create(db.get(), rsrc.getControl())); + new SubmitRuleEvaluator( + accountCache, changeDataFactory.create(db.get(), rsrc.getControl())); List<SubmitRecord> records = evaluator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java index b19f1d1..ac0b749 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
@@ -25,6 +25,7 @@ import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.rules.RulesCache; +import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.project.SubmitRuleEvaluator; import com.google.gerrit.server.query.change.ChangeData; import com.google.gwtorm.server.OrmException; @@ -34,6 +35,7 @@ public class TestSubmitType implements RestModifyView<RevisionResource, TestSubmitRuleInput> { private final Provider<ReviewDb> db; + private final AccountCache accountCache; private final ChangeData.Factory changeDataFactory; private final RulesCache rules; @@ -41,8 +43,13 @@ private Filters filters = Filters.RUN; @Inject - TestSubmitType(Provider<ReviewDb> db, ChangeData.Factory changeDataFactory, RulesCache rules) { + TestSubmitType( + Provider<ReviewDb> db, + AccountCache accountCache, + ChangeData.Factory changeDataFactory, + RulesCache rules) { this.db = db; + this.accountCache = accountCache; this.changeDataFactory = changeDataFactory; this.rules = rules; } @@ -58,7 +65,8 @@ } input.filters = MoreObjects.firstNonNull(input.filters, filters); SubmitRuleEvaluator evaluator = - new SubmitRuleEvaluator(changeDataFactory.create(db.get(), rsrc.getControl())); + new SubmitRuleEvaluator( + accountCache, changeDataFactory.create(db.get(), rsrc.getControl())); SubmitTypeRecord rec = evaluator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unignore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unignore.java new file mode 100644 index 0000000..081fc22 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unignore.java
@@ -0,0 +1,76 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.StarredChangesUtil; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class Unignore + implements RestModifyView<ChangeResource, Unignore.Input>, UiAction<ChangeResource> { + private static final Logger log = LoggerFactory.getLogger(Unignore.class); + + public static class Input {} + + private final Provider<IdentifiedUser> self; + private final StarredChangesUtil stars; + + @Inject + Unignore(Provider<IdentifiedUser> self, StarredChangesUtil stars) { + this.self = self; + this.stars = stars; + } + + @Override + public Description getDescription(ChangeResource rsrc) { + return new UiAction.Description() + .setLabel("Unignore") + .setTitle("Unignore the change") + .setVisible(!rsrc.isUserOwner() && isIgnored(rsrc)); + } + + @Override + public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException { + try { + if (rsrc.isUserOwner() || !isIgnored(rsrc)) { + // early exit for own changes and not ignored changes + return Response.ok(""); + } + stars.unignore(self.get().getAccountId(), rsrc.getProject(), rsrc.getChange().getId()); + } catch (OrmException e) { + throw new RestApiException("failed to unignore change", e); + } + return Response.ok(""); + } + + private boolean isIgnored(ChangeResource rsrc) { + try { + return stars.isIgnoredBy(rsrc.getChange().getId(), self.get().getAccountId()); + } catch (OrmException e) { + log.error("failed to check ignored star", e); + } + return false; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java new file mode 100644 index 0000000..49b41cb --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Unmute.java
@@ -0,0 +1,86 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.webui.UiAction; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.StarredChangesUtil; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class Unmute + implements RestModifyView<ChangeResource, Unmute.Input>, UiAction<ChangeResource> { + private static final Logger log = LoggerFactory.getLogger(Unmute.class); + + public static class Input {} + + private final Provider<IdentifiedUser> self; + private final StarredChangesUtil stars; + + @Inject + Unmute(Provider<IdentifiedUser> self, StarredChangesUtil stars) { + this.self = self; + this.stars = stars; + } + + @Override + public Description getDescription(ChangeResource rsrc) { + return new UiAction.Description() + .setLabel("Unmute") + .setTitle("Unmute the change") + .setVisible(!rsrc.isUserOwner() && isUnMuteable(rsrc.getChange())); + } + + @Override + public Response<String> apply(ChangeResource rsrc, Input input) throws RestApiException { + try { + if (rsrc.isUserOwner() || !isMuted(rsrc.getChange())) { + // early exit for own changes and not muted changes + return Response.ok(""); + } + stars.unmute(self.get().getAccountId(), rsrc.getProject(), rsrc.getChange()); + } catch (OrmException e) { + throw new RestApiException("failed to unmute change", e); + } + return Response.ok(""); + } + + private boolean isMuted(Change change) { + try { + return stars.isMutedBy(change, self.get().getAccountId()); + } catch (OrmException e) { + log.error("failed to check muted star", e); + } + return false; + } + + private boolean isUnMuteable(Change change) { + try { + return isMuted(change) && !stars.isIgnoredBy(change.getId(), self.get().getAccountId()); + } catch (OrmException e) { + log.error("failed to check ignored star", e); + } + return false; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java index b2ca405..ddf48fd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Votes.java
@@ -86,7 +86,9 @@ db.get(), rsrc.getControl(), rsrc.getChange().currentPatchSetId(), - rsrc.getReviewerUser().getAccountId()); + rsrc.getReviewerUser().getAccountId(), + null, + null); for (PatchSetApproval psa : byPatchSetUser) { votes.put(psa.getLabel(), psa.getValue()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/WorkInProgressOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/WorkInProgressOp.java new file mode 100644 index 0000000..5c27936 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -0,0 +1,132 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.gerrit.extensions.api.changes.NotifyHandling; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.ChangeMessage; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.Context; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; + +/* Set work in progress or ready for review state on a change */ +public class WorkInProgressOp implements BatchUpdateOp { + public static class Input { + String message; + + public Input() {} + + public Input(String message) { + this.message = message; + } + } + + public interface Factory { + WorkInProgressOp create(boolean workInProgress, Input in); + } + + private final ChangeMessagesUtil cmUtil; + private final EmailReviewComments.Factory email; + private final PatchSetUtil psUtil; + private final boolean workInProgress; + private final Input in; + + private Change change; + private ChangeNotes notes; + private PatchSet ps; + private ChangeMessage cmsg; + + @Inject + WorkInProgressOp( + ChangeMessagesUtil cmUtil, + EmailReviewComments.Factory email, + PatchSetUtil psUtil, + @Assisted boolean workInProgress, + @Assisted Input in) { + this.cmUtil = cmUtil; + this.email = email; + this.psUtil = psUtil; + this.workInProgress = workInProgress; + this.in = in; + } + + @Override + public boolean updateChange(ChangeContext ctx) throws OrmException { + change = ctx.getChange(); + notes = ctx.getNotes(); + ps = psUtil.get(ctx.getDb(), ctx.getNotes(), change.currentPatchSetId()); + ChangeUpdate update = ctx.getUpdate(change.currentPatchSetId()); + change.setWorkInProgress(workInProgress); + if (!change.hasReviewStarted() && !workInProgress) { + change.setReviewStarted(true); + } + change.setLastUpdatedOn(ctx.getWhen()); + update.setWorkInProgress(workInProgress); + addMessage(ctx, update); + return true; + } + + private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException { + Change c = ctx.getChange(); + StringBuilder buf = + new StringBuilder(c.isWorkInProgress() ? "Set Work In Progress" : "Set Ready For Review"); + + String m = Strings.nullToEmpty(in == null ? null : in.message).trim(); + if (!m.isEmpty()) { + buf.append("\n\n"); + buf.append(m); + } + + cmsg = + ChangeMessagesUtil.newMessage( + ctx, + buf.toString(), + c.isWorkInProgress() + ? ChangeMessagesUtil.TAG_SET_WIP + : ChangeMessagesUtil.TAG_SET_READY); + + cmUtil.addChangeMessage(ctx.getDb(), update, cmsg); + } + + @Override + public void postUpdate(Context ctx) { + if (workInProgress) { + return; + } + email + .create( + NotifyHandling.ALL, + ImmutableListMultimap.of(), + notes, + ps, + ctx.getIdentifiedUser(), + cmsg, + ImmutableList.of(), + cmsg.getMessage(), + ImmutableList.of()) + .sendAsync(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardNameProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardNameProvider.java index 3f3d6fd..79676f6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardNameProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardNameProvider.java
@@ -24,7 +24,7 @@ private final String anonymousCoward; @Inject - public AnonymousCowardNameProvider(@GerritServerConfig final Config cfg) { + public AnonymousCowardNameProvider(@GerritServerConfig Config cfg) { String anonymousCoward = cfg.getString("user", null, "anonymousCoward"); if (anonymousCoward == null) { anonymousCoward = DEFAULT;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java index 6cdb5e56..d5c2439 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -14,13 +14,13 @@ package com.google.gerrit.server.config; -import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO; -import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME; -import static com.google.gerrit.server.account.ExternalId.SCHEME_UUID; +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.account.externalids.ExternalId.SCHEME_UUID; import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.extensions.client.GitBasicAuthPolicy; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.auth.openid.OpenIdProviderPattern; import com.google.gwtjsonrpc.server.SignedToken; import com.google.gwtjsonrpc.server.XsrfException; @@ -67,7 +67,7 @@ private GitBasicAuthPolicy gitBasicAuthPolicy; @Inject - AuthConfig(@GerritServerConfig final Config cfg) throws XsrfException { + AuthConfig(@GerritServerConfig Config cfg) throws XsrfException { authType = toType(cfg); httpHeader = cfg.getString("auth", null, "httpheader"); httpDisplaynameHeader = cfg.getString("auth", null, "httpdisplaynameheader"); @@ -126,7 +126,7 @@ return Collections.unmodifiableList(r); } - private static AuthType toType(final Config cfg) { + private static AuthType toType(Config cfg) { return cfg.getEnum("auth", null, "type", AuthType.OPENID); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CacheResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CacheResource.java index 7b40786..16c7508 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CacheResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CacheResource.java
@@ -31,7 +31,7 @@ this.cacheProvider = cacheProvider; } - public CacheResource(String pluginName, String cacheName, final Cache<?, ?> cache) { + public CacheResource(String pluginName, String cacheName, Cache<?, ?> cache) { this( pluginName, cacheName,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java index f002f8d..1e88842 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java
@@ -27,8 +27,10 @@ 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.AnonymousUser; import com.google.gerrit.server.CurrentUser; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -40,6 +42,7 @@ private final DynamicMap<RestView<CacheResource>> views; private final Provider<ListCaches> list; + private final PermissionBackend permissionBackend; private final Provider<CurrentUser> self; private final DynamicMap<Cache<?, ?>> cacheMap; private final PostCaches postCaches; @@ -48,11 +51,13 @@ CachesCollection( DynamicMap<RestView<CacheResource>> views, Provider<ListCaches> list, + PermissionBackend permissionBackend, Provider<CurrentUser> self, DynamicMap<Cache<?, ?>> cacheMap, PostCaches postCaches) { this.views = views; this.list = list; + this.permissionBackend = permissionBackend; this.self = self; this.cacheMap = cacheMap; this.postCaches = postCaches; @@ -65,15 +70,8 @@ @Override public CacheResource parse(ConfigResource parent, IdString id) - throws AuthException, ResourceNotFoundException { - CurrentUser user = self.get(); - if (user instanceof AnonymousUser) { - throw new AuthException("Authentication required"); - } else if (!user.isIdentifiedUser()) { - throw new ResourceNotFoundException(); - } else if (!user.getCapabilities().canViewCaches()) { - throw new AuthException("not allowed to view caches"); - } + throws AuthException, ResourceNotFoundException, PermissionBackendException { + permissionBackend.user(self).check(GlobalPermission.VIEW_CACHES); String cacheName = id.get(); String pluginName = "gerrit";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CanonicalWebUrlProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CanonicalWebUrlProvider.java index e670e2c..539951f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CanonicalWebUrlProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CanonicalWebUrlProvider.java
@@ -23,7 +23,7 @@ private final String canonicalUrl; @Inject - public CanonicalWebUrlProvider(@GerritServerConfig final Config config) { + public CanonicalWebUrlProvider(@GerritServerConfig Config config) { String u = config.getString("gerrit", null, "canonicalweburl"); if (u != null && !u.endsWith("/")) { u += "/";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java new file mode 100644 index 0000000..c89684c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckAccess.java
@@ -0,0 +1,130 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.common.base.Strings; +import com.google.gerrit.extensions.api.config.AccessCheckInfo; +import com.google.gerrit.extensions.api.config.AccessCheckInput; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.AccountResolver; +import com.google.gerrit.server.permissions.GlobalPermission; +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.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jgit.errors.ConfigInvalidException; + +@Singleton +public class CheckAccess implements RestModifyView<ConfigResource, AccessCheckInput> { + private final Provider<IdentifiedUser> currentUser; + private final AccountResolver accountResolver; + private final Provider<ReviewDb> db; + private final IdentifiedUser.GenericFactory userFactory; + private final ProjectCache projectCache; + private final PermissionBackend permissionBackend; + + @Inject + CheckAccess( + Provider<IdentifiedUser> currentUser, + AccountResolver resolver, + Provider<ReviewDb> db, + IdentifiedUser.GenericFactory userFactory, + ProjectCache projectCache, + PermissionBackend permissionBackend) { + this.currentUser = currentUser; + this.accountResolver = resolver; + this.db = db; + this.userFactory = userFactory; + this.projectCache = projectCache; + this.permissionBackend = permissionBackend; + } + + @Override + public AccessCheckInfo apply(ConfigResource unused, AccessCheckInput input) + throws OrmException, PermissionBackendException, RestApiException, IOException, + ConfigInvalidException { + permissionBackend.user(currentUser.get()).check(GlobalPermission.ADMINISTRATE_SERVER); + + if (input == null) { + throw new BadRequestException("input is required"); + } + if (Strings.isNullOrEmpty(input.account)) { + throw new BadRequestException("input requires 'account'"); + } + if (Strings.isNullOrEmpty(input.project)) { + throw new BadRequestException("input requires 'project'"); + } + + Account match = accountResolver.find(db.get(), input.account); + if (match == null) { + throw new BadRequestException(String.format("cannot find account %s", input.account)); + } + + AccessCheckInfo info = new AccessCheckInfo(); + + Project.NameKey key = new Project.NameKey(input.project); + if (projectCache.get(key) == null) { + info.message = String.format("project %s does not exist", key); + info.status = HttpServletResponse.SC_NOT_FOUND; + return info; + } + + IdentifiedUser user = userFactory.create(match.getId()); + try { + permissionBackend.user(user).project(key).check(ProjectPermission.ACCESS); + } catch (AuthException | PermissionBackendException e) { + info.message = + String.format( + "user %s (%s) cannot see project %s", + user.getNameEmail(), user.getAccount().getId(), key); + info.status = HttpServletResponse.SC_FORBIDDEN; + return info; + } + + if (!Strings.isNullOrEmpty(input.ref)) { + try { + permissionBackend + .user(user) + .ref(new Branch.NameKey(key, input.ref)) + .check(RefPermission.READ); + } catch (AuthException | PermissionBackendException e) { + info.status = HttpServletResponse.SC_FORBIDDEN; + info.message = + String.format( + "user %s (%s) cannot see ref %s in project %s", + user.getNameEmail(), user.getAccount().getId(), input.ref, key); + return info; + } + } + + info.status = HttpServletResponse.SC_OK; + return info; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckConsistency.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckConsistency.java new file mode 100644 index 0000000..eaf45be --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CheckConsistency.java
@@ -0,0 +1,76 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.api.config.ConsistencyCheckInfo; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.CheckAccountExternalIdsResultInfo; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.CheckAccountsResultInfo; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInput; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.AccountsConsistencyChecker; +import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.io.IOException; + +@Singleton +public class CheckConsistency implements RestModifyView<ConfigResource, ConsistencyCheckInput> { + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; + private final AccountsConsistencyChecker accountsConsistencyChecker; + private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker; + + @Inject + CheckConsistency( + PermissionBackend permissionBackend, + Provider<CurrentUser> user, + AccountsConsistencyChecker accountsConsistencyChecker, + ExternalIdsConsistencyChecker externalIdsConsistencyChecker) { + this.permissionBackend = permissionBackend; + this.user = user; + this.accountsConsistencyChecker = accountsConsistencyChecker; + this.externalIdsConsistencyChecker = externalIdsConsistencyChecker; + } + + @Override + public ConsistencyCheckInfo apply(ConfigResource resource, ConsistencyCheckInput input) + throws RestApiException, IOException, OrmException, PermissionBackendException { + permissionBackend.user(user).check(GlobalPermission.ACCESS_DATABASE); + + if (input == null || (input.checkAccounts == null && input.checkAccountExternalIds == null)) { + throw new BadRequestException("input required"); + } + + ConsistencyCheckInfo consistencyCheckInfo = new ConsistencyCheckInfo(); + if (input.checkAccounts != null) { + consistencyCheckInfo.checkAccountsResult = + new CheckAccountsResultInfo(accountsConsistencyChecker.check()); + } + if (input.checkAccountExternalIds != null) { + consistencyCheckInfo.checkAccountExternalIdsResult = + new CheckAccountExternalIdsResultInfo(externalIdsConsistencyChecker.check()); + } + + return consistencyCheckInfo; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java index d5d2960..f268110 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigCollection.java
@@ -44,7 +44,7 @@ @Override public ConfigResource parse(TopLevelResource root, IdString id) throws ResourceNotFoundException { - if (id.equals("server")) { + if (id.get().equals("server")) { return new ConfigResource(); } throw new ResourceNotFoundException(id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java index 0da1d3b..c6527fdc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -31,7 +31,7 @@ public class ConfigUtil { @SuppressWarnings("unchecked") - private static <T> T[] allValuesOf(final T defaultValue) { + private static <T> T[] allValuesOf(T defaultValue) { try { return (T[]) defaultValue.getClass().getMethod("values").invoke(null); } catch (IllegalArgumentException @@ -63,7 +63,7 @@ final T[] all) { String n = valueString.replace(' ', '_').replace('-', '_'); - for (final T e : all) { + for (T e : all) { if (e.name().equalsIgnoreCase(n)) { return e; } @@ -81,7 +81,7 @@ r.append("."); r.append(setting); r.append("; supported values are: "); - for (final T e : all) { + for (T e : all) { r.append(e.name()); r.append(" "); } @@ -194,7 +194,7 @@ * assume if the value does not contain an indication of the units. * @return the setting, or {@code defaultValue} if not set, expressed in {@code units}. */ - public static long getTimeUnit(final String valueString, long defaultValue, TimeUnit wantUnit) { + public static long getTimeUnit(String valueString, long defaultValue, TimeUnit wantUnit) { Matcher m = Pattern.compile("^(0|[1-9][0-9]*)\\s*(.*)$").matcher(valueString); if (!m.matches()) { return defaultValue; @@ -410,8 +410,8 @@ return Integer.class == t || int.class == t; } - private static boolean match(final String a, final String... cases) { - for (final String b : cases) { + private static boolean match(String a, String... cases) { + for (String b : cases) { if (b != null && b.equalsIgnoreCase(a)) { return true; } @@ -434,7 +434,7 @@ + valueString); } - private static IllegalArgumentException notTimeUnit(final String val) { + private static IllegalArgumentException notTimeUnit(String val) { return new IllegalArgumentException("Invalid time unit value: " + val); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java index 48d4507..e9d5e5e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -40,7 +40,7 @@ private final ImmutableSet<ArchiveFormat> archiveFormats; @Inject - DownloadConfig(@GerritServerConfig final Config cfg) { + DownloadConfig(@GerritServerConfig Config cfg) { String[] allSchemes = cfg.getStringList("download", null, "scheme"); if (allSchemes.length == 0) { downloadSchemes =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/EmailExpanderProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/EmailExpanderProvider.java index 1c42c09..734bf03 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/EmailExpanderProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/EmailExpanderProvider.java
@@ -23,7 +23,7 @@ private final EmailExpander expander; @Inject - EmailExpanderProvider(@GerritServerConfig final Config cfg) { + EmailExpanderProvider(@GerritServerConfig Config cfg) { final String s = cfg.getString("auth", null, "emailformat"); if (EmailExpander.Simple.canHandle(s)) { expander = new EmailExpander.Simple(s);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java index 5e19091..366dae1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/FlushCache.java
@@ -23,6 +23,9 @@ import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.config.FlushCache.Input; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -34,17 +37,20 @@ public static final String WEB_SESSIONS = "web_sessions"; + private final PermissionBackend permissionBackend; private final Provider<CurrentUser> self; @Inject - public FlushCache(Provider<CurrentUser> self) { + public FlushCache(PermissionBackend permissionBackend, Provider<CurrentUser> self) { + this.permissionBackend = permissionBackend; this.self = self; } @Override - public Response<String> apply(CacheResource rsrc, Input input) throws AuthException { - if (WEB_SESSIONS.equals(rsrc.getName()) && !self.get().getCapabilities().canMaintainServer()) { - throw new AuthException(String.format("only site maintainers can flush %s", WEB_SESSIONS)); + public Response<String> apply(CacheResource rsrc, Input input) + throws AuthException, PermissionBackendException { + if (WEB_SESSIONS.equals(rsrc.getName())) { + permissionBackend.user(self).check(GlobalPermission.MAINTAIN_SERVER); } rsrc.getCache().invalidateAll();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java index 4f948d7..8612737 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -85,7 +85,6 @@ import com.google.gerrit.server.account.AccountVisibility; import com.google.gerrit.server.account.AccountVisibilityProvider; import com.google.gerrit.server.account.CapabilityCollection; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.ChangeUserName; import com.google.gerrit.server.account.EmailExpander; import com.google.gerrit.server.account.GroupCacheImpl; @@ -94,6 +93,7 @@ import com.google.gerrit.server.account.GroupIncludeCacheImpl; import com.google.gerrit.server.account.GroupMembers; import com.google.gerrit.server.account.VersionedAuthorizedKeys; +import com.google.gerrit.server.account.externalids.ExternalIdModule; import com.google.gerrit.server.api.accounts.AccountExternalIdCreator; import com.google.gerrit.server.auth.AuthBackend; import com.google.gerrit.server.auth.UniversalAuthBackend; @@ -108,6 +108,7 @@ import com.google.gerrit.server.events.EventFactory; import com.google.gerrit.server.events.EventsMetrics; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.extensions.webui.UiActions; import com.google.gerrit.server.git.AbandonOp; import com.google.gerrit.server.git.ChangeMessageModifier; import com.google.gerrit.server.git.EmailMerge; @@ -118,13 +119,14 @@ import com.google.gerrit.server.git.NotesBranchUtil; import com.google.gerrit.server.git.ReceivePackInitializer; import com.google.gerrit.server.git.ReplaceOp; -import com.google.gerrit.server.git.SubmoduleOp; import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.TransferConfig; +import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.git.strategy.SubmitStrategy; import com.google.gerrit.server.git.validators.CommitValidationListener; import com.google.gerrit.server.git.validators.MergeValidationListener; import com.google.gerrit.server.git.validators.MergeValidators; +import com.google.gerrit.server.git.validators.MergeValidators.AccountValidator; import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator; import com.google.gerrit.server.git.validators.OnSubmitValidationListener; import com.google.gerrit.server.git.validators.OnSubmitValidators; @@ -133,7 +135,7 @@ import com.google.gerrit.server.git.validators.UploadValidationListener; import com.google.gerrit.server.git.validators.UploadValidators; import com.google.gerrit.server.group.GroupModule; -import com.google.gerrit.server.index.change.ReindexAfterUpdate; +import com.google.gerrit.server.index.change.ReindexAfterRefUpdate; import com.google.gerrit.server.mail.EmailModule; import com.google.gerrit.server.mail.ListMailFilter; import com.google.gerrit.server.mail.MailFilter; @@ -158,16 +160,15 @@ import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.plugins.ReloadPluginListener; import com.google.gerrit.server.project.AccessControlModule; -import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.CommentLinkProvider; import com.google.gerrit.server.project.PermissionCollection; import com.google.gerrit.server.project.ProjectCacheImpl; -import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.ProjectNode; import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.project.SectionSortCache; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeQueryBuilder; +import com.google.gerrit.server.query.change.ChangeQueryProcessor; import com.google.gerrit.server.query.change.ConflictsCacheImpl; import com.google.gerrit.server.ssh.SshAddressesModule; import com.google.gerrit.server.tools.ToolsCatalog; @@ -213,7 +214,7 @@ bind(Sequences.class); install(authModule); install(AccountByEmailCacheImpl.module()); - install(AccountCacheImpl.module(true)); + install(AccountCacheImpl.module()); install(BatchUpdate.module()); install(ChangeKindCacheImpl.module()); install(ConflictsCacheImpl.module()); @@ -230,6 +231,7 @@ install(new AccessControlModule()); install(new CmdLineParserModule()); install(new EmailModule()); + install(new ExternalIdModule()); install(new GitModule()); install(new GroupModule()); install(new NoteDbModule(cfg)); @@ -243,7 +245,6 @@ factory(DeleteReviewerSender.Factory.class); factory(AddKeySender.Factory.class); factory(CapabilityCollection.Factory.class); - factory(CapabilityControl.Factory.class); factory(ChangeData.Factory.class); factory(ChangeJson.AssistedFactory.class); factory(CreateChangeSender.Factory.class); @@ -259,6 +260,7 @@ factory(RegisterNewEmailSender.Factory.class); factory(ReplacePatchSetSender.Factory.class); factory(SetAssigneeSender.Factory.class); + factory(VisibleRefFilter.Factory.class); bind(PermissionCollection.Factory.class); bind(AccountVisibility.class).toProvider(AccountVisibilityProvider.class).in(SINGLETON); factory(ProjectOwnerGroupsProvider.Factory.class); @@ -289,11 +291,10 @@ bind(PatchSetInfoFactory.class); bind(IdentifiedUser.GenericFactory.class).in(SINGLETON); - bind(ChangeControl.GenericFactory.class); - bind(ProjectControl.GenericFactory.class); bind(AccountControl.Factory.class); install(new AuditModule()); + bind(UiActions.class); install(new com.google.gerrit.server.access.Module()); install(new com.google.gerrit.server.account.Module()); install(new com.google.gerrit.server.api.Module()); @@ -334,7 +335,7 @@ DynamicSet.setOf(binder(), GarbageCollectorListener.class); DynamicSet.setOf(binder(), HeadUpdatedListener.class); DynamicSet.setOf(binder(), UsageDataPublishedListener.class); - DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ReindexAfterUpdate.class); + DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ReindexAfterRefUpdate.class); DynamicSet.bind(binder(), GitReferenceUpdatedListener.class) .to(ProjectConfigEntry.UpdateChecker.class); DynamicSet.setOf(binder(), EventListener.class); @@ -382,11 +383,14 @@ DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeOperatorFactory.class); DynamicMap.mapOf(binder(), ChangeQueryBuilder.ChangeHasOperandFactory.class); + DynamicMap.mapOf(binder(), ChangeQueryProcessor.ChangeAttributeFactory.class); + install(new GitwebConfig.LegacyModule(cfg)); bind(AnonymousUser.class); factory(AbandonOp.Factory.class); + factory(AccountValidator.Factory.class); factory(RefOperationValidators.Factory.class); factory(OnSubmitValidators.Factory.class); factory(MergeValidators.Factory.class); @@ -396,7 +400,6 @@ factory(MergedByPushOp.Factory.class); factory(GitModules.Factory.class); factory(VersionedAuthorizedKeys.Factory.class); - factory(SubmoduleOp.Factory.class); bind(AccountManager.class); factory(ChangeUserName.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigModule.java index 100a7cd..a93d1f2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerConfigModule.java
@@ -32,7 +32,7 @@ /** Creates {@link GerritServerConfig}. */ public class GerritServerConfigModule extends AbstractModule { - public static String getSecureStoreClassName(final Path sitePath) { + public static String getSecureStoreClassName(Path sitePath) { if (sitePath != null) { return getSecureStoreFromGerritConfig(sitePath); } @@ -41,7 +41,7 @@ return nullToDefault(secureStoreProperty); } - private static String getSecureStoreFromGerritConfig(final Path sitePath) { + private static String getSecureStoreFromGerritConfig(Path sitePath) { AbstractModule m = new AbstractModule() { @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerIdProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerIdProvider.java index 83b60e2f1..dd84d78 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerIdProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritServerIdProvider.java
@@ -46,11 +46,11 @@ return; } - // We're not generally supposed to do work in provider constructors, but - // this is a bit of a special case because we really need to have the ID - // available by the time the dbInjector is created. This even applies during - // RebuildNoteDb, which otherwise would have been a reasonable place to do - // the ID generation. Fortunately, it's not much work, and it happens once. + // We're not generally supposed to do work in provider constructors, but this is a bit of a + // special case because we really need to have the ID available by the time the dbInjector + // is created. This even applies during MigrateToNoteDb, which otherwise would have been a + // reasonable place to do the ID generation. Fortunately, it's not much work, and it happens + // once. id = generate(); Config newCfg = readGerritConfig(sitePaths); newCfg.setString(SECTION, null, KEY, id);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java index c0da3f3..7450b32 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetServerInfo.java
@@ -216,7 +216,7 @@ info.replyLabel = Optional.ofNullable(cfg.getString("change", null, "replyLabel")).orElse("Reply") + "\u2026"; info.updateDelay = - (int) ConfigUtil.getTimeUnit(cfg, "change", null, "updateDelay", 30, TimeUnit.SECONDS); + (int) ConfigUtil.getTimeUnit(cfg, "change", null, "updateDelay", 300, TimeUnit.SECONDS); info.submitWholeTopic = Submit.wholeTopicEnabled(cfg); return info; } @@ -310,9 +310,15 @@ PluginConfigInfo info = new PluginConfigInfo(); info.hasAvatars = toBoolean(avatar.get() != null); info.jsResourcePaths = new ArrayList<>(); + info.htmlResourcePaths = new ArrayList<>(); for (WebUiPlugin u : plugins) { - info.jsResourcePaths.add( - String.format("plugins/%s/%s", u.getPluginName(), u.getJavaScriptResourcePath())); + String path = + String.format("plugins/%s/%s", u.getPluginName(), u.getJavaScriptResourcePath()); + if (path.endsWith(".html")) { + info.htmlResourcePaths.add(path); + } else { + info.jsResourcePaths.add(path); + } } return info; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java index 7e9bd71..bbda9eb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ListTasks.java
@@ -19,13 +19,14 @@ import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.CurrentUser; -import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.TaskInfoFactory; import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.git.WorkQueue.ProjectTask; import com.google.gerrit.server.git.WorkQueue.Task; -import com.google.gerrit.server.project.ProjectCache; -import com.google.gerrit.server.project.ProjectState; +import com.google.gerrit.server.permissions.GlobalPermission; +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.util.IdGenerator; import com.google.inject.Inject; import com.google.inject.Provider; @@ -41,37 +42,49 @@ @Singleton public class ListTasks implements RestReadView<ConfigResource> { + private final PermissionBackend permissionBackend; private final WorkQueue workQueue; - private final ProjectCache projectCache; - private final Provider<IdentifiedUser> self; + private final Provider<CurrentUser> self; @Inject - public ListTasks(WorkQueue workQueue, ProjectCache projectCache, Provider<IdentifiedUser> self) { + public ListTasks( + PermissionBackend permissionBackend, WorkQueue workQueue, Provider<CurrentUser> self) { + this.permissionBackend = permissionBackend; this.workQueue = workQueue; - this.projectCache = projectCache; this.self = self; } @Override - public List<TaskInfo> apply(ConfigResource resource) throws AuthException { + public List<TaskInfo> apply(ConfigResource resource) + throws AuthException, PermissionBackendException { CurrentUser user = self.get(); if (!user.isIdentifiedUser()) { throw new AuthException("Authentication required"); } List<TaskInfo> allTasks = getTasks(); - if (user.getCapabilities().canViewQueue()) { + try { + permissionBackend.user(user).check(GlobalPermission.VIEW_QUEUE); return allTasks; + } catch (AuthException e) { + // Fall through to filter tasks. } - Map<String, Boolean> visibilityCache = new HashMap<>(); + Map<String, Boolean> visibilityCache = new HashMap<>(); List<TaskInfo> visibleTasks = new ArrayList<>(); for (TaskInfo task : allTasks) { if (task.projectName != null) { Boolean visible = visibilityCache.get(task.projectName); if (visible == null) { - ProjectState e = projectCache.get(new Project.NameKey(task.projectName)); - visible = e != null ? e.controlFor(user).isVisible() : false; + try { + permissionBackend + .user(user) + .project(new Project.NameKey(task.projectName)) + .check(ProjectPermission.ACCESS); + visible = true; + } catch (AuthException e) { + visible = false; + } visibilityCache.put(task.projectName, visible); } if (visible) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java index a05058e..4f93a1a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/Module.java
@@ -36,6 +36,8 @@ child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class); get(CONFIG_KIND, "version").to(GetVersion.class); get(CONFIG_KIND, "info").to(GetServerInfo.class); + post(CONFIG_KIND, "check.consistency").to(CheckConsistency.class); + post(CONFIG_KIND, "check.access").to(CheckAccess.class); get(CONFIG_KIND, "preferences").to(GetPreferences.class); put(CONFIG_KIND, "preferences").to(SetPreferences.class); get(CONFIG_KIND, "preferences.diff").to(GetDiffPreferences.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java index 1b12495..674a5c6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PluginConfig.java
@@ -17,6 +17,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Strings; import com.google.common.collect.Iterables; +import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.project.ProjectState; import java.util.Arrays; @@ -152,4 +153,13 @@ public Set<String> getNames() { return cfg.getNames(PLUGIN, pluginName, true); } + + public GroupReference getGroupReference(String name) { + return projectConfig.getGroup(GroupReference.extractGroupName(getString(name))); + } + + public void setGroupReference(String name, GroupReference value) { + GroupReference groupRef = projectConfig.resolve(value); + setString(name, groupRef.toConfigValue()); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java index 3cfa2b9..d08f0a9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/PostCaches.java
@@ -26,6 +26,7 @@ import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.server.config.PostCaches.Input; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.ArrayList; @@ -66,7 +67,8 @@ @Override public Response<String> apply(ConfigResource rsrc, Input input) - throws AuthException, BadRequestException, UnprocessableEntityException { + throws AuthException, BadRequestException, UnprocessableEntityException, + PermissionBackendException { if (input == null || input.operation == null) { throw new BadRequestException("operation must be specified"); } @@ -90,7 +92,7 @@ } } - private void flushAll() throws AuthException { + private void flushAll() throws AuthException, PermissionBackendException { for (DynamicMap.Entry<Cache<?, ?>> e : cacheMap) { CacheResource cacheResource = new CacheResource(e.getPluginName(), e.getExportName(), e.getProvider()); @@ -101,7 +103,8 @@ } } - private void flush(List<String> cacheNames) throws UnprocessableEntityException, AuthException { + private void flush(List<String> cacheNames) + throws UnprocessableEntityException, AuthException, PermissionBackendException { List<CacheResource> cacheResources = new ArrayList<>(cacheNames.size()); for (String n : cacheNames) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java index 33e68d3..a2e0356 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -18,8 +18,8 @@ import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.util.ServerRequestContext; import com.google.gerrit.server.util.ThreadLocalRequestContext; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; /** * Provider of the group(s) which should become owners of a newly created project. The only matching @@ -40,7 +40,7 @@ ProjectOwnerGroupsProvider create(Project.NameKey project); } - @AssistedInject + @Inject public ProjectOwnerGroupsProvider( GroupBackend gb, ThreadLocalRequestContext context,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java index 3987aed..fdb400b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
@@ -32,7 +32,7 @@ @Inject public RequestScopedReviewDbProvider( - final SchemaFactory<ReviewDb> schema, final Provider<RequestCleanup> cleanup) { + final SchemaFactory<ReviewDb> schema, Provider<RequestCleanup> cleanup) { this.schema = schema; this.cleanup = cleanup; } @@ -41,7 +41,7 @@ @Override public ReviewDb get() { if (db == null) { - final ReviewDb c; + ReviewDb c; try { c = schema.open(); } catch (OrmException e) { @@ -51,12 +51,9 @@ cleanup .get() .add( - new Runnable() { - @Override - public void run() { - c.close(); - db = null; - } + () -> { + c.close(); + db = null; }); } catch (Throwable e) { c.close();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java index 4792131..55337d5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SetPreferences.java
@@ -85,7 +85,7 @@ com.google.gerrit.server.account.SetPreferences.storeUrlAliases(p, i.urlAliases); p.commit(md); - accountCache.evictAll(); + accountCache.evictAllNoReindex(); GeneralPreferencesInfo r = loadSection(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TasksCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TasksCollection.java index b239856..fcaee8e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/TasksCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TasksCollection.java
@@ -21,12 +21,13 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.server.CurrentUser; -import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.git.WorkQueue.ProjectTask; import com.google.gerrit.server.git.WorkQueue.Task; -import com.google.gerrit.server.project.ProjectCache; -import com.google.gerrit.server.project.ProjectState; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -36,21 +37,21 @@ private final DynamicMap<RestView<TaskResource>> views; private final ListTasks list; private final WorkQueue workQueue; - private final Provider<IdentifiedUser> self; - private final ProjectCache projectCache; + private final Provider<CurrentUser> self; + private final PermissionBackend permissionBackend; @Inject TasksCollection( DynamicMap<RestView<TaskResource>> views, ListTasks list, WorkQueue workQueue, - Provider<IdentifiedUser> self, - ProjectCache projectCache) { + Provider<CurrentUser> self, + PermissionBackend permissionBackend) { this.views = views; this.list = list; this.workQueue = workQueue; this.self = self; - this.projectCache = projectCache; + this.permissionBackend = permissionBackend; } @Override @@ -60,30 +61,42 @@ @Override public TaskResource parse(ConfigResource parent, IdString id) - throws ResourceNotFoundException, AuthException { + throws ResourceNotFoundException, AuthException, PermissionBackendException { CurrentUser user = self.get(); if (!user.isIdentifiedUser()) { throw new AuthException("Authentication required"); } + int taskId; try { - int taskId = (int) Long.parseLong(id.get(), 16); - Task<?> task = workQueue.getTask(taskId); - if (task != null) { - if (self.get().getCapabilities().canViewQueue()) { - return new TaskResource(task); - } else if (task instanceof ProjectTask) { - ProjectTask<?> projectTask = ((ProjectTask<?>) task); - ProjectState e = projectCache.get(projectTask.getProjectNameKey()); - if (e != null && e.controlFor(user).isVisible()) { - return new TaskResource(task); - } - } - } - throw new ResourceNotFoundException(id); + taskId = (int) Long.parseLong(id.get(), 16); } catch (NumberFormatException e) { throw new ResourceNotFoundException(id); } + + Task<?> task = workQueue.getTask(taskId); + if (task instanceof ProjectTask) { + try { + permissionBackend + .user(user) + .project(((ProjectTask<?>) task).getProjectNameKey()) + .check(ProjectPermission.ACCESS); + return new TaskResource(task); + } catch (AuthException e) { + // Fall through and try view queue permission. + } + } + + if (task != null) { + try { + permissionBackend.user(user).check(GlobalPermission.VIEW_QUEUE); + return new TaskResource(task); + } catch (AuthException e) { + // Fall through and return not found. + } + } + + throw new ResourceNotFoundException(id); } @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooter.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooter.java index ac2f0c6..ddd2877 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooter.java
@@ -25,7 +25,7 @@ private final Pattern match; private final String system; - public TrackingFooter(String f, final String m, final String s) throws PatternSyntaxException { + public TrackingFooter(String f, String m, String s) throws PatternSyntaxException { f = f.trim(); if (f.endsWith(":")) { f = f.substring(0, f.length() - 1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java index a897bdc..85528d9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFooters.java
@@ -23,7 +23,7 @@ public class TrackingFooters { protected List<TrackingFooter> trackingFooters; - public TrackingFooters(final List<TrackingFooter> trFooters) { + public TrackingFooters(List<TrackingFooter> trFooters) { trackingFooters = trFooters; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java index 5389b1f..2b1af36 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java
@@ -40,7 +40,7 @@ private static final Logger log = LoggerFactory.getLogger(TrackingFootersProvider.class); @Inject - TrackingFootersProvider(@GerritServerConfig final Config cfg) { + TrackingFootersProvider(@GerritServerConfig Config cfg) { for (String name : cfg.getSubsections(TRACKING_ID_TAG)) { boolean configValid = true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java index 1a8a788..0467c92 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/data/ChangeAttribute.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.data; +import com.google.gerrit.extensions.common.PluginDefinedInfo; import com.google.gerrit.reviewdb.client.Change; import java.util.List; @@ -43,4 +44,5 @@ public List<DependencyAttribute> neededBy; public List<SubmitRecordAttribute> submitRecords; public List<AccountAttribute> allReviewers; + public List<PluginDefinedInfo> plugins; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEdit.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEdit.java index a6464a7..e641abc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEdit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEdit.java
@@ -18,11 +18,6 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; -import com.google.gerrit.reviewdb.client.RefNames; -import com.google.gerrit.reviewdb.client.RevId; -import com.google.gerrit.server.IdentifiedUser; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.revwalk.RevCommit; /** @@ -33,44 +28,25 @@ * change number and P is the patch set number it is based on. */ public class ChangeEdit { - private final IdentifiedUser user; private final Change change; - private final Ref ref; + private final String editRefName; private final RevCommit editCommit; private final PatchSet basePatchSet; public ChangeEdit( - IdentifiedUser user, Change change, Ref ref, RevCommit editCommit, PatchSet basePatchSet) { - checkNotNull(user); - checkNotNull(change); - checkNotNull(ref); - checkNotNull(editCommit); - checkNotNull(basePatchSet); - this.user = user; - this.change = change; - this.ref = ref; - this.editCommit = editCommit; - this.basePatchSet = basePatchSet; + Change change, String editRefName, RevCommit editCommit, PatchSet basePatchSet) { + this.change = checkNotNull(change); + this.editRefName = checkNotNull(editRefName); + this.editCommit = checkNotNull(editCommit); + this.basePatchSet = checkNotNull(basePatchSet); } public Change getChange() { return change; } - public IdentifiedUser getUser() { - return user; - } - - public Ref getRef() { - return ref; - } - - public RevId getRevision() { - return new RevId(ObjectId.toString(ref.getObjectId())); - } - public String getRefName() { - return RefNames.refsEdit(user.getAccountId(), change.getId(), basePatchSet.getId()); + return editRefName; } public RevCommit getEditCommit() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java index 3d1fb79..79dc9c2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -14,11 +14,10 @@ package com.google.gerrit.server.edit; -import static com.google.common.base.Preconditions.checkState; - -import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MergeConflictException; import com.google.gerrit.extensions.restapi.RawInput; import com.google.gerrit.reviewdb.client.Change; @@ -36,14 +35,19 @@ import com.google.gerrit.server.edit.tree.TreeCreator; import com.google.gerrit.server.edit.tree.TreeModification; import com.google.gerrit.server.index.change.ChangeIndexer; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.InvalidChangeOperationException; +import com.google.gerrit.server.util.CommitMessageUtil; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; import java.sql.Timestamp; +import java.util.List; import java.util.Optional; import java.util.TimeZone; import org.eclipse.jgit.lib.BatchRefUpdate; @@ -76,6 +80,7 @@ private final ChangeIndexer indexer; private final Provider<ReviewDb> reviewDb; private final Provider<CurrentUser> currentUser; + private final PermissionBackend permissionBackend; private final ChangeEditUtil changeEditUtil; private final PatchSetUtil patchSetUtil; @@ -85,11 +90,13 @@ ChangeIndexer indexer, Provider<ReviewDb> reviewDb, Provider<CurrentUser> currentUser, + PermissionBackend permissionBackend, ChangeEditUtil changeEditUtil, PatchSetUtil patchSetUtil) { this.indexer = indexer; this.reviewDb = reviewDb; this.currentUser = currentUser; + this.permissionBackend = permissionBackend; this.tz = gerritIdent.getTimeZone(); this.changeEditUtil = changeEditUtil; this.patchSetUtil = patchSetUtil; @@ -103,10 +110,12 @@ * be created * @throws AuthException if the user isn't authenticated or not allowed to use change edits * @throws InvalidChangeOperationException if a change edit already existed for the change + * @throws PermissionBackendException */ public void createEdit(Repository repository, ChangeControl changeControl) - throws AuthException, IOException, InvalidChangeOperationException, OrmException { - ensureAuthenticatedAndPermitted(changeControl); + throws AuthException, IOException, InvalidChangeOperationException, OrmException, + PermissionBackendException { + assertCanEdit(changeControl); Optional<ChangeEdit> changeEdit = lookupChangeEdit(changeControl); if (changeEdit.isPresent()) { @@ -116,8 +125,7 @@ PatchSet currentPatchSet = lookupCurrentPatchSet(changeControl); ObjectId patchSetCommitId = getPatchSetCommitId(currentPatchSet); - createEditReference( - repository, changeControl, currentPatchSet, patchSetCommitId, TimeUtil.nowTs()); + createEdit(repository, changeControl, currentPatchSet, patchSetCommitId, TimeUtil.nowTs()); } /** @@ -131,11 +139,12 @@ * change, the change edit is already based on the latest patch set, or the change represents * the root commit * @throws MergeConflictException if rebase fails due to merge conflicts + * @throws PermissionBackendException */ public void rebaseEdit(Repository repository, ChangeControl changeControl) throws AuthException, InvalidChangeOperationException, IOException, OrmException, - MergeConflictException { - ensureAuthenticatedAndPermitted(changeControl); + MergeConflictException, PermissionBackendException { + assertCanEdit(changeControl); Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl); if (!optionalChangeEdit.isPresent()) { @@ -194,12 +203,15 @@ * @param newCommitMessage the new commit message * @throws AuthException if the user isn't authenticated or not allowed to use change edits * @throws UnchangedCommitMessageException if the commit message is the same as before + * @throws PermissionBackendException + * @throws BadRequestException if the commit message is malformed */ public void modifyMessage( Repository repository, ChangeControl changeControl, String newCommitMessage) - throws AuthException, IOException, UnchangedCommitMessageException, OrmException { - ensureAuthenticatedAndPermitted(changeControl); - newCommitMessage = getWellFormedCommitMessage(newCommitMessage); + throws AuthException, IOException, UnchangedCommitMessageException, OrmException, + PermissionBackendException, BadRequestException { + assertCanEdit(changeControl); + newCommitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(newCommitMessage); Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl); PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, changeControl); @@ -218,9 +230,9 @@ createCommit(repository, basePatchSetCommit, baseTree, newCommitMessage, nowTimestamp); if (optionalChangeEdit.isPresent()) { - updateEditReference(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp); + updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp); } else { - createEditReference(repository, changeControl, basePatchSet, newEditCommit, nowTimestamp); + createEdit(repository, changeControl, basePatchSet, newEditCommit, nowTimestamp); } } @@ -235,10 +247,12 @@ * @param newContent the new file content * @throws AuthException if the user isn't authenticated or not allowed to use change edits * @throws InvalidChangeOperationException if the file already had the specified content + * @throws PermissionBackendException */ public void modifyFile( Repository repository, ChangeControl changeControl, String filePath, RawInput newContent) - throws AuthException, InvalidChangeOperationException, IOException, OrmException { + throws AuthException, InvalidChangeOperationException, IOException, OrmException, + PermissionBackendException { modifyTree(repository, changeControl, new ChangeFileContentModification(filePath, newContent)); } @@ -252,9 +266,11 @@ * @param file path of the file which should be deleted * @throws AuthException if the user isn't authenticated or not allowed to use change edits * @throws InvalidChangeOperationException if the file does not exist + * @throws PermissionBackendException */ public void deleteFile(Repository repository, ChangeControl changeControl, String file) - throws AuthException, InvalidChangeOperationException, IOException, OrmException { + throws AuthException, InvalidChangeOperationException, IOException, OrmException, + PermissionBackendException { modifyTree(repository, changeControl, new DeleteFileModification(file)); } @@ -270,13 +286,15 @@ * @throws AuthException if the user isn't authenticated or not allowed to use change edits * @throws InvalidChangeOperationException if the file was already renamed to the specified new * name + * @throws PermissionBackendException */ public void renameFile( Repository repository, ChangeControl changeControl, String currentFilePath, String newFilePath) - throws AuthException, InvalidChangeOperationException, IOException, OrmException { + throws AuthException, InvalidChangeOperationException, IOException, OrmException, + PermissionBackendException { modifyTree(repository, changeControl, new RenameFileModification(currentFilePath, newFilePath)); } @@ -291,16 +309,19 @@ * @param file the path of the file which should be restored * @throws AuthException if the user isn't authenticated or not allowed to use change edits * @throws InvalidChangeOperationException if the file was already restored + * @throws PermissionBackendException */ public void restoreFile(Repository repository, ChangeControl changeControl, String file) - throws AuthException, InvalidChangeOperationException, IOException, OrmException { + throws AuthException, InvalidChangeOperationException, IOException, OrmException, + PermissionBackendException { modifyTree(repository, changeControl, new RestoreFileModification(file)); } private void modifyTree( Repository repository, ChangeControl changeControl, TreeModification treeModification) - throws AuthException, IOException, OrmException, InvalidChangeOperationException { - ensureAuthenticatedAndPermitted(changeControl); + throws AuthException, IOException, OrmException, InvalidChangeOperationException, + PermissionBackendException { + assertCanEdit(changeControl); Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl); PatchSet basePatchSet = getBasePatchSet(optionalChangeEdit, changeControl); @@ -308,7 +329,7 @@ RevCommit baseCommit = optionalChangeEdit.map(ChangeEdit::getEditCommit).orElse(basePatchSetCommit); - ObjectId newTreeId = createNewTree(repository, baseCommit, treeModification); + ObjectId newTreeId = createNewTree(repository, baseCommit, ImmutableList.of(treeModification)); String commitMessage = baseCommit.getFullMessage(); Timestamp nowTimestamp = TimeUtil.nowTs(); @@ -316,35 +337,102 @@ createCommit(repository, basePatchSetCommit, newTreeId, commitMessage, nowTimestamp); if (optionalChangeEdit.isPresent()) { - updateEditReference(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp); + updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp); } else { - createEditReference(repository, changeControl, basePatchSet, newEditCommit, nowTimestamp); + createEdit(repository, changeControl, basePatchSet, newEditCommit, nowTimestamp); } } - private void ensureAuthenticatedAndPermitted(ChangeControl changeControl) - throws AuthException, OrmException { - ensureAuthenticated(); - ensurePermitted(changeControl); + /** + * Applies the indicated modifications to the specified patch set. If a change edit exists and is + * based on the same patch set, the modified patch set tree is merged with the change edit. If the + * change edit doesn't exist, a new one will be created. + * + * @param repository the affected Git repository + * @param changeControl the {@code ChangeControl} of the change to which the patch set belongs + * @param patchSet the {@code PatchSet} which should be modified + * @param treeModifications the modifications which should be applied + * @return the resulting {@code ChangeEdit} + * @throws AuthException if the user isn't authenticated or not allowed to use change edits + * @throws InvalidChangeOperationException if the existing change edit is based on another patch + * set or no change edit exists but the specified patch set isn't the current one + * @throws MergeConflictException if the modified patch set tree can't be merged with an existing + * change edit + */ + public ChangeEdit combineWithModifiedPatchSetTree( + Repository repository, + ChangeControl changeControl, + PatchSet patchSet, + List<TreeModification> treeModifications) + throws AuthException, IOException, InvalidChangeOperationException, MergeConflictException, + OrmException, PermissionBackendException { + assertCanEdit(changeControl); + + Optional<ChangeEdit> optionalChangeEdit = lookupChangeEdit(changeControl); + ensureAllowedPatchSet(changeControl, optionalChangeEdit, patchSet); + + RevCommit patchSetCommit = lookupCommit(repository, patchSet); + ObjectId newTreeId = createNewTree(repository, patchSetCommit, treeModifications); + + if (optionalChangeEdit.isPresent()) { + ChangeEdit changeEdit = optionalChangeEdit.get(); + newTreeId = merge(repository, changeEdit, newTreeId); + if (ObjectId.equals(newTreeId, changeEdit.getEditCommit().getTree())) { + // Modifications are already contained in the change edit. + return changeEdit; + } + } + + String commitMessage = + optionalChangeEdit.map(ChangeEdit::getEditCommit).orElse(patchSetCommit).getFullMessage(); + Timestamp nowTimestamp = TimeUtil.nowTs(); + ObjectId newEditCommit = + createCommit(repository, patchSetCommit, newTreeId, commitMessage, nowTimestamp); + + if (optionalChangeEdit.isPresent()) { + return updateEdit(repository, optionalChangeEdit.get(), newEditCommit, nowTimestamp); + } + return createEdit(repository, changeControl, patchSet, newEditCommit, nowTimestamp); } - private void ensureAuthenticated() throws AuthException { + private void assertCanEdit(ChangeControl changeControl) + throws AuthException, PermissionBackendException { if (!currentUser.get().isIdentifiedUser()) { throw new AuthException("Authentication required"); } - } - - private void ensurePermitted(ChangeControl changeControl) throws OrmException, AuthException { - if (!changeControl.canAddPatchSet(reviewDb.get())) { - throw new AuthException("Not allowed to edit a change."); + try { + permissionBackend + .user(currentUser) + .database(reviewDb) + .change(changeControl.getNotes()) + .check(ChangePermission.ADD_PATCH_SET); + } catch (AuthException denied) { + throw new AuthException("edit not permitted", denied); } } - private String getWellFormedCommitMessage(String commitMessage) { - String wellFormedMessage = Strings.nullToEmpty(commitMessage).trim(); - checkState(!wellFormedMessage.isEmpty(), "Commit message cannot be null or empty"); - wellFormedMessage = wellFormedMessage + "\n"; - return wellFormedMessage; + private static void ensureAllowedPatchSet( + ChangeControl changeControl, Optional<ChangeEdit> optionalChangeEdit, PatchSet patchSet) + throws InvalidChangeOperationException { + if (optionalChangeEdit.isPresent()) { + ChangeEdit changeEdit = optionalChangeEdit.get(); + if (!isBasedOn(changeEdit, patchSet)) { + throw new InvalidChangeOperationException( + String.format( + "Only the patch set %s on which the existing change edit is based may be modified " + + "(specified patch set: %s)", + changeEdit.getBasePatchSet().getId(), patchSet.getId())); + } + } else { + PatchSet.Id patchSetId = patchSet.getId(); + PatchSet.Id currentPatchSetId = changeControl.getChange().currentPatchSetId(); + if (!patchSetId.equals(currentPatchSetId)) { + throw new InvalidChangeOperationException( + String.format( + "A change edit may only be created for the current patch set %s (and not for %s)", + currentPatchSetId, patchSetId)); + } + } } private Optional<ChangeEdit> lookupChangeEdit(ChangeControl changeControl) @@ -372,16 +460,21 @@ private static RevCommit lookupCommit(Repository repository, PatchSet patchSet) throws IOException { ObjectId patchSetCommitId = getPatchSetCommitId(patchSet); + return lookupCommit(repository, patchSetCommitId); + } + + private static RevCommit lookupCommit(Repository repository, ObjectId commitId) + throws IOException { try (RevWalk revWalk = new RevWalk(repository)) { - return revWalk.parseCommit(patchSetCommitId); + return revWalk.parseCommit(commitId); } } private static ObjectId createNewTree( - Repository repository, RevCommit baseCommit, TreeModification treeModification) + Repository repository, RevCommit baseCommit, List<TreeModification> treeModifications) throws IOException, InvalidChangeOperationException { TreeCreator treeCreator = new TreeCreator(baseCommit); - treeCreator.addTreeModification(treeModification); + treeCreator.addTreeModifications(treeModifications); ObjectId newTreeId = treeCreator.createNewTreeAndGetId(repository); if (ObjectId.equals(newTreeId, baseCommit.getTree())) { @@ -390,7 +483,7 @@ return newTreeId; } - private ObjectId merge(Repository repository, ChangeEdit changeEdit, ObjectId newTreeId) + private static ObjectId merge(Repository repository, ChangeEdit changeEdit, ObjectId newTreeId) throws IOException, MergeConflictException { PatchSet basePatchSet = changeEdit.getBasePatchSet(); ObjectId basePatchSetCommitId = getPatchSetCommitId(basePatchSet); @@ -436,17 +529,20 @@ return ObjectId.fromString(patchSet.getRevision().get()); } - private void createEditReference( + private ChangeEdit createEdit( Repository repository, ChangeControl changeControl, PatchSet basePatchSet, - ObjectId newEditCommit, + ObjectId newEditCommitId, Timestamp timestamp) throws IOException, OrmException { Change change = changeControl.getChange(); String editRefName = getEditRefName(change, basePatchSet); - updateReference(repository, editRefName, ObjectId.zeroId(), newEditCommit, timestamp); + updateReference(repository, editRefName, ObjectId.zeroId(), newEditCommitId, timestamp); reindex(change); + + RevCommit newEditCommit = lookupCommit(repository, newEditCommitId); + return new ChangeEdit(change, editRefName, newEditCommit, basePatchSet); } private String getEditRefName(Change change, PatchSet basePatchSet) { @@ -454,13 +550,17 @@ return RefNames.refsEdit(me.getAccountId(), change.getId(), basePatchSet.getId()); } - private void updateEditReference( - Repository repository, ChangeEdit changeEdit, ObjectId newEditCommit, Timestamp timestamp) + private ChangeEdit updateEdit( + Repository repository, ChangeEdit changeEdit, ObjectId newEditCommitId, Timestamp timestamp) throws IOException, OrmException { String editRefName = changeEdit.getRefName(); RevCommit currentEditCommit = changeEdit.getEditCommit(); - updateReference(repository, editRefName, currentEditCommit, newEditCommit, timestamp); + updateReference(repository, editRefName, currentEditCommit, newEditCommitId, timestamp); reindex(changeEdit.getChange()); + + RevCommit newEditCommit = lookupCommit(repository, newEditCommitId); + return new ChangeEdit( + changeEdit.getChange(), editRefName, newEditCommit, changeEdit.getBasePatchSet()); } private void updateReference(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java index 6509ecc..e51312b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -53,6 +53,7 @@ import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.Repository; @@ -74,7 +75,6 @@ private final Provider<ReviewDb> db; private final Provider<CurrentUser> user; private final ChangeKindCache changeKindCache; - private final BatchUpdate.Factory updateFactory; private final PatchSetUtil psUtil; @Inject @@ -86,7 +86,6 @@ Provider<ReviewDb> db, Provider<CurrentUser> user, ChangeKindCache changeKindCache, - BatchUpdate.Factory updateFactory, PatchSetUtil psUtil) { this.gitManager = gitManager; this.patchSetInserterFactory = patchSetInserterFactory; @@ -95,7 +94,6 @@ this.db = db; this.user = user; this.changeKindCache = changeKindCache; - this.updateFactory = updateFactory; this.psUtil = psUtil; } @@ -149,7 +147,7 @@ try (RevWalk rw = new RevWalk(repo)) { RevCommit commit = rw.parseCommit(ref.getObjectId()); PatchSet basePs = getBasePatchSet(ctl, ref); - return Optional.of(new ChangeEdit(u, change, ref, commit, basePs)); + return Optional.of(new ChangeEdit(change, ref.getName(), commit, basePs)); } } } @@ -157,6 +155,8 @@ /** * Promote change edit to patch set, by squashing the edit into its parent. * + * @param updateFactory factory for creating updates. + * @param ctl the {@code ChangeControl} of the change to which the change edit belongs * @param edit change edit to publish * @param notify Notify handling that defines to whom email notifications should be sent after the * change edit is published. @@ -167,21 +167,23 @@ * @throws RestApiException */ public void publish( + BatchUpdate.Factory updateFactory, + ChangeControl ctl, final ChangeEdit edit, NotifyHandling notify, ListMultimap<RecipientType, Account.Id> accountsToNotify) throws IOException, OrmException, RestApiException, UpdateException { Change change = edit.getChange(); try (Repository repo = gitManager.openRepository(change.getProject()); - RevWalk rw = new RevWalk(repo); - ObjectInserter oi = repo.newObjectInserter()) { + ObjectInserter oi = repo.newObjectInserter(); + ObjectReader reader = oi.newReader(); + RevWalk rw = new RevWalk(reader)) { PatchSet basePatchSet = edit.getBasePatchSet(); if (!basePatchSet.getId().equals(change.currentPatchSetId())) { throw new ResourceConflictException("only edit for current patch set can be published"); } RevCommit squashed = squashEdit(rw, oi, edit.getEditCommit(), basePatchSet); - ChangeControl ctl = changeControlFactory.controlFor(db.get(), change, edit.getUser()); PatchSet.Id psId = ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId()); PatchSetInserter inserter = patchSetInserterFactory @@ -194,7 +196,8 @@ // Previously checked that the base patch set is the current patch set. ObjectId prior = ObjectId.fromString(basePatchSet.getRevision().get()); - ChangeKind kind = changeKindCache.getChangeKind(change.getProject(), repo, prior, squashed); + ChangeKind kind = + changeKindCache.getChangeKind(change.getProject(), rw, repo.getConfig(), prior, squashed); if (kind == ChangeKind.NO_CODE_CHANGE) { message.append("Commit message was updated."); inserter.setDescription("Edit commit message"); @@ -218,24 +221,11 @@ new BatchUpdateOp() { @Override public void updateRepo(RepoContext ctx) throws Exception { - deleteRef(ctx.getRepository(), edit); + ctx.addRefUpdate(edit.getEditCommit().copy(), ObjectId.zeroId(), edit.getRefName()); } }); bu.execute(); - } catch (UpdateException e) { - if (e.getCause() instanceof IOException - && e.getMessage() - .equals( - String.format( - "%s: Failed to delete ref %s: %s", - IOException.class.getName(), - edit.getRefName(), - RefUpdate.Result.LOCK_FAILURE.name()))) { - throw new ResourceConflictException("edit is already published"); - } } - - indexer.index(db.get(), inserter.getChange()); } } @@ -280,7 +270,7 @@ private static void deleteRef(Repository repo, ChangeEdit edit) throws IOException { String refName = edit.getRefName(); RefUpdate ru = repo.updateRef(refName, true); - ru.setExpectedOldObjectId(edit.getRef().getObjectId()); + ru.setExpectedOldObjectId(edit.getEditCommit()); ru.setForceUpdate(true); RefUpdate.Result result = ru.delete(); switch (result) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java index dc35309..3d75e6a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/ChangeFileContentModification.java
@@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import com.google.common.annotations.VisibleForTesting; import com.google.common.io.ByteStreams; import com.google.gerrit.extensions.restapi.RawInput; import java.io.IOException; @@ -53,6 +54,16 @@ return Collections.singletonList(changeContentEdit); } + @Override + public String getFilePath() { + return filePath; + } + + @VisibleForTesting + RawInput getNewContent() { + return newContent; + } + /** A {@code PathEdit} which changes the contents of a file. */ private static class ChangeContent extends DirCacheEditor.PathEdit {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java index 62da19a..feffb70 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/DeleteFileModification.java
@@ -34,4 +34,9 @@ DirCacheEditor.DeletePath deletePathEdit = new DirCacheEditor.DeletePath(filePath); return Collections.singletonList(deletePathEdit); } + + @Override + public String getFilePath() { + return filePath; + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java index aeacd23..b847599 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RenameFileModification.java
@@ -52,4 +52,9 @@ } } } + + @Override + public String getFilePath() { + return newFilePath; + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java index 1bd55f6..393a866 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/RestoreFileModification.java
@@ -58,4 +58,9 @@ } } } + + @Override + public String getFilePath() { + return filePath; + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java index 7e9a96a..e867e76 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeCreator.java
@@ -36,8 +36,6 @@ public class TreeCreator { private final RevCommit baseCommit; - // At the moment, a list wouldn't be necessary as only one modification is - // applied per created tree. This is going to change in the near future. private final List<TreeModification> treeModifications = new ArrayList<>(); public TreeCreator(RevCommit baseCommit) { @@ -45,14 +43,14 @@ } /** - * Apply a modification to the tree which is taken as a basis. If this method is called multiple + * Apply modifications to the tree which is taken as a basis. If this method is called multiple * times, the modifications are applied subsequently in exactly the order they were provided. * - * @param treeModification a modification which should be applied to the base tree + * @param treeModifications modifications which should be applied to the base tree */ - public void addTreeModification(TreeModification treeModification) { - checkNotNull(treeModification, "treeModification must not be null"); - treeModifications.add(treeModification); + public void addTreeModifications(List<TreeModification> treeModifications) { + checkNotNull(treeModifications, "treeModifications must not be null"); + this.treeModifications.addAll(treeModifications); } /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java index 217a309..2656707 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/tree/TreeModification.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.edit.tree; +import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.util.List; import org.eclipse.jgit.dircache.DirCacheEditor; @@ -35,4 +36,14 @@ */ List<DirCacheEditor.PathEdit> getPathEdits(Repository repository, RevCommit baseCommit) throws IOException; + + /** + * Indicates a file path which is affected by this {@code TreeModification}. If the modification + * refers to several file paths (e.g. renaming a file), returning either of them is appropriate as + * long as the returned value is deterministic. + * + * @return an affected file path + */ + @VisibleForTesting + String getFilePath(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java index 17fc52b..c0f9c29 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java
@@ -16,14 +16,19 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.IdentifiedUser; +import java.io.IOException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.ReceiveCommand; -public class CommitReceivedEvent extends RefEvent { +public class CommitReceivedEvent extends RefEvent implements AutoCloseable { static final String TYPE = "commit-received"; public ReceiveCommand command; public Project project; public String refName; + public RevWalk revWalk; public RevCommit commit; public IdentifiedUser user; @@ -35,14 +40,18 @@ ReceiveCommand command, Project project, String refName, - RevCommit commit, - IdentifiedUser user) { + ObjectReader reader, + ObjectId commitId, + IdentifiedUser user) + throws IOException { this(); this.command = command; this.project = project; this.refName = refName; - this.commit = commit; + this.revWalk = new RevWalk(reader); + this.commit = revWalk.parseCommit(commitId); this.user = user; + revWalk.parseBody(commit); } @Override @@ -54,4 +63,9 @@ public String getRefName() { return refName; } + + @Override + public void close() { + revWalk.close(); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java index 80dcb78..ce04f26 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -311,7 +311,7 @@ // set whose parent matches this patch set's revision. for (ChangeData cd : queryProvider.get().byProjectGroups(change.getProject(), currentPs.getGroups())) { - patchSets: + PATCH_SETS: for (PatchSet ps : cd.patchSets()) { RevCommit commit = rw.parseCommit(ObjectId.fromString(ps.getRevision().get())); for (RevCommit p : commit.getParents()) { @@ -319,7 +319,7 @@ continue; } ca.neededBy.add(newNeededBy(checkNotNull(cd.change()), ps)); - continue patchSets; + continue PATCH_SETS; } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java index 15c1ae9..f1d7d96 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/StreamEventsApiListener.java
@@ -149,7 +149,7 @@ return psUtil.get(db.get(), notes, PatchSet.Id.fromRef(info.ref)); } - private Supplier<ChangeAttribute> changeAttributeSupplier(final Change change) { + private Supplier<ChangeAttribute> changeAttributeSupplier(Change change) { return Suppliers.memoize( new Supplier<ChangeAttribute>() { @Override @@ -159,7 +159,7 @@ }); } - private Supplier<AccountAttribute> accountAttributeSupplier(final AccountInfo account) { + private Supplier<AccountAttribute> accountAttributeSupplier(AccountInfo account) { return Suppliers.memoize( new Supplier<AccountAttribute>() { @Override @@ -172,7 +172,7 @@ } private Supplier<PatchSetAttribute> patchSetAttributeSupplier( - final Change change, final PatchSet patchSet) { + final Change change, PatchSet patchSet) { return Suppliers.memoize( new Supplier<PatchSetAttribute>() { @Override @@ -298,7 +298,7 @@ } @Override - public void onReviewerDeleted(final ReviewerDeletedListener.Event ev) { + public void onReviewerDeleted(ReviewerDeletedListener.Event ev) { try { ChangeNotes notes = getNotes(ev.getChange()); Change change = notes.getChange(); @@ -362,7 +362,7 @@ } @Override - public void onGitReferenceUpdated(final GitReferenceUpdatedListener.Event ev) { + public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event ev) { RefUpdatedEvent event = new RefUpdatedEvent(); if (ev.getUpdater() != null) { event.submitter = accountAttributeSupplier(ev.getUpdater());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java index 85ee4f9..ae15cfd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/webui/UiActions.java
@@ -16,20 +16,27 @@ import com.google.common.base.Predicate; import com.google.common.collect.FluentIterable; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission; import com.google.gerrit.extensions.registration.DynamicMap; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.RestCollection; import com.google.gerrit.extensions.restapi.RestResource; import com.google.gerrit.extensions.restapi.RestView; 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.account.CapabilityUtils; +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 com.google.inject.Provider; +import com.google.inject.Singleton; import java.util.Objects; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@Singleton public class UiActions { private static final Logger log = LoggerFactory.getLogger(UiActions.class); @@ -37,57 +44,70 @@ return UiAction.Description::isEnabled; } - public static <R extends RestResource> FluentIterable<UiAction.Description> from( - RestCollection<?, R> collection, R resource, Provider<CurrentUser> userProvider) { - return from(collection.views(), resource, userProvider); + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> userProvider; + + @Inject + UiActions(PermissionBackend permissionBackend, Provider<CurrentUser> userProvider) { + this.permissionBackend = permissionBackend; + this.userProvider = userProvider; } - public static <R extends RestResource> FluentIterable<UiAction.Description> from( - DynamicMap<RestView<R>> views, R resource, Provider<CurrentUser> userProvider) { + public <R extends RestResource> FluentIterable<UiAction.Description> from( + RestCollection<?, R> collection, R resource) { + return from(collection.views(), resource); + } + + public <R extends RestResource> FluentIterable<UiAction.Description> from( + DynamicMap<RestView<R>> views, R resource) { return FluentIterable.from(views) - .transform( - (DynamicMap.Entry<RestView<R>> e) -> { - int d = e.getExportName().indexOf('.'); - if (d < 0) { - return null; - } - - RestView<R> view; - try { - view = e.getProvider().get(); - } catch (RuntimeException err) { - log.error( - String.format( - "error creating view %s.%s", e.getPluginName(), e.getExportName()), - err); - return null; - } - - if (!(view instanceof UiAction)) { - return null; - } - - try { - CapabilityUtils.checkRequiresCapability( - userProvider, e.getPluginName(), view.getClass()); - } catch (AuthException exc) { - return null; - } - - UiAction.Description dsc = ((UiAction<R>) view).getDescription(resource); - if (dsc == null || !dsc.isVisible()) { - return null; - } - - String name = e.getExportName().substring(d + 1); - PrivateInternals_UiActionDescription.setMethod( - dsc, e.getExportName().substring(0, d)); - PrivateInternals_UiActionDescription.setId( - dsc, "gerrit".equals(e.getPluginName()) ? name : e.getPluginName() + '~' + name); - return dsc; - }) + .transform((e) -> describe(e, resource)) .filter(Objects::nonNull); } - private UiActions() {} + @Nullable + private <R extends RestResource> UiAction.Description describe( + DynamicMap.Entry<RestView<R>> e, R resource) { + int d = e.getExportName().indexOf('.'); + if (d < 0) { + return null; + } + + RestView<R> view; + try { + view = e.getProvider().get(); + } catch (RuntimeException err) { + log.error( + String.format("error creating view %s.%s", e.getPluginName(), e.getExportName()), err); + return null; + } + + if (!(view instanceof UiAction)) { + return null; + } + + try { + Set<GlobalOrPluginPermission> need = + GlobalPermission.fromAnnotation(e.getPluginName(), view.getClass()); + if (!need.isEmpty() && permissionBackend.user(userProvider).test(need).isEmpty()) { + // A permission is required, but test returned no candidates. + return null; + } + } catch (PermissionBackendException err) { + log.error( + String.format("exception testing view %s.%s", e.getPluginName(), e.getExportName()), err); + return null; + } + + UiAction.Description dsc = ((UiAction<R>) view).getDescription(resource); + if (dsc == null || !dsc.isVisible()) { + return null; + } + + String name = e.getExportName().substring(d + 1); + PrivateInternals_UiActionDescription.setMethod(dsc, e.getExportName().substring(0, d)); + PrivateInternals_UiActionDescription.setId( + dsc, "gerrit".equals(e.getPluginName()) ? name : e.getPluginName() + '~' + name); + return dsc; + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java b/gerrit-server/src/main/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java new file mode 100644 index 0000000..3fc786b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/fixes/FixReplacementInterpreter.java
@@ -0,0 +1,154 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.fixes; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.gerrit.common.RawInputUtil; +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.reviewdb.client.Comment; +import com.google.gerrit.reviewdb.client.FixReplacement; +import com.google.gerrit.server.change.FileContentUtil; +import com.google.gerrit.server.edit.tree.ChangeFileContentModification; +import com.google.gerrit.server.edit.tree.TreeModification; +import com.google.gerrit.server.project.ProjectState; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; + +/** An interpreter for {@code FixReplacement}s. */ +@Singleton +public class FixReplacementInterpreter { + + private static final Comparator<FixReplacement> ASC_RANGE_FIX_REPLACEMENT_COMPARATOR = + Comparator.comparing(fixReplacement -> fixReplacement.range); + + private final FileContentUtil fileContentUtil; + + @Inject + public FixReplacementInterpreter(FileContentUtil fileContentUtil) { + this.fileContentUtil = fileContentUtil; + } + + /** + * Transforms the given {@code FixReplacement}s into {@code TreeModification}s. + * + * @param repository the affected Git repository + * @param projectState the affected project + * @param patchSetCommitId the patch set which should be modified + * @param fixReplacements the replacements which should be applied + * @return a list of {@code TreeModification}s representing the given replacements + * @throws ResourceNotFoundException if a file to which one of the replacements refers doesn't + * exist + * @throws ResourceConflictException if the replacements can't be transformed into {@code + * TreeModification}s + */ + public List<TreeModification> toTreeModifications( + Repository repository, + ProjectState projectState, + ObjectId patchSetCommitId, + List<FixReplacement> fixReplacements) + throws ResourceNotFoundException, IOException, ResourceConflictException { + checkNotNull(fixReplacements, "Fix replacements must not be null"); + + Map<String, List<FixReplacement>> fixReplacementsPerFilePath = + fixReplacements + .stream() + .collect(Collectors.groupingBy(fixReplacement -> fixReplacement.path)); + + List<TreeModification> treeModifications = new ArrayList<>(); + for (Map.Entry<String, List<FixReplacement>> entry : fixReplacementsPerFilePath.entrySet()) { + TreeModification treeModification = + toTreeModification( + repository, projectState, patchSetCommitId, entry.getKey(), entry.getValue()); + treeModifications.add(treeModification); + } + return treeModifications; + } + + private TreeModification toTreeModification( + Repository repository, + ProjectState projectState, + ObjectId patchSetCommitId, + String filePath, + List<FixReplacement> fixReplacements) + throws ResourceNotFoundException, IOException, ResourceConflictException { + String fileContent = getFileContent(repository, projectState, patchSetCommitId, filePath); + String newFileContent = getNewFileContent(fileContent, fixReplacements); + return new ChangeFileContentModification(filePath, RawInputUtil.create(newFileContent)); + } + + private String getFileContent( + Repository repository, ProjectState projectState, ObjectId patchSetCommitId, String filePath) + throws ResourceNotFoundException, IOException { + try (BinaryResult fileContent = + fileContentUtil.getContent(repository, projectState, patchSetCommitId, filePath)) { + return fileContent.asString(); + } + } + + private static String getNewFileContent(String fileContent, List<FixReplacement> fixReplacements) + throws ResourceConflictException { + List<FixReplacement> sortedReplacements = new ArrayList<>(fixReplacements); + sortedReplacements.sort(ASC_RANGE_FIX_REPLACEMENT_COMPARATOR); + + LineIdentifier lineIdentifier = new LineIdentifier(fileContent); + StringModifier fileContentModifier = new StringModifier(fileContent); + for (FixReplacement fixReplacement : sortedReplacements) { + Comment.Range range = fixReplacement.range; + try { + int startLineIndex = lineIdentifier.getStartIndexOfLine(range.startLine); + int startLineLength = lineIdentifier.getLengthOfLine(range.startLine); + + int endLineIndex = lineIdentifier.getStartIndexOfLine(range.endLine); + int endLineLength = lineIdentifier.getLengthOfLine(range.endLine); + + if (range.startChar > startLineLength || range.endChar > endLineLength) { + throw new ResourceConflictException( + String.format( + "Range %s refers to a non-existent offset (start line length: %s," + + " end line length: %s)", + toString(range), startLineLength, endLineLength)); + } + + int startIndex = startLineIndex + range.startChar; + int endIndex = endLineIndex + range.endChar; + fileContentModifier.replace(startIndex, endIndex, fixReplacement.replacement); + } catch (StringIndexOutOfBoundsException e) { + // Most of the StringIndexOutOfBoundsException should never occur because we reject fix + // replacements for invalid ranges. However, we can't cover all cases for efficiency + // reasons. For instance, we don't determine the number of lines in a file. That's why we + // need to map this exception and thus provide a meaningful error. + throw new ResourceConflictException( + String.format("Cannot apply fix replacement for range %s", toString(range)), e); + } + } + return fileContentModifier.getResult(); + } + + private static String toString(Comment.Range range) { + return String.format( + "(%s:%s - %s:%s)", range.startLine, range.startChar, range.endLine, range.endChar); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/fixes/LineIdentifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/fixes/LineIdentifier.java new file mode 100644 index 0000000..c32d822 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/fixes/LineIdentifier.java
@@ -0,0 +1,110 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.fixes; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * An identifier of lines in a string. Lines are sequences of characters which are separated by any + * Unicode linebreak sequence as defined by the regular expression {@code \R}. If data for several + * lines is requested, calls which are ordered according to ascending line numbers are the most + * efficient. + */ +class LineIdentifier { + + private static final Pattern LINE_SEPARATOR_PATTERN = Pattern.compile("\\R"); + private final Matcher lineSeparatorMatcher; + + private int nextLineNumber; + private int nextLineStartIndex; + private int currentLineStartIndex; + private int currentLineEndIndex; + + LineIdentifier(String string) { + checkNotNull(string); + lineSeparatorMatcher = LINE_SEPARATOR_PATTERN.matcher(string); + reset(); + } + + /** + * Returns the start index of the indicated line within the given string. Start indices are + * zero-based while line numbers are one-based. + * + * <p><b>Note:</b> Requesting data for several lines is more efficient if those calls occur with + * increasing line number. + * + * @param lineNumber the line whose start index should be determined + * @return the start index of the line + * @throws StringIndexOutOfBoundsException if the line number is negative, zero or greater than + * the identified number of lines + */ + public int getStartIndexOfLine(int lineNumber) { + findLine(lineNumber); + return currentLineStartIndex; + } + + /** + * Returns the length of the indicated line in the given string. The character(s) used to separate + * lines aren't included in the count. Line numbers are one-based. + * + * <p><b>Note:</b> Requesting data for several lines is more efficient if those calls occur with + * increasing line number. + * + * @param lineNumber the line whose length should be determined + * @return the length of the line + * @throws StringIndexOutOfBoundsException if the line number is negative, zero or greater than + * the identified number of lines + */ + public int getLengthOfLine(int lineNumber) { + findLine(lineNumber); + return currentLineEndIndex - currentLineStartIndex; + } + + private void findLine(int targetLineNumber) { + if (targetLineNumber <= 0) { + throw new StringIndexOutOfBoundsException("Line number must be positive"); + } + if (targetLineNumber < nextLineNumber) { + reset(); + } + while (nextLineNumber < targetLineNumber + 1 && lineSeparatorMatcher.find()) { + currentLineStartIndex = nextLineStartIndex; + currentLineEndIndex = lineSeparatorMatcher.start(); + nextLineStartIndex = lineSeparatorMatcher.end(); + nextLineNumber++; + } + + // End of string + if (nextLineNumber == targetLineNumber) { + currentLineStartIndex = nextLineStartIndex; + currentLineEndIndex = lineSeparatorMatcher.regionEnd(); + } + if (nextLineNumber < targetLineNumber) { + throw new StringIndexOutOfBoundsException( + String.format("Line %d isn't available", targetLineNumber)); + } + } + + private void reset() { + nextLineNumber = 1; + nextLineStartIndex = 0; + currentLineStartIndex = 0; + currentLineEndIndex = 0; + lineSeparatorMatcher.reset(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/fixes/StringModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/fixes/StringModifier.java new file mode 100644 index 0000000..ccd40b3 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/fixes/StringModifier.java
@@ -0,0 +1,77 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.fixes; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A modifier of a string. It allows to replace multiple parts of a string by indicating those parts + * with indices based on the unmodified string. There is one limitation though: Replacements which + * affect lower indices of the string must be specified before replacements for higher indices. + */ +class StringModifier { + + private final StringBuilder stringBuilder; + + private int characterShift = 0; + private int previousEndOffset = Integer.MIN_VALUE; + + StringModifier(String string) { + checkNotNull(string, "string must not be null"); + stringBuilder = new StringBuilder(string); + } + + /** + * Replaces part of the string with another content. When called multiple times, the calls must be + * ordered according to increasing start indices. Overlapping replacement regions aren't + * supported. + * + * @param startIndex the beginning index in the unmodified string (inclusive) + * @param endIndex the ending index in the unmodified string (exclusive) + * @param replacement the string which should be used instead of the original content + * @throws StringIndexOutOfBoundsException if the start index is smaller than the end index of a + * previous call of this method + */ + public void replace(int startIndex, int endIndex, String replacement) { + checkNotNull(replacement, "replacement string must not be null"); + if (previousEndOffset > startIndex) { + throw new StringIndexOutOfBoundsException( + String.format( + "Not supported to replace the content starting at index %s after previous " + + "replacement which ended at index %s", + startIndex, previousEndOffset)); + } + int shiftedStartIndex = startIndex + characterShift; + int shiftedEndIndex = endIndex + characterShift; + if (shiftedEndIndex > stringBuilder.length()) { + throw new StringIndexOutOfBoundsException( + String.format("end %s > length %s", shiftedEndIndex, stringBuilder.length())); + } + stringBuilder.replace(shiftedStartIndex, shiftedEndIndex, replacement); + + int replacedContentLength = endIndex - startIndex; + characterShift += replacement.length() - replacedContentLength; + previousEndOffset = endIndex; + } + + /** + * Returns the modified string including all specified replacements. + * + * @return the modified string + */ + public String getResult() { + return stringBuilder.toString(); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/AbandonOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AbandonOp.java index 99b647a..f4185c9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/AbandonOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/AbandonOp.java
@@ -25,6 +25,7 @@ import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.extensions.events.ChangeAbandoned; import com.google.gerrit.server.mail.send.AbandonedSender; @@ -34,8 +35,8 @@ import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,7 +65,7 @@ @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify); } - @AssistedInject + @Inject AbandonOp( AbandonedSender.Factory abandonedSenderFactory, ChangeMessagesUtil cmUtil, @@ -96,7 +97,7 @@ PatchSet.Id psId = change.currentPatchSetId(); ChangeUpdate update = ctx.getUpdate(psId); if (!change.getStatus().isOpen()) { - throw new ResourceConflictException("change is " + status(change)); + throw new ResourceConflictException("change is " + ChangeUtil.status(change)); } else if (change.getStatus() == Change.Status.DRAFT) { throw new ResourceConflictException("draft changes cannot be abandoned"); } @@ -137,8 +138,4 @@ } changeAbandoned.fire(change, patchSet, account, msgTxt, ctx.getWhen(), notifyHandling); } - - private static String status(Change change) { - return change != null ? change.getStatus().name().toLowerCase() : "deleted"; - } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java index a3ea7f8..a48b3ea 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
@@ -17,7 +17,6 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.GerritServerConfig; -import com.google.gerrit.server.git.WorkQueue.Executor; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.util.RequestScopePropagator; import com.google.inject.Inject; @@ -30,6 +29,7 @@ import java.io.OutputStream; import java.util.Collection; import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Repository; @@ -63,7 +63,7 @@ @Provides @Singleton @Named(TIMEOUT_NAME) - long getTimeoutMillis(@GerritServerConfig final Config cfg) { + long getTimeoutMillis(@GerritServerConfig Config cfg) { return ConfigUtil.getTimeUnit( cfg, "receive", null, "timeout", TimeUnit.MINUTES.toMillis(4), TimeUnit.MILLISECONDS); } @@ -72,7 +72,7 @@ private class Worker implements ProjectRunnable { private final Collection<ReceiveCommand> commands; - private Worker(final Collection<ReceiveCommand> commands) { + private Worker(Collection<ReceiveCommand> commands) { this.commands = commands; } @@ -125,19 +125,19 @@ } private final ReceiveCommits rc; - private final Executor executor; + private final ScheduledExecutorService executor; private final RequestScopePropagator scopePropagator; private final MultiProgressMonitor progress; private final long timeoutMillis; @Inject AsyncReceiveCommits( - final ReceiveCommits.Factory factory, - @ReceiveCommitsExecutor final Executor executor, - final RequestScopePropagator scopePropagator, - @Named(TIMEOUT_NAME) final long timeoutMillis, - @Assisted final ProjectControl projectControl, - @Assisted final Repository repo) { + ReceiveCommits.Factory factory, + @ReceiveCommitsExecutor ScheduledExecutorService executor, + RequestScopePropagator scopePropagator, + @Named(TIMEOUT_NAME) long timeoutMillis, + @Assisted ProjectControl projectControl, + @Assisted Repository repo) { this.executor = executor; this.scopePropagator = scopePropagator; rc = factory.create(projectControl, repo); @@ -148,7 +148,7 @@ } @Override - public void onPreReceive(final ReceivePack rp, final Collection<ReceiveCommand> commands) { + public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) { try { progress.waitFor( executor.submit(scopePropagator.wrap(new Worker(commands))), @@ -163,7 +163,7 @@ rc.addError("internal error while processing changes"); // ReceiveCommits has tried its best to catch errors, so anything at this // point is very bad. - for (final ReceiveCommand c : commands) { + for (ReceiveCommand c : commands) { if (c.getResult() == Result.NOT_ATTEMPTED) { c.setResult(Result.REJECTED_OTHER_REASON, "internal error"); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java index d09e857..322d158 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -30,7 +30,6 @@ import java.util.Date; import java.util.List; import java.util.TimeZone; -import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.Constants; @@ -75,10 +74,10 @@ @Inject BanCommit( - final Provider<IdentifiedUser> currentUser, - final GitRepositoryManager repoManager, - @GerritPersonIdent final PersonIdent gerritIdent, - final NotesBranchUtil.Factory notesBranchUtilFactory) { + Provider<IdentifiedUser> currentUser, + GitRepositoryManager repoManager, + @GerritPersonIdent PersonIdent gerritIdent, + NotesBranchUtil.Factory notesBranchUtilFactory) { this.currentUser = currentUser; this.repoManager = repoManager; this.notesBranchUtilFactory = notesBranchUtilFactory; @@ -86,8 +85,8 @@ } public BanCommitResult ban( - final ProjectControl projectControl, final List<ObjectId> commitsToBan, final String reason) - throws PermissionDeniedException, IOException, ConcurrentRefUpdateException { + ProjectControl projectControl, List<ObjectId> commitsToBan, String reason) + throws PermissionDeniedException, LockFailureException, IOException { if (!projectControl.isOwner()) { throw new PermissionDeniedException("Not project owner: not permitted to ban commits"); } @@ -100,7 +99,7 @@ RevWalk revWalk = new RevWalk(repo); ObjectInserter inserter = repo.newObjectInserter()) { ObjectId noteId = null; - for (final ObjectId commitToBan : commitsToBan) { + for (ObjectId commitToBan : commitsToBan) { try { revWalk.parseCommit(commitToBan); } catch (MissingObjectException e) { @@ -146,8 +145,7 @@ return currentUser.get().newCommitterIdent(now, tz); } - private static String buildCommitMessage( - final List<ObjectId> bannedCommits, final String reason) { + private static String buildCommitMessage(List<ObjectId> bannedCommits, String reason) { final StringBuilder commitMsg = new StringBuilder(); commitMsg.append("Banning "); commitMsg.append(bannedCommits.size()); @@ -161,7 +159,7 @@ } commitMsg.append("The following commits are banned:\n"); final StringBuilder commitList = new StringBuilder(); - for (final ObjectId c : bannedCommits) { + for (ObjectId c : bannedCommits) { if (commitList.length() > 0) { commitList.append(",\n"); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java index baa6013..9fadae2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
@@ -23,15 +23,15 @@ private final List<ObjectId> alreadyBannedCommits = new ArrayList<>(4); private final List<ObjectId> ignoredObjectIds = new ArrayList<>(4); - public void commitBanned(final ObjectId commitId) { + public void commitBanned(ObjectId commitId) { newlyBannedCommits.add(commitId); } - public void commitAlreadyBanned(final ObjectId commitId) { + public void commitAlreadyBanned(ObjectId commitId) { alreadyBannedCommits.add(commitId); } - public void notACommit(final ObjectId id) { + public void notACommit(ObjectId id) { ignoredObjectIds.add(id); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java index 03d44ca..d4fa9ad 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeSet.java
@@ -22,6 +22,7 @@ import com.google.common.collect.MultimapBuilder; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.query.change.ChangeData; import com.google.gwtorm.server.OrmException; import java.util.Collection; @@ -91,6 +92,14 @@ return changeData.values(); } + public ImmutableSet<Project.NameKey> projects() throws OrmException { + ImmutableSet.Builder<Project.NameKey> ret = ImmutableSet.builder(); + for (ChangeData cd : changeData.values()) { + ret.add(cd.project()); + } + return ret.build(); + } + public ImmutableSet<Change.Id> nonVisibleIds() { return nonVisibleChanges.keySet(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java index 80c705e..83a552c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -84,7 +84,7 @@ } @Override - public void markUninteresting(final RevCommit c) + public void markUninteresting(RevCommit c) throws MissingObjectException, IncorrectObjectTypeException, IOException { checkArgument(c instanceof CodeReviewCommit); super.markUninteresting(c); @@ -120,7 +120,7 @@ */ private CommitMergeStatus statusCode; - public CodeReviewCommit(final AnyObjectId id) { + public CodeReviewCommit(AnyObjectId id) { super(id); } @@ -144,7 +144,7 @@ this.patchsetId = patchsetId; } - public void copyFrom(final CodeReviewCommit src) { + public void copyFrom(CodeReviewCommit src) { control = src.control; patchsetId = src.patchsetId; statusCode = src.statusCode;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/DefaultQueueOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/DefaultQueueOp.java index a9c21ff..b30acfa 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/DefaultQueueOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/DefaultQueueOp.java
@@ -20,7 +20,7 @@ public abstract class DefaultQueueOp implements Runnable { private final WorkQueue workQueue; - protected DefaultQueueOp(final WorkQueue wq) { + protected DefaultQueueOp(WorkQueue wq) { workQueue = wq; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java index 33c31fd..3bf89c7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
@@ -112,7 +112,7 @@ return result; } - private void fire(final Project.NameKey p, final Properties statistics) { + private void fire(Project.NameKey p, Properties statistics) { if (!listeners.iterator().hasNext()) { return; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java index e680ea7..46916c8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModules.java
@@ -23,8 +23,8 @@ import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.server.util.RequestId; import com.google.gerrit.server.util.SubmoduleSectionParser; +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.Collection; @@ -55,7 +55,7 @@ private final RequestId submissionId; Set<SubmoduleSubscription> subscriptions; - @AssistedInject + @Inject GitModules( @CanonicalWebUrl @Nullable String canonicalWebUrl, @Assisted Branch.NameKey branch,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java index 6a05d22..323f352 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
@@ -142,16 +142,6 @@ return Result.create(unchanged, updated, deleted); } - /** - * @param ctl change control (for any user). - * @param lt label type. - * @param id account ID. - * @return whether the given account ID has any permissions to vote on this label for this change. - */ - public boolean canVote(ChangeControl ctl, LabelType lt, Account.Id id) { - return !getRange(ctl, lt, id).isEmpty(); - } - private PatchSetApproval copy(PatchSetApproval src) { return new PatchSetApproval(src.getPatchSetId(), src); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LargeObjectException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LargeObjectException.java index bcde7f8..04db42c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LargeObjectException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LargeObjectException.java
@@ -25,8 +25,7 @@ private static final long serialVersionUID = 1L; - public LargeObjectException( - final String message, final org.eclipse.jgit.errors.LargeObjectException cause) { + public LargeObjectException(String message, org.eclipse.jgit.errors.LargeObjectException cause) { super(message, cause); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java index ca19d47..276de9e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -66,7 +66,7 @@ private final Config serverConfig; @Inject - Lifecycle(@GerritServerConfig final Config cfg) { + Lifecycle(@GerritServerConfig Config cfg) { this.serverConfig = cfg; } @@ -242,7 +242,7 @@ } } - private void onCreateProject(final Project.NameKey newProjectName) { + private void onCreateProject(Project.NameKey newProjectName) { namesUpdateLock.lock(); try { SortedSet<Project.NameKey> n = new TreeSet<>(names); @@ -253,7 +253,7 @@ } } - private boolean isUnreasonableName(final Project.NameKey nameKey) { + private boolean isUnreasonableName(Project.NameKey nameKey) { final String name = nameKey.get(); return name.length() == 0 // no empty paths
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java index 1511da0..353cba2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -20,6 +20,8 @@ import static java.util.Comparator.comparing; 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.collect.ImmutableListMultimap; @@ -40,6 +42,9 @@ 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.metrics.Counter0; +import com.google.gerrit.metrics.Description; +import com.google.gerrit.metrics.MetricMaker; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; @@ -66,10 +71,13 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.RetryHelper; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.RequestId; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; @@ -80,6 +88,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; @@ -103,14 +112,17 @@ private static final Logger log = LoggerFactory.getLogger(MergeOp.class); private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS = SubmitRuleOptions.defaults().build(); + private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_ALLOW_CLOSED = + SUBMIT_RULE_OPTIONS.toBuilder().allowClosed(true).build(); public static class CommitStatus { private final ImmutableMap<Change.Id, ChangeData> changes; private final ImmutableSetMultimap<Branch.NameKey, Change.Id> byBranch; private final Map<Change.Id, CodeReviewCommit> commits; private final ListMultimap<Change.Id, String> problems; + private final boolean allowClosed; - private CommitStatus(ChangeSet cs) throws OrmException { + private CommitStatus(ChangeSet cs, boolean allowClosed) throws OrmException { checkArgument( !cs.furtherHiddenChanges(), "CommitStatus must not be called with hidden changes"); changes = cs.changesById(); @@ -121,6 +133,7 @@ byBranch = bb.build(); commits = new HashMap<>(); problems = MultimapBuilder.treeKeys(comparing(Change.Id::get)).arrayListValues(1).build(); + this.allowClosed = allowClosed; } public ImmutableSet<Change.Id> getChangeIds() { @@ -173,7 +186,7 @@ // date by this point. ChangeData cd = checkNotNull(changes.get(id), "ChangeData for %s", id); return checkNotNull( - cd.getSubmitRecords(SUBMIT_RULE_OPTIONS), + cd.getSubmitRecords(submitRuleOptions(allowClosed)), "getSubmitRecord only valid after submit rules are evalutated"); } @@ -217,19 +230,22 @@ private final InternalChangeQuery internalChangeQuery; private final SubmitStrategyFactory submitStrategyFactory; private final SubmoduleOp.Factory subOpFactory; - private final MergeOpRepoManager orm; + private final Provider<MergeOpRepoManager> ormProvider; private final NotifyUtil notifyUtil; + private final RetryHelper retryHelper; private Timestamp ts; private RequestId submissionId; private IdentifiedUser caller; + private MergeOpRepoManager orm; private CommitStatus commitStatus; private ReviewDb db; private SubmitInput submitInput; private ListMultimap<RecipientType, Account.Id> accountsToNotify; private Set<Project.NameKey> allProjects; private boolean dryrun; + private TopicMetrics topicMetrics; @Inject MergeOp( @@ -241,8 +257,10 @@ InternalChangeQuery internalChangeQuery, SubmitStrategyFactory submitStrategyFactory, SubmoduleOp.Factory subOpFactory, - MergeOpRepoManager orm, - NotifyUtil notifyUtil) { + Provider<MergeOpRepoManager> ormProvider, + NotifyUtil notifyUtil, + TopicMetrics topicMetrics, + RetryHelper retryHelper) { this.cmUtil = cmUtil; this.batchUpdateFactory = batchUpdateFactory; this.internalUserFactory = internalUserFactory; @@ -251,21 +269,26 @@ this.internalChangeQuery = internalChangeQuery; this.submitStrategyFactory = submitStrategyFactory; this.subOpFactory = subOpFactory; - this.orm = orm; + this.ormProvider = ormProvider; this.notifyUtil = notifyUtil; + this.retryHelper = retryHelper; + this.topicMetrics = topicMetrics; } @Override public void close() { - orm.close(); + if (orm != null) { + orm.close(); + } } - public static void checkSubmitRule(ChangeData cd) throws ResourceConflictException, OrmException { + public static void checkSubmitRule(ChangeData cd, boolean allowClosed) + throws ResourceConflictException, OrmException { PatchSet patchSet = cd.currentPatchSet(); if (patchSet == null) { throw new ResourceConflictException("missing current patch set for change " + cd.getId()); } - List<SubmitRecord> results = getSubmitRecords(cd); + List<SubmitRecord> results = getSubmitRecords(cd, allowClosed); if (SubmitRecord.findOkRecord(results).isPresent()) { // Rules supplied a valid solution. return; @@ -299,8 +322,13 @@ throw new IllegalStateException(); } - private static List<SubmitRecord> getSubmitRecords(ChangeData cd) throws OrmException { - return cd.submitRecords(SUBMIT_RULE_OPTIONS); + private static SubmitRuleOptions submitRuleOptions(boolean allowClosed) { + return allowClosed ? SUBMIT_RULE_OPTIONS_ALLOW_CLOSED : SUBMIT_RULE_OPTIONS; + } + + private static List<SubmitRecord> getSubmitRecords(ChangeData cd, boolean allowClosed) + throws OrmException { + return cd.submitRecords(submitRuleOptions(allowClosed)); } private static String describeLabels(ChangeData cd, List<SubmitRecord.Label> labels) @@ -334,17 +362,23 @@ return Joiner.on("; ").join(labelResults); } - private void checkSubmitRulesAndState(ChangeSet cs) throws ResourceConflictException { + private void checkSubmitRulesAndState(ChangeSet cs, boolean allowMerged) + throws ResourceConflictException { checkArgument( !cs.furtherHiddenChanges(), "checkSubmitRulesAndState called for topic with hidden change"); for (ChangeData cd : cs.changes()) { try { - if (cd.change().getStatus() != Change.Status.NEW) { - commitStatus.problem( - cd.getId(), - "Change " + cd.getId() + " is " + cd.change().getStatus().toString().toLowerCase()); + Change.Status status = cd.change().getStatus(); + if (status != Change.Status.NEW) { + if (!(status == Change.Status.MERGED && allowMerged)) { + commitStatus.problem( + cd.getId(), + "Change " + cd.getId() + " is " + cd.change().getStatus().toString().toLowerCase()); + } + } else if (cd.change().isWorkInProgress()) { + commitStatus.problem(cd.getId(), "Change " + cd.getId() + " is work in progress"); } else { - checkSubmitRule(cd); + checkSubmitRule(cd, allowMerged); } } catch (ResourceConflictException e) { commitStatus.problem(cd.getId(), e.getMessage()); @@ -357,13 +391,13 @@ commitStatus.maybeFailVerbose(); } - private void bypassSubmitRules(ChangeSet cs) { + private void bypassSubmitRules(ChangeSet cs, boolean allowClosed) { checkArgument( !cs.furtherHiddenChanges(), "cannot bypass submit rules for topic with hidden change"); for (ChangeData cd : cs.changes()) { List<SubmitRecord> records; try { - records = new ArrayList<>(getSubmitRecords(cd)); + records = new ArrayList<>(getSubmitRecords(cd, allowClosed)); } catch (OrmException e) { log.warn("Error checking submit rules for change " + cd.getId(), e); records = new ArrayList<>(1); @@ -371,7 +405,7 @@ SubmitRecord forced = new SubmitRecord(); forced.status = SubmitRecord.Status.FORCED; records.add(forced); - cd.setSubmitRecords(SUBMIT_RULE_OPTIONS, records); + cd.setSubmitRecords(submitRuleOptions(allowClosed), records); } } @@ -397,7 +431,7 @@ boolean checkSubmitRules, SubmitInput submitInput, boolean dryrun) - throws OrmException, RestApiException { + throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException { this.submitInput = submitInput; this.accountsToNotify = notifyUtil.resolveAccounts(submitInput.notifyDetails); this.dryrun = dryrun; @@ -405,7 +439,7 @@ this.ts = TimeUtil.nowTs(); submissionId = RequestId.forChange(change); this.db = db; - orm.setContext(db, ts, caller, submissionId); + openRepoManager(); logDebug("Beginning integration of {}", change); try { @@ -416,21 +450,47 @@ throw new AuthException( "A change to be submitted with " + change.getId() + " is not visible"); } - this.commitStatus = new CommitStatus(cs); - MergeSuperSet.reloadChanges(cs); logDebug("Calculated to merge {}", cs); - if (checkSubmitRules) { - logDebug("Checking submit rules and state"); - checkSubmitRulesAndState(cs); - } else { - logDebug("Bypassing submit rules"); - bypassSubmitRules(cs); + + // Count cross-project submissions outside of the retry loop. The chance of a single project + // failing increases with the number of projects, so the failure count would be inflated if + // this metric were incremented inside of integrateIntoHistory. + int projects = cs.projects().size(); + if (projects > 1) { + topicMetrics.topicSubmissions.increment(); } - try { - integrateIntoHistory(cs); - } catch (IntegrationException e) { - logError("Error from integrateIntoHistory", e); - throw new ResourceConflictException(e.getMessage(), e); + + RetryTracker retryTracker = new RetryTracker(); + retryHelper.execute( + updateFactory -> { + long attempt = retryTracker.lastAttemptNumber + 1; + boolean isRetry = attempt > 1; + if (isRetry) { + logDebug("Retrying, attempt #{}; skipping merged changes", attempt); + this.ts = TimeUtil.nowTs(); + openRepoManager(); + } + this.commitStatus = new CommitStatus(cs, isRetry); + MergeSuperSet.reloadChanges(cs); + if (checkSubmitRules) { + logDebug("Checking submit rules and state"); + checkSubmitRulesAndState(cs, isRetry); + } else { + logDebug("Bypassing submit rules"); + bypassSubmitRules(cs, isRetry); + } + try { + integrateIntoHistory(cs); + } catch (IntegrationException e) { + logError("Error from integrateIntoHistory", e); + throw new ResourceConflictException(e.getMessage(), e); + } + return null; + }, + retryTracker); + + if (projects > 1) { + topicMetrics.topicSubmissionsCompleted.increment(); } } catch (IOException e) { // Anything before the merge attempt is an error @@ -438,7 +498,44 @@ } } - private void integrateIntoHistory(ChangeSet cs) throws IntegrationException, RestApiException { + private void openRepoManager() { + if (orm != null) { + orm.close(); + } + orm = ormProvider.get(); + orm.setContext(db, ts, caller, submissionId); + } + + private class RetryTracker implements RetryListener { + long lastAttemptNumber; + + @Override + public <V> void onRetry(Attempt<V> attempt) { + lastAttemptNumber = attempt.getAttemptNumber(); + } + } + + @Singleton + private static class TopicMetrics { + final Counter0 topicSubmissions; + final Counter0 topicSubmissionsCompleted; + + @Inject + TopicMetrics(MetricMaker metrics) { + topicSubmissions = + metrics.newCounter( + "topic/cross_project_submit", + new Description("Attempts at cross project topic submission").setRate()); + topicSubmissionsCompleted = + metrics.newCounter( + "topic/cross_project_submit_completed", + new Description("Cross project topic submissions that concluded successfully") + .setRate()); + } + } + + private void integrateIntoHistory(ChangeSet cs) + throws IntegrationException, RestApiException, UpdateException { checkArgument(!cs.furtherHiddenChanges(), "cannot integrate hidden changes into history"); logDebug("Beginning merge attempt on {}", cs); Map<Branch.NameKey, BranchBatch> toSubmit = new HashMap<>(); @@ -450,16 +547,19 @@ throw new IntegrationException("Error reading changes to submit", e); } Set<Branch.NameKey> branches = cbb.keySet(); + for (Branch.NameKey branch : branches) { OpenRepo or = openRepo(branch.getParentKey()); if (or != null) { toSubmit.put(branch, validateChangeList(or, cbb.get(branch))); } } + // Done checks that don't involve running submit strategies. commitStatus.maybeFailVerbose(); - SubmoduleOp submoduleOp = subOpFactory.create(branches, orm); + try { + SubmoduleOp submoduleOp = subOpFactory.create(branches, orm); List<SubmitStrategy> strategies = getSubmitStrategies(toSubmit, submoduleOp, dryrun); this.allProjects = submoduleOp.getProjectsInOrder(); batchUpdateFactory.execute( @@ -472,6 +572,15 @@ } catch (IOException | SubmoduleException e) { throw new IntegrationException(e); } catch (UpdateException e) { + if (e.getCause() instanceof LockFailureException) { + // Lock failures are a special case: RetryHelper depends on this specific causal chain in + // order to trigger a retry. The downside of throwing here is we will not get the nicer + // error message constructed below, in the case where this is the final attempt and the + // operation is not retried further. This is not a huge downside, and is hopefully so rare + // as to be unnoticeable, assuming RetryHelper is retrying sufficiently. + throw e; + } + // BatchUpdate may have inadvertently wrapped an IntegrationException // thrown by some legacy SubmitStrategyOp code that intended the error // message to be user-visible. Copy the message from the wrapped @@ -519,9 +628,7 @@ submitStrategyFactory.create( submitting.submitType(), db, - or.repo, or.rw, - or.ins, or.canMergeFlag, getAlreadyAccepted(or, ob.oldTip), allCommits, @@ -530,7 +637,7 @@ ob.mergeTip, commitStatus, submissionId, - submitInput.notify, + submitInput, accountsToNotify, submoduleOp, dryrun);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java index 78fc495..d547d7f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
@@ -29,20 +29,20 @@ private final CodeReviewRevWalk rw; private final RevFlag canMergeFlag; private final Set<RevCommit> accepted; + private final Set<CodeReviewCommit> incoming; - public MergeSorter(CodeReviewRevWalk rw, Set<RevCommit> alreadyAccepted, RevFlag canMergeFlag) { + public MergeSorter( + CodeReviewRevWalk rw, + Set<RevCommit> alreadyAccepted, + RevFlag canMergeFlag, + Set<CodeReviewCommit> incoming) { this.rw = rw; this.canMergeFlag = canMergeFlag; this.accepted = alreadyAccepted; + this.incoming = incoming; } - Collection<CodeReviewCommit> sort(final Collection<CodeReviewCommit> toMerge) throws IOException { - return sort(toMerge, toMerge); - } - - Collection<CodeReviewCommit> sort( - final Collection<CodeReviewCommit> toMerge, final Collection<CodeReviewCommit> incoming) - throws IOException { + Collection<CodeReviewCommit> sort(Collection<CodeReviewCommit> toMerge) throws IOException { final Set<CodeReviewCommit> heads = new HashSet<>(); final Set<CodeReviewCommit> sort = new HashSet<>(toMerge); while (!sort.isEmpty()) { @@ -82,7 +82,7 @@ return heads; } - private static <T> T removeOne(final Collection<T> c) { + private static <T> T removeOne(Collection<T> c) { final Iterator<T> i = c.iterator(); final T r = i.next(); i.remove();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java index 9dc13d0..485caff 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSuperSet.java
@@ -31,6 +31,7 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.change.Submit; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.git.MergeOpRepoManager.OpenRepo; @@ -94,6 +95,7 @@ abstract ImmutableSet<String> hashes(); } + private final AccountCache accountCache; private final ChangeData.Factory changeDataFactory; private final Provider<InternalChangeQuery> queryProvider; private final Provider<MergeOpRepoManager> repoManagerProvider; @@ -107,10 +109,12 @@ @Inject MergeSuperSet( @GerritServerConfig Config cfg, + AccountCache accountCache, ChangeData.Factory changeDataFactory, Provider<InternalChangeQuery> queryProvider, Provider<MergeOpRepoManager> repoManagerProvider) { this.cfg = cfg; + this.accountCache = accountCache; this.changeDataFactory = changeDataFactory; this.queryProvider = queryProvider; this.repoManagerProvider = repoManagerProvider; @@ -161,7 +165,7 @@ SubmitTypeRecord str = ps == cd.currentPatchSet() ? cd.submitTypeRecord() - : new SubmitRuleEvaluator(cd).setPatchSet(ps).getSubmitType(); + : new SubmitRuleEvaluator(accountCache, cd).setPatchSet(ps).getSubmitType(); if (!str.isOk()) { logErrorAndThrow("Failed to get submit type for " + cd.getId() + ": " + str.errorMessage); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java index 2526db194..8026cae 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -192,9 +192,9 @@ } public CodeReviewCommit getFirstFastForward( - final CodeReviewCommit mergeTip, final RevWalk rw, final List<CodeReviewCommit> toMerge) + CodeReviewCommit mergeTip, RevWalk rw, List<CodeReviewCommit> toMerge) throws IntegrationException { - for (final Iterator<CodeReviewCommit> i = toMerge.iterator(); i.hasNext(); ) { + for (Iterator<CodeReviewCommit> i = toMerge.iterator(); i.hasNext(); ) { try { final CodeReviewCommit n = i.next(); if (mergeTip == null || rw.isMergedInto(mergeTip, n)) { @@ -209,11 +209,10 @@ } public List<CodeReviewCommit> reduceToMinimalMerge( - MergeSorter mergeSorter, Collection<CodeReviewCommit> toSort, Set<CodeReviewCommit> incoming) - throws IntegrationException { + MergeSorter mergeSorter, Collection<CodeReviewCommit> toSort) throws IntegrationException { List<CodeReviewCommit> result = new ArrayList<>(); try { - result.addAll(mergeSorter.sort(toSort, incoming)); + result.addAll(mergeSorter.sort(toSort)); } catch (IOException e) { throw new IntegrationException("Branch head sorting failed", e); } @@ -222,8 +221,8 @@ } public CodeReviewCommit createCherryPickFromCommit( - Repository repo, ObjectInserter inserter, + Config repoConfig, RevCommit mergeTip, RevCommit originalCommit, PersonIdent cherryPickCommitterIdent, @@ -234,7 +233,7 @@ throws MissingObjectException, IncorrectObjectTypeException, IOException, MergeIdenticalTreeException, MergeConflictException { - final ThreeWayMerger m = newThreeWayMerger(repo, inserter); + final ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig); m.setBase(originalCommit.getParent(parentIndex)); if (m.merge(mergeTip, originalCommit)) { @@ -249,14 +248,15 @@ mergeCommit.setAuthor(originalCommit.getAuthorIdent()); mergeCommit.setCommitter(cherryPickCommitterIdent); mergeCommit.setMessage(commitMsg); + matchAuthorToCommitterDate(project, mergeCommit); return rw.parseCommit(inserter.insert(mergeCommit)); } throw new MergeConflictException("merge conflict"); } public static RevCommit createMergeCommit( - Repository repo, ObjectInserter inserter, + Config repoConfig, RevCommit mergeTip, RevCommit originalCommit, String mergeStrategy, @@ -271,7 +271,7 @@ "'" + originalCommit.getName() + "' has already been merged"); } - Merger m = newMerger(repo, inserter, mergeStrategy); + Merger m = newMerger(inserter, repoConfig, mergeStrategy); if (m.merge(false, mergeTip, originalCommit)) { ObjectId tree = m.getResultTreeId(); @@ -355,7 +355,7 @@ PatchSetApproval submitAudit = null; - for (final PatchSetApproval a : safeGetApprovals(ctl, psId)) { + for (PatchSetApproval a : safeGetApprovals(ctl, psId)) { if (a.getValue() <= 0) { // Negative votes aren't counted. continue; @@ -450,7 +450,7 @@ private Iterable<PatchSetApproval> safeGetApprovals(ChangeControl ctl, PatchSet.Id psId) { try { - return approvalsUtil.byPatchSet(db.get(), ctl, psId); + return approvalsUtil.byPatchSet(db.get(), ctl, psId, null, null); } catch (OrmException e) { log.error("Can't read approval records for " + psId, e); return Collections.emptyList(); @@ -458,7 +458,7 @@ } private static boolean contains(List<FooterLine> footers, FooterKey key, String val) { - for (final FooterLine line : footers) { + for (FooterLine line : footers) { if (line.matches(key) && val.equals(line.getValue())) { return true; } @@ -467,7 +467,7 @@ } private static boolean isSignedOffBy(List<FooterLine> footers, String email) { - for (final FooterLine line : footers) { + for (FooterLine line : footers) { if (line.matches(FooterKey.SIGNED_OFF_BY) && email.equals(line.getEmailAddress())) { return true; } @@ -476,17 +476,14 @@ } public boolean canMerge( - final MergeSorter mergeSorter, - final Repository repo, - final CodeReviewCommit mergeTip, - final CodeReviewCommit toMerge) + MergeSorter mergeSorter, Repository repo, CodeReviewCommit mergeTip, CodeReviewCommit toMerge) throws IntegrationException { if (hasMissingDependencies(mergeSorter, toMerge)) { return false; } try (ObjectInserter ins = new InMemoryInserter(repo)) { - return newThreeWayMerger(repo, ins).merge(new AnyObjectId[] {mergeTip, toMerge}); + return newThreeWayMerger(ins, repo.getConfig()).merge(new AnyObjectId[] {mergeTip, toMerge}); } catch (LargeObjectException e) { log.warn("Cannot merge due to LargeObjectException: " + toMerge.name()); return false; @@ -542,7 +539,7 @@ // that on the current merge tip. // try (ObjectInserter ins = new InMemoryInserter(repo)) { - ThreeWayMerger m = newThreeWayMerger(repo, ins); + ThreeWayMerger m = newThreeWayMerger(ins, repo.getConfig()); m.setBase(toMerge.getParent(0)); return m.merge(mergeTip, toMerge); } catch (IOException e) { @@ -563,8 +560,8 @@ || canMerge(mergeSorter, repo, mergeTip, toMerge); } - public boolean hasMissingDependencies( - final MergeSorter mergeSorter, final CodeReviewCommit toMerge) throws IntegrationException { + public boolean hasMissingDependencies(MergeSorter mergeSorter, CodeReviewCommit toMerge) + throws IntegrationException { try { return !mergeSorter.sort(Collections.singleton(toMerge)).contains(toMerge); } catch (IOException e) { @@ -575,14 +572,14 @@ public CodeReviewCommit mergeOneCommit( PersonIdent author, PersonIdent committer, - Repository repo, CodeReviewRevWalk rw, ObjectInserter inserter, + Config repoConfig, Branch.NameKey destBranch, CodeReviewCommit mergeTip, CodeReviewCommit n) throws IntegrationException { - final ThreeWayMerger m = newThreeWayMerger(repo, inserter); + ThreeWayMerger m = newThreeWayMerger(inserter, repoConfig); try { if (m.merge(new AnyObjectId[] {mergeTip, n})) { return writeMergeCommit( @@ -657,7 +654,7 @@ if (merged.size() > 1) { msgbuf.append("\n\n* changes:\n"); - for (final CodeReviewCommit c : merged) { + for (CodeReviewCommit c : merged) { rw.parseBody(c); msgbuf.append(" "); msgbuf.append(c.getShortMessage()); @@ -706,8 +703,8 @@ } } - public ThreeWayMerger newThreeWayMerger(final Repository repo, final ObjectInserter inserter) { - return newThreeWayMerger(repo, inserter, mergeStrategyName()); + public ThreeWayMerger newThreeWayMerger(ObjectInserter inserter, Config repoConfig) { + return newThreeWayMerger(inserter, repoConfig, mergeStrategyName()); } public String mergeStrategyName() { @@ -730,8 +727,8 @@ } public static ThreeWayMerger newThreeWayMerger( - Repository repo, final ObjectInserter inserter, String strategyName) { - Merger m = newMerger(repo, inserter, strategyName); + ObjectInserter inserter, Config repoConfig, String strategyName) { + Merger m = newMerger(inserter, repoConfig, strategyName); checkArgument( m instanceof ThreeWayMerger, "merge strategy %s does not support three-way merging", @@ -739,12 +736,10 @@ return (ThreeWayMerger) m; } - public static Merger newMerger( - Repository repo, final ObjectInserter inserter, String strategyName) { + public static Merger newMerger(ObjectInserter inserter, Config repoConfig, String strategyName) { MergeStrategy strategy = MergeStrategy.get(strategyName); checkArgument(strategy != null, "invalid merge strategy: %s", strategyName); - Merger m = strategy.newMerger(repo, true); - m.setObjectInserter( + return strategy.newMerger( new ObjectInserter.Filter() { @Override protected ObjectInserter delegate() { @@ -756,15 +751,12 @@ @Override public void close() {} - }); - return m; + }, + repoConfig); } public void markCleanMerges( - final RevWalk rw, - final RevFlag canMergeFlag, - final CodeReviewCommit mergeTip, - final Set<RevCommit> alreadyAccepted) + RevWalk rw, RevFlag canMergeFlag, CodeReviewCommit mergeTip, Set<RevCommit> alreadyAccepted) throws IntegrationException { if (mergeTip == null) { // If mergeTip is null here, branchTip was null, indicating a new branch @@ -866,4 +858,14 @@ throw new ResourceNotFoundException(e.getMessage()); } } + + private static void matchAuthorToCommitterDate(ProjectState project, CommitBuilder commit) { + if (project.isMatchAuthorToCommitterDate()) { + commit.setAuthor( + new PersonIdent( + commit.getAuthor(), + commit.getCommitter().getWhen(), + commit.getCommitter().getTimeZone())); + } + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java index a7c8b53..59017e7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergedByPushOp.java
@@ -34,9 +34,9 @@ import com.google.gerrit.server.update.Context; import com.google.gerrit.server.util.RequestScopePropagator; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.io.IOException; import java.util.Collections; import java.util.concurrent.ExecutorService; @@ -74,7 +74,7 @@ private PatchSet patchSet; private PatchSetInfo info; - @AssistedInject + @Inject MergedByPushOp( PatchSetInfoFactory patchSetInfoFactory, ChangeMessagesUtil cmUtil, @@ -166,7 +166,7 @@ } @Override - public void postUpdate(final Context ctx) { + public void postUpdate(Context ctx) { if (!correctBranch) { return; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java index e3b1ad6..b057c92 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -22,7 +22,6 @@ import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.io.IOException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.BatchRefUpdate; @@ -185,7 +184,7 @@ private boolean closeRepository; private IdentifiedUser author; - @AssistedInject + @Inject public MetaDataUpdate( GitReferenceUpdated gitRefUpdated, @Assisted Project.NameKey projectName,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java index 9101b44..2b9cad1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -63,7 +63,7 @@ private int count; private int lastPercent; - Task(final String subTaskName, final int totalWork) { + Task(String subTaskName, int totalWork) { this.name = subTaskName; this.total = totalWork; } @@ -76,7 +76,7 @@ * @param completed number of work units completed. */ @Override - public void update(final int completed) { + public void update(int completed) { boolean w = false; synchronized (MultiProgressMonitor.this) { count += completed; @@ -141,7 +141,7 @@ * @param out stream for writing progress messages. * @param taskName name of the overall task. */ - public MultiProgressMonitor(final OutputStream out, final String taskName) { + public MultiProgressMonitor(OutputStream out, String taskName) { this(out, taskName, 500, TimeUnit.MILLISECONDS); } @@ -154,10 +154,7 @@ * @param maxIntervalUnit time unit for progress interval. */ public MultiProgressMonitor( - final OutputStream out, - final String taskName, - long maxIntervalTime, - TimeUnit maxIntervalUnit) { + OutputStream out, String taskName, long maxIntervalTime, TimeUnit maxIntervalUnit) { this.out = out; this.taskName = taskName; maxIntervalNanos = NANOSECONDS.convert(maxIntervalTime, maxIntervalUnit); @@ -168,7 +165,7 @@ * * @see #waitFor(Future, long, TimeUnit) */ - public void waitFor(final Future<?> workerFuture) throws ExecutionException { + public void waitFor(Future<?> workerFuture) throws ExecutionException { waitFor(workerFuture, 0, null); } @@ -186,8 +183,7 @@ * @throws ExecutionException if this thread or a worker thread was interrupted, the worker was * cancelled, or timed out waiting for a worker to call {@link #end()}. */ - public void waitFor( - final Future<?> workerFuture, final long timeoutTime, final TimeUnit timeoutUnit) + public void waitFor(Future<?> workerFuture, long timeoutTime, TimeUnit timeoutUnit) throws ExecutionException { long overallStart = System.nanoTime(); long deadline; @@ -268,7 +264,7 @@ * @param subTaskWork total work units in sub-task, or {@link #UNKNOWN}. * @return sub-task handle. */ - public Task beginSubTask(final String subTask, final int subTaskWork) { + public Task beginSubTask(String subTask, int subTaskWork) { Task task = new Task(subTask, subTaskWork); tasks.add(task); return task;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java index 2020550..24b3727 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -14,32 +14,29 @@ package com.google.gerrit.server.git; +import static com.google.common.base.MoreObjects.firstNonNull; + import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.update.RefUpdateUtil; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; import java.io.IOException; -import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; -import org.eclipse.jgit.errors.CorruptObjectException; -import org.eclipse.jgit.errors.IncorrectObjectTypeException; -import org.eclipse.jgit.errors.MissingObjectException; +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.ObjectReader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.RefUpdate; -import org.eclipse.jgit.lib.RefUpdate.Result; import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.notes.Note; import org.eclipse.jgit.notes.NoteMap; -import org.eclipse.jgit.notes.NoteMapMerger; import org.eclipse.jgit.notes.NoteMerger; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; /** A utility class for updating a notes branch with automatic merge of note trees. */ public class NotesBranchUtil { @@ -47,9 +44,6 @@ NotesBranchUtil create(Project.NameKey project, Repository db, ObjectInserter inserter); } - private static final int MAX_LOCK_FAILURE_CALLS = 10; - private static final int SLEEP_ON_LOCK_FAILURE_MS = 25; - private final PersonIdent gerritIdent; private final GitReferenceUpdated gitRefUpdated; private final Project.NameKey project; @@ -70,8 +64,8 @@ @Inject public NotesBranchUtil( - @GerritPersonIdent final PersonIdent gerritIdent, - final GitReferenceUpdated gitRefUpdated, + @GerritPersonIdent PersonIdent gerritIdent, + GitReferenceUpdated gitRefUpdated, @Assisted Project.NameKey project, @Assisted Repository db, @Assisted ObjectInserter inserter) { @@ -86,16 +80,20 @@ * Create a new commit in the {@code notesBranch} by updating existing or creating new notes from * the {@code notes} map. * + * <p>Does not retry in the case of lock failure; callers may use {@link + * com.google.gerrit.server.update.RetryHelper}. + * * @param notes map of notes * @param notesBranch notes branch to update * @param commitAuthor author of the commit in the notes branch * @param commitMessage for the commit in the notes branch - * @throws IOException - * @throws ConcurrentRefUpdateException + * @throws LockFailureException if committing the notes failed due to a lock failure on the notes + * branch + * @throws IOException if committing the notes failed for any other reason */ public final void commitAllNotes( NoteMap notes, String notesBranch, PersonIdent commitAuthor, String commitMessage) - throws IOException, ConcurrentRefUpdateException { + throws IOException { this.overwrite = true; commitNotes(notes, notesBranch, commitAuthor, commitMessage); } @@ -105,17 +103,21 @@ * {@code notes} map. The notes from the {@code notes} map which already exist in the note-tree of * the tip of the {@code notesBranch} will not be updated. * + * <p>Does not retry in the case of lock failure; callers may use {@link + * com.google.gerrit.server.update.RetryHelper}. + * * @param notes map of notes * @param notesBranch notes branch to update * @param commitAuthor author of the commit in the notes branch * @param commitMessage for the commit in the notes branch * @return map with those notes from the {@code notes} that were newly created - * @throws IOException - * @throws ConcurrentRefUpdateException + * @throws LockFailureException if committing the notes failed due to a lock failure on the notes + * branch + * @throws IOException if committing the notes failed for any other reason */ public final NoteMap commitNewNotes( NoteMap notes, String notesBranch, PersonIdent commitAuthor, String commitMessage) - throws IOException, ConcurrentRefUpdateException { + throws IOException { this.overwrite = false; commitNotes(notes, notesBranch, commitAuthor, commitMessage); NoteMap newlyCreated = NoteMap.newEmptyMap(); @@ -129,7 +131,7 @@ private void commitNotes( NoteMap notes, String notesBranch, PersonIdent commitAuthor, String commitMessage) - throws IOException, ConcurrentRefUpdateException { + throws LockFailureException, IOException { try { revWalk = new RevWalk(db); reader = db.newObjectReader(); @@ -209,61 +211,16 @@ return revWalk.parseCommit(commitId); } - private void updateRef(String notesBranch) - throws IOException, MissingObjectException, IncorrectObjectTypeException, - CorruptObjectException, ConcurrentRefUpdateException { + private void updateRef(String notesBranch) throws LockFailureException, IOException { if (baseCommit != null && oursCommit.getTree().equals(baseCommit.getTree())) { // If the trees are identical, there is no change in the notes. // Avoid saving this commit as it has no new information. return; } - - int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS; - RefUpdate refUpdate = createRefUpdate(notesBranch, oursCommit, baseCommit); - - for (; ; ) { - Result result = refUpdate.update(); - - if (result == Result.LOCK_FAILURE) { - if (--remainingLockFailureCalls > 0) { - try { - Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS); - } catch (InterruptedException e) { - // ignore - } - } else { - throw new ConcurrentRefUpdateException( - "Failed to lock the ref: " + notesBranch, refUpdate.getRef(), result); - } - - } else if (result == Result.REJECTED) { - RevCommit theirsCommit = revWalk.parseCommit(refUpdate.getOldObjectId()); - NoteMap theirs = NoteMap.read(revWalk.getObjectReader(), theirsCommit); - NoteMapMerger merger = new NoteMapMerger(db, getNoteMerger(), MergeStrategy.RESOLVE); - NoteMap merged = merger.merge(base, ours, theirs); - RevCommit mergeCommit = - createCommit(merged, gerritIdent, "Merged note commits\n", theirsCommit, oursCommit); - refUpdate = createRefUpdate(notesBranch, mergeCommit, theirsCommit); - remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS; - - } else if (result == Result.IO_FAILURE) { - throw new IOException("Couldn't update " + notesBranch + ". " + result.name()); - } else { - gitRefUpdated.fire(project, refUpdate, null); - break; - } - } - } - - private RefUpdate createRefUpdate( - String notesBranch, ObjectId newObjectId, ObjectId expectedOldObjectId) throws IOException { - RefUpdate refUpdate = db.updateRef(notesBranch); - refUpdate.setNewObjectId(newObjectId); - if (expectedOldObjectId == null) { - refUpdate.setExpectedOldObjectId(ObjectId.zeroId()); - } else { - refUpdate.setExpectedOldObjectId(expectedOldObjectId); - } - return refUpdate; + BatchRefUpdate bru = db.getRefDatabase().newBatchUpdate(); + bru.addCommand( + new ReceiveCommand(firstNonNull(baseCommit, ObjectId.zeroId()), oursCommit, notesBranch)); + RefUpdateUtil.executeChecked(bru, revWalk); + gitRefUpdated.fire(project, bru, null); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java index 20f053a..a4719a9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
@@ -64,18 +64,15 @@ } public <T> Callable<T> scope(RequestContext requestContext, Callable<T> callable) { - final Context ctx = new Context(); - final Callable<T> wrapped = context(requestContext, cleanup(callable)); - return new Callable<T>() { - @Override - public T call() throws Exception { - Context old = current.get(); - current.set(ctx); - try { - return wrapped.call(); - } finally { - current.set(old); - } + Context ctx = new Context(); + Callable<T> wrapped = context(requestContext, cleanup(callable)); + return () -> { + Context old = current.get(); + current.set(ctx); + try { + return wrapped.call(); + } finally { + current.set(old); } }; } @@ -94,7 +91,7 @@ public static final Scope REQUEST = new Scope() { @Override - public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) { + public <T> Provider<T> scope(Key<T> key, Provider<T> creator) { return new Provider<T>() { @Override public T get() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java index 61d8cfe..91379fd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -88,6 +88,8 @@ private static final String PROJECT = "project"; private static final String KEY_DESCRIPTION = "description"; + private static final String KEY_MATCH_AUTHOR_DATE_WITH_COMMITTER_DATE = + "matchAuthorToCommitterDate"; public static final String ACCESS = "access"; private static final String KEY_INHERIT_FROM = "inheritFrom"; @@ -155,6 +157,9 @@ ImmutableSet.of( "MaxWithBlock", "AnyWithBlock", "MaxNoBlock", "NoBlock", "NoOp", "PatchSetLock"); + private static final String REVIEWER = "reviewer"; + private static final String KEY_ENABLE_REVIEWER_BY_EMAIL = "enableByEmail"; + private static final String LEGACY_PERMISSION_PUSH_TAG = "pushTag"; private static final String LEGACY_PERMISSION_PUSH_SIGNED_TAG = "pushSignedTag"; @@ -163,6 +168,9 @@ private static final SubmitType DEFAULT_SUBMIT_ACTION = SubmitType.MERGE_IF_NECESSARY; private static final ProjectState DEFAULT_STATE_VALUE = ProjectState.ACTIVE; + private static final String EXTENSION_PANELS = "extension-panels"; + private static final String KEY_PANEL = "panel"; + private Project.NameKey projectName; private Project project; private AccountsSection accountsSection; @@ -182,6 +190,8 @@ private boolean checkReceivedObjects; private Set<String> sectionsWithUnknownPermissions; private boolean hasLegacyPermissions; + private Map<String, List<String>> extensionPanelSections; + private Map<String, GroupReference> groupsByName; public static ProjectConfig read(MetaDataUpdate update) throws IOException, ConfigInvalidException { @@ -197,6 +207,10 @@ return r; } + public Map<String, List<String>> getExtensionPanelSections() { + return extensionPanelSections; + } + public static CommentLinkInfoImpl buildCommentLink(Config cfg, String name, boolean allowRaw) throws IllegalArgumentException { String match = cfg.getString(COMMENTLINK, name, KEY_MATCH); @@ -402,7 +416,13 @@ } public GroupReference resolve(GroupReference group) { - return groupList.resolve(group); + GroupReference groupRef = groupList.resolve(group); + if (groupRef != null + && groupRef.getUUID() != null + && !groupsByName.containsKey(groupRef.getName())) { + groupsByName.put(groupRef.getName(), groupRef); + } + return groupRef; } /** @return the group reference, if the group is used by at least one rule. */ @@ -410,6 +430,14 @@ return groupList.byUUID(uuid); } + /** + * @return the group reference corresponding to the specified group name if the group is used by + * at least one rule or plugin value. + */ + public GroupReference getGroup(String groupName) { + return groupsByName.get(groupName); + } + /** @return set of all groups used by this configuration. */ public Set<AccountGroup.UUID> getAllGroupUUIDs() { return groupList.uuids(); @@ -473,7 +501,7 @@ @Override protected void onLoad() throws IOException, ConfigInvalidException { readGroupList(); - Map<String, GroupReference> groupsByName = mapGroupReferences(); + groupsByName = mapGroupReferences(); rulesId = getObjectId("rules.pl"); Config rc = readConfig(PROJECT_CONFIG); @@ -507,34 +535,63 @@ p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT)); p.setRejectImplicitMerges( getEnum(rc, RECEIVE, null, KEY_REJECT_IMPLICIT_MERGES, InheritableBoolean.INHERIT)); + p.setEnableReviewerByEmail( + getEnum(rc, REVIEWER, null, KEY_ENABLE_REVIEWER_BY_EMAIL, InheritableBoolean.INHERIT)); p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, DEFAULT_SUBMIT_ACTION)); p.setUseContentMerge(getEnum(rc, SUBMIT, null, KEY_MERGE_CONTENT, InheritableBoolean.INHERIT)); + p.setMatchAuthorToCommitterDate( + getEnum( + rc, + SUBMIT, + null, + KEY_MATCH_AUTHOR_DATE_WITH_COMMITTER_DATE, + InheritableBoolean.INHERIT)); p.setState(getEnum(rc, PROJECT, null, KEY_STATE, DEFAULT_STATE_VALUE)); p.setDefaultDashboard(rc.getString(DASHBOARD, null, KEY_DEFAULT)); p.setLocalDefaultDashboard(rc.getString(DASHBOARD, null, KEY_LOCAL_DEFAULT)); - loadAccountsSection(rc, groupsByName); - loadContributorAgreements(rc, groupsByName); - loadAccessSections(rc, groupsByName); + loadAccountsSection(rc); + loadContributorAgreements(rc); + loadAccessSections(rc); loadBranchOrderSection(rc); - loadNotifySections(rc, groupsByName); + loadNotifySections(rc); loadLabelSections(rc); loadCommentLinkSections(rc); loadSubscribeSections(rc); mimeTypes = new ConfiguredMimeTypes(projectName.get(), rc); loadPluginSections(rc); loadReceiveSection(rc); + loadExtensionPanelSections(rc); } - private void loadAccountsSection(Config rc, Map<String, GroupReference> groupsByName) { + private void loadAccountsSection(Config rc) { accountsSection = new AccountsSection(); accountsSection.setSameGroupVisibility( loadPermissionRules(rc, ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY, groupsByName, false)); } - private void loadContributorAgreements(Config rc, Map<String, GroupReference> groupsByName) { + private void loadExtensionPanelSections(Config rc) { + Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2); + extensionPanelSections = Maps.newLinkedHashMap(); + for (String name : rc.getSubsections(EXTENSION_PANELS)) { + String lower = name.toLowerCase(); + if (lowerNames.containsKey(lower)) { + error( + new ValidationError( + PROJECT_CONFIG, + String.format( + "Extension Panels \"%s\" conflicts with \"%s\"", name, lowerNames.get(lower)))); + } + lowerNames.put(lower, name); + extensionPanelSections.put( + name, + new ArrayList<>(Arrays.asList(rc.getStringList(EXTENSION_PANELS, name, KEY_PANEL)))); + } + } + + private void loadContributorAgreements(Config rc) { contributorAgreements = new HashMap<>(); for (String name : rc.getSubsections(CONTRIBUTOR_AGREEMENT)) { ContributorAgreement ca = getContributorAgreement(name, true); @@ -594,7 +651,7 @@ * type = submitted_changes * </pre> */ - private void loadNotifySections(Config rc, Map<String, GroupReference> groupsByName) { + private void loadNotifySections(Config rc) { notifySections = new HashMap<>(); for (String sectionName : rc.getSubsections(NOTIFY)) { NotifyConfig n = new NotifyConfig(); @@ -607,8 +664,8 @@ n.setHeader(rc.getEnum(NOTIFY, sectionName, KEY_HEADER, NotifyConfig.Header.BCC)); for (String dst : rc.getStringList(NOTIFY, sectionName, KEY_EMAIL)) { - if (dst.startsWith("group ")) { - String groupName = dst.substring(6).trim(); + String groupName = GroupReference.extractGroupName(dst); + if (groupName != null) { GroupReference ref = groupsByName.get(groupName); if (ref == null) { ref = new GroupReference(null, groupName); @@ -639,7 +696,7 @@ } } - private void loadAccessSections(Config rc, Map<String, GroupReference> groupsByName) { + private void loadAccessSections(Config rc) { accessSections = new HashMap<>(); sectionsWithUnknownPermissions = new HashSet<>(); for (String refName : rc.getSubsections(ACCESS)) { @@ -940,17 +997,15 @@ pluginConfigs.put(plugin, pluginConfig); for (String name : rc.getNames(PLUGIN, plugin)) { String value = rc.getString(PLUGIN, plugin, name); - if (value.startsWith("Group[")) { - GroupReference refFromString = GroupReference.fromString(value); - GroupReference ref = groupList.byUUID(refFromString.getUUID()); + String groupName = GroupReference.extractGroupName(value); + if (groupName != null) { + GroupReference ref = groupsByName.get(groupName); if (ref == null) { - ref = refFromString; error( new ValidationError( - PROJECT_CONFIG, - "group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME)); + PROJECT_CONFIG, "group \"" + groupName + "\" not in " + GroupList.FILE_NAME)); } - rc.setString(PLUGIN, plugin, name, ref.toString()); + rc.setString(PLUGIN, plugin, name, value); } pluginConfig.setStringList( PLUGIN, plugin, name, Arrays.asList(rc.getStringList(PLUGIN, plugin, name))); @@ -996,7 +1051,6 @@ rc.unset(PROJECT, null, KEY_DESCRIPTION); } set(rc, ACCESS, null, KEY_INHERIT_FROM, p.getParentName()); - set( rc, RECEIVE, @@ -1052,9 +1106,23 @@ KEY_REJECT_IMPLICIT_MERGES, p.getRejectImplicitMerges(), InheritableBoolean.INHERIT); + set( + rc, + REVIEWER, + null, + KEY_ENABLE_REVIEWER_BY_EMAIL, + p.getEnableReviewerByEmail(), + InheritableBoolean.INHERIT); set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), DEFAULT_SUBMIT_ACTION); set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), InheritableBoolean.INHERIT); + set( + rc, + SUBMIT, + null, + KEY_MATCH_AUTHOR_DATE_WITH_COMMITTER_DATE, + p.getMatchAuthorToCommitterDate(), + InheritableBoolean.INHERIT); set(rc, PROJECT, null, KEY_STATE, p.getState(), DEFAULT_STATE_VALUE); @@ -1076,7 +1144,7 @@ return true; } - public static final String validMaxObjectSizeLimit(String value) throws ConfigInvalidException { + public static String validMaxObjectSizeLimit(String value) throws ConfigInvalidException { if (value == null) { return null; } @@ -1288,40 +1356,55 @@ setBooleanConfigKey( rc, + LABEL, name, KEY_ALLOW_POST_SUBMIT, label.allowPostSubmit(), LabelType.DEF_ALLOW_POST_SUBMIT); setBooleanConfigKey( - rc, name, KEY_COPY_MIN_SCORE, label.isCopyMinScore(), LabelType.DEF_COPY_MIN_SCORE); - setBooleanConfigKey( - rc, name, KEY_COPY_MAX_SCORE, label.isCopyMaxScore(), LabelType.DEF_COPY_MAX_SCORE); + rc, + LABEL, + name, + KEY_COPY_MIN_SCORE, + label.isCopyMinScore(), + LabelType.DEF_COPY_MIN_SCORE); setBooleanConfigKey( rc, + LABEL, + name, + KEY_COPY_MAX_SCORE, + label.isCopyMaxScore(), + LabelType.DEF_COPY_MAX_SCORE); + setBooleanConfigKey( + rc, + LABEL, name, KEY_COPY_ALL_SCORES_ON_TRIVIAL_REBASE, label.isCopyAllScoresOnTrivialRebase(), LabelType.DEF_COPY_ALL_SCORES_ON_TRIVIAL_REBASE); setBooleanConfigKey( rc, + LABEL, name, KEY_COPY_ALL_SCORES_IF_NO_CODE_CHANGE, label.isCopyAllScoresIfNoCodeChange(), LabelType.DEF_COPY_ALL_SCORES_IF_NO_CODE_CHANGE); setBooleanConfigKey( rc, + LABEL, name, KEY_COPY_ALL_SCORES_IF_NO_CHANGE, label.isCopyAllScoresIfNoChange(), LabelType.DEF_COPY_ALL_SCORES_IF_NO_CHANGE); setBooleanConfigKey( rc, + LABEL, name, KEY_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE, label.isCopyAllScoresOnMergeFirstParentUpdate(), LabelType.DEF_COPY_ALL_SCORES_ON_MERGE_FIRST_PARENT_UPDATE); setBooleanConfigKey( - rc, name, KEY_CAN_OVERRIDE, label.canOverride(), LabelType.DEF_CAN_OVERRIDE); + rc, LABEL, name, KEY_CAN_OVERRIDE, label.canOverride(), LabelType.DEF_CAN_OVERRIDE); List<String> values = Lists.newArrayListWithCapacity(label.getValues().size()); for (LabelValue value : label.getValues()) { values.add(value.format()); @@ -1335,11 +1418,11 @@ } private static void setBooleanConfigKey( - Config rc, String name, String key, boolean value, boolean defaultValue) { + Config rc, String section, String name, String key, boolean value, boolean defaultValue) { if (value == defaultValue) { - rc.unset(LABEL, name, key); + rc.unset(section, name, key); } else { - rc.setBoolean(LABEL, name, key, value); + rc.setBoolean(section, name, key, value); } } @@ -1354,11 +1437,12 @@ Config pluginConfig = e.getValue(); for (String name : pluginConfig.getNames(PLUGIN, plugin)) { String value = pluginConfig.getString(PLUGIN, plugin, name); - if (value.startsWith("Group[")) { - GroupReference ref = resolve(GroupReference.fromString(value)); - if (ref.getUUID() != null) { + String groupName = GroupReference.extractGroupName(value); + if (groupName != null) { + GroupReference ref = groupsByName.get(groupName); + if (ref != null && ref.getUUID() != null) { keepGroups.add(ref.getUUID()); - pluginConfig.setString(PLUGIN, plugin, name, ref.toString()); + pluginConfig.setString(PLUGIN, plugin, name, "group " + ref.getName()); } } rc.setStringList(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java index 28425e0..89bbf0f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java
@@ -14,11 +14,13 @@ package com.google.gerrit.server.git; +import java.util.concurrent.ScheduledThreadPoolExecutor; + public interface QueueProvider { enum QueueType { INTERACTIVE, BATCH } - WorkQueue.Executor getQueue(QueueType type); + ScheduledThreadPoolExecutor getQueue(QueueType type); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java index 94e78f8..d596987 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
@@ -42,23 +42,26 @@ private final RevCommit initialTip; private final Set<RevCommit> alreadyAccepted; private final InternalChangeQuery internalChangeQuery; + private final Set<CodeReviewCommit> incoming; public RebaseSorter( CodeReviewRevWalk rw, RevCommit initialTip, Set<RevCommit> alreadyAccepted, RevFlag canMergeFlag, - InternalChangeQuery internalChangeQuery) { + InternalChangeQuery internalChangeQuery, + Set<CodeReviewCommit> incoming) { this.rw = rw; this.canMergeFlag = canMergeFlag; this.initialTip = initialTip; this.alreadyAccepted = alreadyAccepted; this.internalChangeQuery = internalChangeQuery; + this.incoming = incoming; } - public List<CodeReviewCommit> sort(Collection<CodeReviewCommit> incoming) throws IOException { + public List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort) throws IOException { final List<CodeReviewCommit> sorted = new ArrayList<>(); - final Set<CodeReviewCommit> sort = new HashSet<>(incoming); + final Set<CodeReviewCommit> sort = new HashSet<>(toSort); while (!sort.isEmpty()) { final CodeReviewCommit n = removeOne(sort); @@ -128,7 +131,7 @@ } } - private static <T> T removeOne(final Collection<T> c) { + private static <T> T removeOne(Collection<T> c) { final Iterator<T> i = c.iterator(); final T r = i.next(); i.remove();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java index 399dfc7..6b3f173 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.git; +import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.gerrit.common.FooterConstants.CHANGE_ID; @@ -59,6 +60,7 @@ import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType; +import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.registration.DynamicMap.Entry; import com.google.gerrit.extensions.registration.DynamicSet; @@ -81,8 +83,8 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.Sequences; -import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountResolver; +import com.google.gerrit.server.account.AccountsUpdate; import com.google.gerrit.server.change.ChangeInserter; import com.google.gerrit.server.change.SetHashtagsOp; import com.google.gerrit.server.config.AllProjectsName; @@ -92,7 +94,6 @@ import com.google.gerrit.server.edit.ChangeEdit; import com.google.gerrit.server.edit.ChangeEditUtil; import com.google.gerrit.server.events.CommitReceivedEvent; -import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.git.MultiProgressMonitor.Task; import com.google.gerrit.server.git.validators.CommitValidationException; import com.google.gerrit.server.git.validators.CommitValidationMessage; @@ -105,7 +106,12 @@ import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.patch.PatchSetInfoFactory; -import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.GlobalPermission; +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.NoSuchChangeException; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectControl; @@ -117,6 +123,9 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.Context; +import com.google.gerrit.server.update.RepoContext; +import com.google.gerrit.server.update.RepoOnlyOp; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.LabelVote; import com.google.gerrit.server.util.MagicBranch; @@ -145,9 +154,9 @@ import java.util.concurrent.Future; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; -import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; @@ -179,6 +188,7 @@ /** Receives change upload using the Git receive-pack protocol. */ public class ReceiveCommits { private static final Logger log = LoggerFactory.getLogger(ReceiveCommits.class); + private static final String BYPASS_REVIEW = "bypass-review"; public static final Pattern NEW_PATCHSET = Pattern.compile("^" + REFS_CHANGES + "(?:[0-9][0-9]/)?([1-9][0-9]*)(?:/new)?$"); @@ -291,9 +301,11 @@ private final Sequences seq; private final Provider<InternalChangeQuery> queryProvider; private final ChangeNotes.Factory notesFactory; + private final AccountsUpdate.Server accountsUpdate; private final AccountResolver accountResolver; + private final PermissionBackend permissionBackend; + private final PermissionBackend.ForProject permissions; private final CmdLineParser.Factory optionParserFactory; - private final GitReferenceUpdated gitRefUpdated; private final PatchSetInfoFactory patchSetInfoFactory; private final PatchSetUtil psUtil; private final ProjectCache projectCache; @@ -301,7 +313,6 @@ private final CommitValidators.Factory commitValidatorsFactory; private final RefOperationValidators.Factory refValidatorsFactory; private final TagCache tagCache; - private final AccountCache accountCache; private final ChangeInserter.Factory changeInserterFactory; private final RequestScopePropagator requestScopePropagator; private final SshInfo sshInfo; @@ -341,6 +352,15 @@ private final ChangeEditUtil editUtil; private final ChangeIndexer indexer; + /** + * Actual commands to be executed, as opposed to the mix of actual and magic commands that were + * provided over the wire. + * + * <p>Excludes commands executed implicitly as part of other {@link BatchUpdateOp}s, such as + * creating patch set refs. + */ + private final List<ReceiveCommand> actualCommands = new ArrayList<>(); + private final List<ValidationMessage> messages = new ArrayList<>(); private ListMultimap<Error, String> errors = LinkedListMultimap.create(); private Task newProgress; @@ -348,7 +368,6 @@ private Task closeProgress; private Task commandProgress; private MessageSender messageSender; - private BatchRefUpdate batch; @Inject ReceiveCommits( @@ -356,15 +375,15 @@ Sequences seq, Provider<InternalChangeQuery> queryProvider, ChangeNotes.Factory notesFactory, + AccountsUpdate.Server accountsUpdate, AccountResolver accountResolver, + PermissionBackend permissionBackend, CmdLineParser.Factory optionParserFactory, - GitReferenceUpdated gitRefUpdated, PatchSetInfoFactory patchSetInfoFactory, PatchSetUtil psUtil, ProjectCache projectCache, TagCache tagCache, - AccountCache accountCache, - @Nullable SearchingChangeCacheImpl changeCache, + VisibleRefFilter.Factory refFilterFactory, ChangeInserter.Factory changeInserterFactory, CommitValidators.Factory commitValidatorsFactory, RefOperationValidators.Factory refValidatorsFactory, @@ -389,21 +408,21 @@ SetHashtagsOp.Factory hashtagsFactory, ReplaceOp.Factory replaceOpFactory, MergedByPushOp.Factory mergedByPushOpFactory) - throws IOException { + throws IOException, PermissionBackendException { this.user = projectControl.getUser().asIdentifiedUser(); this.db = db; this.seq = seq; this.queryProvider = queryProvider; this.notesFactory = notesFactory; + this.accountsUpdate = accountsUpdate; this.accountResolver = accountResolver; + this.permissionBackend = permissionBackend; this.optionParserFactory = optionParserFactory; - this.gitRefUpdated = gitRefUpdated; this.patchSetInfoFactory = patchSetInfoFactory; this.psUtil = psUtil; this.projectCache = projectCache; this.canonicalWebUrl = canonicalWebUrl; this.tagCache = tagCache; - this.accountCache = accountCache; this.changeInserterFactory = changeInserterFactory; this.commitValidatorsFactory = commitValidatorsFactory; this.refValidatorsFactory = refValidatorsFactory; @@ -463,11 +482,17 @@ } }); - if (!projectControl.allRefsAreVisible()) { + permissions = permissionBackend.user(user).project(project.getNameKey()); + // If the user lacks READ permission, some references may be filtered and hidden from view. + // Check objects mentioned inside the incoming pack file are reachable from visible refs. + try { + permissionBackend.user(user).project(project.getNameKey()).check(ProjectPermission.READ); + } catch (AuthException e) { rp.setCheckReferencedObjectsAreReachable(receiveConfig.checkReferencedObjectsAreReachable); } + rp.setAdvertiseRefsHook( - new VisibleRefFilter(tagCache, notesFactory, changeCache, repo, projectControl, db, false)); + refFilterFactory.create(projectControl.getProjectState(), repo).setShowMetadata(false)); List<AdvertiseRefsHook> advHooks = new ArrayList<>(3); advHooks.add( new AdvertiseRefsHook() { @@ -574,37 +599,20 @@ closeProgress = progress.beginSubTask("closed", UNKNOWN); commandProgress = progress.beginSubTask("refs", UNKNOWN); - batch = repo.getRefDatabase().newBatchUpdate(); - batch.setPushCertificate(rp.getPushCertificate()); - batch.setRefLogIdent(rp.getRefLogIdent()); - batch.setRefLogMessage("push", true); - - parseCommands(commands); + try { + parseCommands(commands); + } catch (PermissionBackendException err) { + for (ReceiveCommand cmd : actualCommands) { + if (cmd.getResult() == NOT_ATTEMPTED) { + cmd.setResult(REJECTED_OTHER_REASON, "internal server error"); + } + } + logError(String.format("Failed to process refs in %s", project.getName()), err); + } if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) { selectNewAndReplacedChangesFromMagicBranch(); } preparePatchSetsForReplace(); - - logDebug("Executing batch with {} commands", batch.getCommands().size()); - if (!batch.getCommands().isEmpty()) { - try { - if (!batch.isAllowNonFastForwards() && magicBranch != null && magicBranch.edit) { - logDebug("Allowing non-fast-forward for edit ref"); - batch.setAllowNonFastForwards(true); - } - batch.execute(rp.getRevWalk(), commandProgress); - } catch (IOException err) { - int cnt = 0; - for (ReceiveCommand cmd : batch.getCommands()) { - if (cmd.getResult() == NOT_ATTEMPTED) { - cmd.setResult(REJECTED_OTHER_REASON, "internal server error"); - cnt++; - } - } - logError(String.format("Failed to store %d refs in %s", cnt, project.getName()), err); - } - } - insertChangesAndPatchSets(); newProgress.end(); replaceProgress.end(); @@ -619,44 +627,24 @@ } Set<Branch.NameKey> branches = new HashSet<>(); - for (ReceiveCommand c : batch.getCommands()) { - if (c.getResult() == OK) { - String refName = c.getRefName(); - if (c.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward - logDebug("Updating tag cache on fast-forward of {}", c.getRefName()); - tagCache.updateFastForward(project.getNameKey(), refName, c.getOldId(), c.getNewId()); - } + for (ReceiveCommand c : actualCommands) { + // Most post-update steps should happen in UpdateOneRefOp#postUpdate. The only steps that + // should happen in this loop are things that can't happen within one BatchUpdate because they + // involve kicking off an additional BatchUpdate. + if (c.getResult() != OK) { + continue; + } + if (isHead(c) || isConfig(c)) { + switch (c.getType()) { + case CREATE: + case UPDATE: + case UPDATE_NONFASTFORWARD: + autoCloseChanges(c); + branches.add(new Branch.NameKey(project.getNameKey(), c.getRefName())); + break; - if (isHead(c) || isConfig(c)) { - switch (c.getType()) { - case CREATE: - case UPDATE: - case UPDATE_NONFASTFORWARD: - autoCloseChanges(c); - branches.add(new Branch.NameKey(project.getNameKey(), refName)); - break; - - case DELETE: - break; - } - } - - if (isConfig(c)) { - logDebug("Reloading project in cache"); - projectCache.evict(project); - ProjectState ps = projectCache.get(project.getNameKey()); - try { - repo.setGitwebDescription(ps.getProject().getDescription()); - } catch (IOException e) { - log.warn("cannot update description of " + project.getName(), e); - } - } - - if (!MagicBranch.isMagicBranch(refName)) { - logDebug("Firing ref update for {}", c.getRefName()); - gitRefUpdated.fire(project.getNameKey(), c, user.getAccount()); - } else { - logDebug("Assuming ref update event for {} has fired", c.getRefName()); + case DELETE: + break; } } } @@ -751,115 +739,42 @@ } private void insertChangesAndPatchSets() { - int replaceCount = 0; - int okToInsert = 0; - - for (Map.Entry<Change.Id, ReplaceRequest> e : replaceByChange.entrySet()) { - ReplaceRequest replace = e.getValue(); - if (magicBranch != null && replace.inputCommand == magicBranch.cmd) { - replaceCount++; - - if (replace.cmd != null && replace.cmd.getResult() == OK) { - okToInsert++; - } - } else if (replace.cmd != null && replace.cmd.getResult() == OK) { - String refName = replace.inputCommand.getRefName(); - checkState( - NEW_PATCHSET.matcher(refName).matches(), - "expected a new patch set command as input when creating %s; got %s", - replace.cmd.getRefName(), - refName); - try { - logDebug("One-off insertion of patch set for {}", refName); - replace.insertPatchSetWithoutBatchUpdate(); - replace.inputCommand.setResult(OK); - } catch (IOException | UpdateException | RestApiException err) { - reject(replace.inputCommand, "internal server error"); - logError( - String.format( - "Cannot add patch set to change %d in project %s", - e.getKey().get(), project.getName()), - err); - } - } else if (replace.inputCommand.getResult() == NOT_ATTEMPTED) { - reject(replace.inputCommand, "internal server error"); - logError(String.format("Replacement for project %s was not attempted", project.getName())); - } - } - - // refs/for/ or refs/drafts/ not used, or it already failed earlier. - // No need to continue. - if (magicBranch == null) { - logDebug("No magic branch, nothing more to do"); - return; - } else if (magicBranch.cmd.getResult() != NOT_ATTEMPTED) { + ReceiveCommand magicBranchCmd = magicBranch != null ? magicBranch.cmd : null; + if (magicBranchCmd != null && magicBranchCmd.getResult() != NOT_ATTEMPTED) { logWarn( String.format( "Skipping change updates on %s because ref update failed: %s %s", project.getName(), - magicBranch.cmd.getResult(), - Strings.nullToEmpty(magicBranch.cmd.getMessage()))); - return; - } - - List<String> lastCreateChangeErrors = new ArrayList<>(); - for (CreateRequest create : newChanges) { - if (create.cmd.getResult() == OK) { - okToInsert++; - } else { - String createChangeResult = - String.format( - "%s %s", create.cmd.getResult(), Strings.nullToEmpty(create.cmd.getMessage())) - .trim(); - lastCreateChangeErrors.add(createChangeResult); - logError( - String.format( - "Command %s on %s:%s not completed: %s", - create.cmd.getType(), - project.getName(), - create.cmd.getRefName(), - createChangeResult)); - } - } - - logDebug( - "Counted {} ok to insert, out of {} to replace and {} new", - okToInsert, - replaceCount, - newChanges.size()); - - if (okToInsert != replaceCount + newChanges.size()) { - // One or more new references failed to create. Assume the - // system isn't working correctly anymore and abort. - reject( - magicBranch.cmd, - "Unable to create changes: " + lastCreateChangeErrors.stream().collect(joining(" "))); - logError( - String.format( - "Only %d of %d new change refs created in %s; aborting", - okToInsert, replaceCount + newChanges.size(), project.getName())); + magicBranchCmd.getResult(), + Strings.nullToEmpty(magicBranchCmd.getMessage()))); return; } try (BatchUpdate bu = batchUpdateFactory.create( - db, magicBranch.dest.getParentKey(), user.materializedCopy(), TimeUtil.nowTs()); - ObjectInserter ins = repo.newObjectInserter()) { - bu.setRepository(repo, rp.getRevWalk(), ins).updateChangesInParallel(); + db, project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs()); + ObjectInserter ins = repo.newObjectInserter(); + ObjectReader reader = ins.newReader(); + RevWalk rw = new RevWalk(reader)) { + bu.setRepository(repo, rw, ins).updateChangesInParallel(); bu.setRequestId(receiveId); + bu.setRefLogMessage("push"); + + logDebug("Adding {} replace requests", newChanges.size()); for (ReplaceRequest replace : replaceByChange.values()) { - if (replace.inputCommand == magicBranch.cmd) { - replace.addOps(bu, replaceProgress); - } + replace.addOps(bu, replaceProgress); } + logDebug("Adding {} create requests", newChanges.size()); for (CreateRequest create : newChanges) { create.addOps(bu); } - for (UpdateGroupsRequest update : updateGroups) { - update.addOps(bu); - } + logDebug("Adding {} group update requests", newChanges.size()); + updateGroups.forEach(r -> r.addOps(bu)); + + logDebug("Adding {} additional ref updates", actualCommands.size()); + actualCommands.forEach(c -> bu.addRepoOnlyOp(new UpdateOneRefOp(c))); logDebug("Executing batch"); try { @@ -867,10 +782,17 @@ } catch (UpdateException e) { throw INSERT_EXCEPTION.apply(e); } - magicBranch.cmd.setResult(OK); + if (magicBranchCmd != null) { + magicBranchCmd.setResult(OK); + } for (ReplaceRequest replace : replaceByChange.values()) { String rejectMessage = replace.getRejectMessage(); - if (rejectMessage != null) { + if (rejectMessage == null) { + if (replace.inputCommand.getResult() == NOT_ATTEMPTED) { + // Not necessarily the magic branch, so need to set OK on the original value. + replace.inputCommand.setResult(OK); + } + } else { logDebug("Rejecting due to message from ReplaceOp"); reject(replace.inputCommand, rejectMessage); } @@ -878,10 +800,10 @@ } catch (ResourceConflictException e) { addMessage(e.getMessage()); - reject(magicBranch.cmd, "conflict"); + reject(magicBranchCmd, "conflict"); } catch (RestApiException | IOException err) { logError("Can't insert change/patch set for " + project.getName(), err); - reject(magicBranch.cmd, "internal server error: " + err.getMessage()); + reject(magicBranchCmd, "internal server error: " + err.getMessage()); } if (magicBranch != null && magicBranch.submit) { @@ -889,10 +811,14 @@ submit(newChanges, replaceByChange.values()); } catch (ResourceConflictException e) { addMessage(e.getMessage()); - reject(magicBranch.cmd, "conflict"); - } catch (RestApiException | OrmException e) { + reject(magicBranchCmd, "conflict"); + } catch (RestApiException + | OrmException + | UpdateException + | IOException + | ConfigInvalidException e) { logError("Error submitting changes to " + project.getName(), e); - reject(magicBranch.cmd, "error during submit"); + reject(magicBranchCmd, "error during submit"); } } } @@ -921,7 +847,8 @@ return displayName; } - private void parseCommands(Collection<ReceiveCommand> commands) { + private void parseCommands(Collection<ReceiveCommand> commands) + throws PermissionBackendException { List<String> optionList = rp.getPushOptions(); if (optionList != null) { for (String option : optionList) { @@ -1040,10 +967,13 @@ continue; } } else { - if (!oldParent.equals(newParent) - && !user.getCapabilities().canAdministrateServer()) { - reject(cmd, "invalid project configuration: only Gerrit admin can set parent"); - continue; + if (!oldParent.equals(newParent)) { + try { + permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER); + } catch (AuthException e) { + reject(cmd, "invalid project configuration: only Gerrit admin can set parent"); + continue; + } } if (projectCache.get(newParent) == null) { @@ -1123,7 +1053,7 @@ } } - private void parseCreate(ReceiveCommand cmd) { + private void parseCreate(ReceiveCommand cmd) throws PermissionBackendException { RevObject obj; try { obj = rp.getRevWalk().parseAny(cmd.getNewId()); @@ -1141,35 +1071,44 @@ } RefControl ctl = projectControl.controlForRef(cmd.getRefName()); - if (ctl.canCreate(db, rp.getRepository(), obj)) { - if (!validRefOperation(cmd)) { - return; - } - validateNewCommits(ctl, cmd); - batch.addCommand(cmd); - } else { - reject(cmd, "prohibited by Gerrit: create access denied for " + cmd.getRefName()); + String rejectReason = ctl.canCreate(rp.getRepository(), obj); + if (rejectReason != null) { + reject(cmd, "prohibited by Gerrit: " + rejectReason); + return; } + + if (!validRefOperation(cmd)) { + // validRefOperation sets messages, so no need to provide more feedback. + return; + } + + validateNewCommits(ctl, cmd); + actualCommands.add(cmd); } - private void parseUpdate(ReceiveCommand cmd) { + private void parseUpdate(ReceiveCommand cmd) throws PermissionBackendException { logDebug("Updating {}", cmd); - RefControl ctl = projectControl.controlForRef(cmd.getRefName()); - if (ctl.canUpdate()) { + boolean ok; + try { + permissions.ref(cmd.getRefName()).check(RefPermission.UPDATE); + ok = true; + } catch (AuthException err) { + ok = false; + } + if (ok) { if (isHead(cmd) && !isCommit(cmd)) { return; } - if (!validRefOperation(cmd)) { return; } - validateNewCommits(ctl, cmd); - batch.addCommand(cmd); + validateNewCommits(projectControl.controlForRef(cmd.getRefName()), cmd); + actualCommands.add(cmd); } else { - if (RefNames.REFS_CONFIG.equals(ctl.getRefName())) { + if (RefNames.REFS_CONFIG.equals(cmd.getRefName())) { errors.put(Error.CONFIG_UPDATE, RefNames.REFS_CONFIG); } else { - errors.put(Error.UPDATE, ctl.getRefName()); + errors.put(Error.UPDATE, cmd.getRefName()); } reject(cmd, "prohibited by Gerrit: ref update access denied"); } @@ -1192,28 +1131,34 @@ return false; } - private void parseDelete(ReceiveCommand cmd) { + private void parseDelete(ReceiveCommand cmd) throws PermissionBackendException { logDebug("Deleting {}", cmd); - RefControl ctl = projectControl.controlForRef(cmd.getRefName()); - if (ctl.getRefName().startsWith(REFS_CHANGES)) { - errors.put(Error.DELETE_CHANGES, ctl.getRefName()); + if (cmd.getRefName().startsWith(REFS_CHANGES)) { + errors.put(Error.DELETE_CHANGES, cmd.getRefName()); reject(cmd, "cannot delete changes"); - } else if (ctl.canDelete()) { + } else if (canDelete(cmd)) { if (!validRefOperation(cmd)) { return; } - batch.addCommand(cmd); + actualCommands.add(cmd); + } else if (RefNames.REFS_CONFIG.equals(cmd.getRefName())) { + reject(cmd, "cannot delete project configuration"); } else { - if (RefNames.REFS_CONFIG.equals(ctl.getRefName())) { - reject(cmd, "cannot delete project configuration"); - } else { - errors.put(Error.DELETE, ctl.getRefName()); - reject(cmd, "cannot delete references"); - } + errors.put(Error.DELETE, cmd.getRefName()); + reject(cmd, "cannot delete references"); } } - private void parseRewind(ReceiveCommand cmd) { + private boolean canDelete(ReceiveCommand cmd) throws PermissionBackendException { + try { + permissions.ref(cmd.getRefName()).check(RefPermission.DELETE); + return true; + } catch (AuthException e) { + return false; + } + } + + private void parseRewind(ReceiveCommand cmd) throws PermissionBackendException { RevCommit newObject; try { newObject = rp.getRevWalk().parseCommit(cmd.getNewId()); @@ -1236,11 +1181,18 @@ } } - if (ctl.canForceUpdate()) { + boolean ok; + try { + permissions.ref(cmd.getRefName()).check(RefPermission.FORCE_UPDATE); + ok = true; + } catch (AuthException err) { + ok = false; + } + if (ok) { if (!validRefOperation(cmd)) { return; } - batch.setAllowNonFastForwards(true).addCommand(cmd); + actualCommands.add(cmd); } else { cmd.setResult( REJECTED_NONFASTFORWARD, " need '" + PermissionRule.FORCE_PUSH + "' privilege."); @@ -1251,17 +1203,19 @@ private static final Splitter COMMAS = Splitter.on(',').omitEmptyStrings(); final ReceiveCommand cmd; + final LabelTypes labelTypes; + final NotesMigration notesMigration; + private final boolean defaultPublishComments; Branch.NameKey dest; RefControl ctl; + PermissionBackend.ForRef perm; Set<Account.Id> reviewer = Sets.newLinkedHashSet(); Set<Account.Id> cc = Sets.newLinkedHashSet(); Map<String, Short> labels = new HashMap<>(); String message; List<RevCommit> baseCommit; - LabelTypes labelTypes; CmdLineParser clp; Set<String> hashtags = new HashSet<>(); - NotesMigration notesMigration; @Option(name = "--base", metaVar = "BASE", usage = "merge base of changes") List<ObjectId> base; @@ -1272,6 +1226,22 @@ @Option(name = "--draft", usage = "mark new/updated changes as draft") boolean draft; + @Option(name = "--private", usage = "mark new/updated change as private") + boolean isPrivate; + + @Option(name = "--remove-private", usage = "remove privacy flag from updated change") + boolean removePrivate; + + @Option( + name = "--wip", + aliases = {"-work-in-progress"}, + usage = "mark change as work in progress" + ) + boolean workInProgress; + + @Option(name = "--ready", usage = "mark change as ready") + boolean ready; + @Option( name = "--edit", aliases = {"-e"}, @@ -1285,6 +1255,16 @@ @Option(name = "--merged", usage = "create single change for a merged commit") boolean merged; + @Option(name = "--publish-comments", usage = "publish all draft comments on updated changes") + private boolean publishComments; + + @Option( + name = "--no-publish-comments", + aliases = {"--np"}, + usage = "do not publish draft comments" + ) + private boolean noPublishComments; + @Option( name = "--notify", usage = @@ -1292,7 +1272,7 @@ + "should be sent. Allowed values are NONE, OWNER, " + "OWNER_REVIEWERS, ALL. If not set, the default is ALL." ) - NotifyHandling notify = NotifyHandling.ALL; + private NotifyHandling notify; @Option(name = "--notify-to", metaVar = "USER", usage = "user that should be notified") List<Account.Id> tos = new ArrayList<>(); @@ -1346,7 +1326,7 @@ metaVar = "MESSAGE", usage = "Comment message to apply to the review" ) - void addMessage(final String token) { + void addMessage(String token) { // git push does not allow spaces in refs. message = token.replace("_", " "); } @@ -1365,14 +1345,24 @@ if (!hashtag.isEmpty()) { hashtags.add(hashtag); } - //TODO(dpursehouse): validate hashtags + // TODO(dpursehouse): validate hashtags } - MagicBranchInput(ReceiveCommand cmd, LabelTypes labelTypes, NotesMigration notesMigration) { + MagicBranchInput( + IdentifiedUser user, + ReceiveCommand cmd, + LabelTypes labelTypes, + NotesMigration notesMigration) { this.cmd = cmd; this.draft = cmd.getRefName().startsWith(MagicBranch.NEW_DRAFT_CHANGE); this.labelTypes = labelTypes; this.notesMigration = notesMigration; + GeneralPreferencesInfo prefs = user.getAccount().getGeneralPreferencesInfo(); + this.defaultPublishComments = + prefs != null + ? firstNonNull( + user.getAccount().getGeneralPreferencesInfo().publishCommentsOnPush, false) + : false; } MailRecipients getMailRecipients() { @@ -1388,6 +1378,15 @@ return accountsToNotify; } + boolean shouldPublishComments() { + if (publishComments) { + return true; + } else if (noPublishComments) { + return false; + } + return defaultPublishComments; + } + String parse( CmdLineParser clp, Repository repo, @@ -1434,6 +1433,26 @@ } return ref.substring(0, split); } + + public NotifyHandling getNotify() { + if (notify != null) { + return notify; + } + if (workInProgress) { + return NotifyHandling.OWNER; + } + return NotifyHandling.ALL; + } + + public NotifyHandling getNotify(ChangeNotes notes) { + if (notify != null) { + return notify; + } + if (workInProgress || (!ready && notes.getChange().isWorkInProgress())) { + return NotifyHandling.OWNER; + } + return NotifyHandling.ALL; + } } /** @@ -1449,7 +1468,7 @@ return ImmutableListMultimap.copyOf(pushOptions); } - private void parseMagicBranch(ReceiveCommand cmd) { + private void parseMagicBranch(ReceiveCommand cmd) throws PermissionBackendException { // Permit exactly one new change request per push. if (magicBranch != null) { reject(cmd, "duplicate request"); @@ -1457,7 +1476,7 @@ } logDebug("Found magic branch {}", cmd.getRefName()); - magicBranch = new MagicBranchInput(cmd, labelTypes, notesMigration); + magicBranch = new MagicBranchInput(user, cmd, labelTypes, notesMigration); magicBranch.reviewer.addAll(reviewersFromCommandLine); magicBranch.cc.addAll(ccFromCommandLine); @@ -1500,7 +1519,9 @@ magicBranch.dest = new Branch.NameKey(project.getNameKey(), ref); magicBranch.ctl = projectControl.controlForRef(ref); - if (!magicBranch.ctl.canWrite()) { + magicBranch.perm = permissions.ref(ref); + if (projectControl.getProject().getState() + != com.google.gerrit.extensions.client.ProjectState.ACTIVE) { reject(cmd, "project is read only"); return; } @@ -1519,9 +1540,26 @@ } } - if (!magicBranch.ctl.canUpload()) { + try { + magicBranch.perm.check(RefPermission.CREATE_CHANGE); + } catch (AuthException denied) { errors.put(Error.CODE_REVIEW, ref); - reject(cmd, "cannot upload review"); + reject(cmd, denied.getMessage()); + return; + } + + if (magicBranch.isPrivate && magicBranch.removePrivate) { + reject(cmd, "the options 'private' and 'remove-private' are mutually exclusive"); + return; + } + + if (magicBranch.workInProgress && magicBranch.ready) { + reject(cmd, "the options 'wip' and 'ready' are mutually exclusive"); + return; + } + if (magicBranch.publishComments && magicBranch.noPublishComments) { + reject( + cmd, "the options 'publish-comments' and 'no-publish-comments' are mutually exclusive"); return; } @@ -1530,10 +1568,13 @@ return; } - if (magicBranch.submit - && !projectControl.controlForRef(MagicBranch.NEW_CHANGE + ref).canSubmit(true)) { - reject(cmd, "submit not allowed"); - return; + if (magicBranch.submit) { + try { + permissions.ref(ref).check(RefPermission.UPDATE_BY_SUBMIT); + } catch (AuthException e) { + reject(cmd, e.getMessage()); + return; + } } RevWalk walk = rp.getRevWalk(); @@ -1825,7 +1866,7 @@ logDebug("Creating new change for {} even though it is already tracked", name); } - if (!validCommit(rp.getRevWalk(), magicBranch.ctl, magicBranch.cmd, c)) { + if (!validCommit(rp.getRevWalk(), magicBranch.perm, magicBranch.ctl, magicBranch.cmd, c)) { // Not a change the user can propose? Abort as early as possible. newChanges = Collections.emptyList(); logDebug("Aborting early due to invalid commit"); @@ -1971,7 +2012,6 @@ for (int i = 0; i < newChanges.size(); i++) { CreateRequest create = newChanges.get(i); create.setChangeId(newIds.get(i)); - batch.addCommand(create.cmd); create.groups = ImmutableList.copyOf(groups.get(create.commit)); } for (ReplaceRequest replace : replaceByChange.values()) { @@ -2134,8 +2174,10 @@ changeInserterFactory .create(changeId, commit, refName) .setTopic(magicBranch.topic) + .setPrivate(magicBranch.isPrivate) + .setWorkInProgress(magicBranch.workInProgress) // Changes already validated in validateNewCommits. - .setValidatePolicy(CommitValidators.Policy.NONE); + .setValidate(false); if (magicBranch.draft) { ins.setDraft(magicBranch.draft); @@ -2178,13 +2220,13 @@ .setExtraCC(recipients.getCcOnly()) .setApprovals(approvals) .setMessage(msg.toString()) - .setNotify(magicBranch.notify) + .setNotify(magicBranch.getNotify()) .setAccountsToNotify(magicBranch.getAccountsToNotify()) .setRequestScopePropagator(requestScopePropagator) .setSendMail(true) - .setUpdateRef(false) .setPatchSetDescription(magicBranch.message)); if (!magicBranch.hashtags.isEmpty()) { + // Any change owner is allowed to add hashtags when creating a change. bu.addOp( changeId, hashtagsFactory.create(new HashtagsInput(magicBranch.hashtags)).setFireEvent(false)); @@ -2217,7 +2259,7 @@ } private void submit(Collection<CreateRequest> create, Collection<ReplaceRequest> replace) - throws OrmException, RestApiException { + throws OrmException, RestApiException, UpdateException, IOException, ConfigInvalidException { Map<ObjectId, Change> bySha = Maps.newHashMapWithExpectedSize(create.size() + replace.size()); for (CreateRequest r : create) { checkNotNull(r.change, "cannot submit new change %s; op may not have run", r.changeId); @@ -2258,7 +2300,7 @@ req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error"); } } - } catch (IOException err) { + } catch (IOException | PermissionBackendException err) { logError( String.format( "Cannot read repository before replacement for project %s", project.getName()), @@ -2271,15 +2313,6 @@ } logDebug("Read {} changes to replace", replaceByChange.size()); - for (ReplaceRequest req : replaceByChange.values()) { - if (req.inputCommand.getResult() == NOT_ATTEMPTED && req.cmd != null) { - if (req.prev != null) { - batch.addCommand(req.prev); - } - batch.addCommand(req.cmd); - } - } - if (magicBranch != null && magicBranch.cmd.getResult() != NOT_ATTEMPTED) { // Cancel creations tied to refs/for/ or refs/drafts/ command. for (ReplaceRequest req : replaceByChange.values()) { @@ -2308,7 +2341,6 @@ final ReceiveCommand inputCommand; final boolean checkMergedInto; ChangeNotes notes; - ChangeControl changeCtl; BiMap<RevCommit, PatchSet.Id> revisions; PatchSet.Id psId; ReceiveCommand prev; @@ -2323,7 +2355,7 @@ Change.Id toChange, RevCommit newCommit, ReceiveCommand cmd, boolean checkMergedInto) { this.ontoChange = toChange; this.newCommitId = newCommit.copy(); - this.inputCommand = cmd; + this.inputCommand = checkNotNull(cmd); this.checkMergedInto = checkMergedInto; revisions = HashBiMap.create(); @@ -2356,8 +2388,10 @@ * @return whether the new commit is valid * @throws IOException * @throws OrmException + * @throws PermissionBackendException */ - boolean validate(boolean autoClose) throws IOException, OrmException { + boolean validate(boolean autoClose) + throws IOException, OrmException, PermissionBackendException { if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) { return false; } else if (notes == null) { @@ -2365,7 +2399,8 @@ return false; } - priorPatchSet = notes.getChange().currentPatchSetId(); + Change change = notes.getChange(); + priorPatchSet = change.currentPatchSetId(); if (!revisions.containsValue(priorPatchSet)) { reject(inputCommand, "change " + ontoChange + " missing revisions"); return false; @@ -2373,16 +2408,18 @@ RevCommit newCommit = rp.getRevWalk().parseCommit(newCommitId); RevCommit priorCommit = revisions.inverse().get(priorPatchSet); - - changeCtl = projectControl.controlFor(notes); - if (!changeCtl.canAddPatchSet(db)) { + try { + permissions.change(notes).database(db).check(ChangePermission.ADD_PATCH_SET); + } catch (AuthException no) { String locked = "."; - if (changeCtl.isPatchSetLocked(db)) { + if (projectControl.controlFor(notes).isPatchSetLocked(db)) { locked = ". Change is patch set locked."; } reject(inputCommand, "cannot add patch set to " + ontoChange + locked); return false; - } else if (notes.getChange().getStatus().isClosed()) { + } + + if (change.getStatus().isClosed()) { reject(inputCommand, "change " + ontoChange + " closed"); return false; } else if (revisions.containsKey(newCommit)) { @@ -2407,7 +2444,9 @@ } } - if (!validCommit(rp.getRevWalk(), changeCtl.getRefControl(), inputCommand, newCommit)) { + PermissionBackend.ForRef perm = permissions.ref(change.getDest().get()); + RefControl refctl = projectControl.controlForRef(change.getDest()); + if (!validCommit(rp.getRevWalk(), perm, refctl, inputCommand, newCommit)) { return false; } rp.getRevWalk().parseBody(priorCommit); @@ -2458,7 +2497,7 @@ Optional<ChangeEdit> edit = null; try { - edit = editUtil.byChange(changeCtl); + edit = editUtil.byChange(projectControl.controlFor(notes)); } catch (AuthException | IOException e) { logError("Cannot retrieve edit", e); return false; @@ -2468,13 +2507,12 @@ if (edit.get().getBasePatchSet().getId().equals(psId)) { // replace edit cmd = - new ReceiveCommand( - edit.get().getRef().getObjectId(), newCommitId, edit.get().getRefName()); + new ReceiveCommand(edit.get().getEditCommit(), newCommitId, edit.get().getRefName()); } else { // delete old edit ref on rebase prev = new ReceiveCommand( - edit.get().getRef().getObjectId(), ObjectId.zeroId(), edit.get().getRefName()); + edit.get().getEditCommit(), ObjectId.zeroId(), edit.get().getRefName()); createEditCommand(); } } else { @@ -2495,27 +2533,19 @@ private void newPatchSet() throws IOException { RevCommit newCommit = rp.getRevWalk().parseCommit(newCommitId); - psId = ChangeUtil.nextPatchSetId(allRefs, notes.getChange().currentPatchSetId()); + psId = + ChangeUtil.nextPatchSetIdFromAllRefsMap(allRefs, notes.getChange().currentPatchSetId()); info = patchSetInfoFactory.get(rp.getRevWalk(), newCommit, psId); cmd = new ReceiveCommand(ObjectId.zeroId(), newCommitId, psId.toRefName()); } void addOps(BatchUpdate bu, @Nullable Task progress) throws IOException { - if (cmd.getResult() == NOT_ATTEMPTED) { - // TODO(dborowitz): When does this happen? Only when an edit ref is - // involved? - cmd.execute(rp); - } if (magicBranch != null && magicBranch.edit) { - bu.addOp( - notes.getChangeId(), - new BatchUpdateOp() { - @Override - public boolean updateChange(ChangeContext ctx) throws Exception { - // return pseudo dirty state to trigger reindexing - return true; - } - }); + bu.addOp(notes.getChangeId(), new ReindexOnlyOp()); + if (prev != null) { + bu.addRepoOnlyOp(new UpdateOneRefOp(prev)); + } + bu.addRepoOnlyOp(new UpdateOneRefOp(cmd)); return; } RevWalk rw = rp.getRevWalk(); @@ -2538,26 +2568,13 @@ groups, magicBranch, rp.getPushCertificate()) - .setRequestScopePropagator(requestScopePropagator) - .setUpdateRef(false); + .setRequestScopePropagator(requestScopePropagator); bu.addOp(notes.getChangeId(), replaceOp); if (progress != null) { bu.addOp(notes.getChangeId(), new ChangeProgressOp(progress)); } } - void insertPatchSetWithoutBatchUpdate() throws IOException, UpdateException, RestApiException { - try (BatchUpdate bu = - batchUpdateFactory.create( - db, projectControl.getProject().getNameKey(), user, TimeUtil.nowTs()); - ObjectInserter ins = repo.newObjectInserter()) { - bu.setRepository(repo, rp.getRevWalk(), ins); - bu.setRequestId(receiveId); - addOps(bu, replaceProgress); - bu.execute(); - } - } - String getRejectMessage() { return replaceOp != null ? replaceOp.getRejectMessage() : null; } @@ -2599,6 +2616,47 @@ } } + private class UpdateOneRefOp implements RepoOnlyOp { + private final ReceiveCommand cmd; + + private UpdateOneRefOp(ReceiveCommand cmd) { + this.cmd = checkNotNull(cmd); + } + + @Override + public void updateRepo(RepoContext ctx) throws IOException { + ctx.addRefUpdate(cmd); + } + + @Override + public void postUpdate(Context ctx) { + String refName = cmd.getRefName(); + if (cmd.getType() == ReceiveCommand.Type.UPDATE) { // aka fast-forward + logDebug("Updating tag cache on fast-forward of {}", cmd.getRefName()); + tagCache.updateFastForward(project.getNameKey(), refName, cmd.getOldId(), cmd.getNewId()); + } + if (isConfig(cmd)) { + logDebug("Reloading project in cache"); + projectCache.evict(project); + ProjectState ps = projectCache.get(project.getNameKey()); + try { + logDebug("Updating project description"); + repo.setGitwebDescription(ps.getProject().getDescription()); + } catch (IOException e) { + log.warn("cannot update description of " + project.getName(), e); + } + } + } + } + + private static class ReindexOnlyOp implements BatchUpdateOp { + @Override + public boolean updateChange(ChangeContext ctx) { + // Trigger reindexing even though change isn't actually updated. + return true; + } + } + private List<Ref> refs(Change.Id changeId) { return refsByChange().get(changeId); } @@ -2684,17 +2742,22 @@ return true; } - private void validateNewCommits(RefControl ctl, ReceiveCommand cmd) { - if (ctl.canForgeAuthor() - && ctl.canForgeCommitter() - && ctl.canForgeGerritServerIdentity() - && ctl.canUploadMerges() - && !projectControl.getProjectState().isUseSignedOffBy() - && Iterables.isEmpty(rejectCommits) - && !RefNames.REFS_CONFIG.equals(ctl.getRefName()) + private void validateNewCommits(RefControl ctl, ReceiveCommand cmd) + throws PermissionBackendException { + PermissionBackend.ForRef perm = permissions.ref(ctl.getRefName()); + if (!RefNames.REFS_CONFIG.equals(cmd.getRefName()) && !(MagicBranch.isMagicBranch(cmd.getRefName()) - || NEW_PATCHSET.matcher(cmd.getRefName()).matches())) { - logDebug("Short-circuiting new commit validation"); + || NEW_PATCHSET.matcher(cmd.getRefName()).matches()) + && pushOptions.containsKey(BYPASS_REVIEW)) { + try { + perm.check(RefPermission.BYPASS_REVIEW); + if (!Iterables.isEmpty(rejectCommits)) { + throw new AuthException("reject-commits prevents " + BYPASS_REVIEW); + } + logDebug("Short-circuiting new commit validation"); + } catch (AuthException denied) { + reject(cmd, denied.getMessage()); + } return; } @@ -2715,20 +2778,28 @@ i++; if (existing.keySet().contains(c)) { continue; - } else if (!validCommit(walk, ctl, cmd, c)) { + } else if (!validCommit(walk, perm, ctl, cmd, c)) { break; } if (defaultName && user.hasEmailAddress(c.getCommitterIdent().getEmailAddress())) { try { - Account a = db.accounts().get(user.getAccountId()); - if (a != null && Strings.isNullOrEmpty(a.getFullName())) { - a.setFullName(c.getCommitterIdent().getName()); - db.accounts().update(Collections.singleton(a)); - user.getAccount().setFullName(a.getFullName()); - accountCache.evict(a.getId()); + String committerName = c.getCommitterIdent().getName(); + Account account = + accountsUpdate + .create() + .update( + db, + user.getAccountId(), + a -> { + if (Strings.isNullOrEmpty(a.getFullName())) { + a.setFullName(committerName); + } + }); + if (account != null && Strings.isNullOrEmpty(account.getFullName())) { + user.getAccount().setFullName(account.getFullName()); } - } catch (OrmException e) { + } catch (OrmException | IOException | ConfigInvalidException e) { logWarn("Cannot default full_name", e); } finally { defaultName = false; @@ -2742,7 +2813,8 @@ } } - private boolean validCommit(RevWalk rw, RefControl ctl, ReceiveCommand cmd, ObjectId id) + private boolean validCommit( + RevWalk rw, PermissionBackend.ForRef perm, RefControl ctl, ReceiveCommand cmd, ObjectId id) throws IOException { if (validCommits.contains(id)) { @@ -2751,21 +2823,18 @@ RevCommit c = rw.parseCommit(id); rw.parseBody(c); - CommitReceivedEvent receiveEvent = - new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, user); - CommitValidators.Policy policy; - if (magicBranch != null - && cmd.getRefName().equals(magicBranch.cmd.getRefName()) - && magicBranch.merged) { - policy = CommitValidators.Policy.MERGED; - } else { - policy = CommitValidators.Policy.RECEIVE_COMMITS; - } - - try { - messages.addAll( - commitValidatorsFactory.create(policy, ctl, sshInfo, repo).validate(receiveEvent)); + try (CommitReceivedEvent receiveEvent = + new CommitReceivedEvent(cmd, project, ctl.getRefName(), rw.getObjectReader(), c, user)) { + boolean isMerged = + magicBranch != null + && cmd.getRefName().equals(magicBranch.cmd.getRefName()) + && magicBranch.merged; + CommitValidators validators = + isMerged + ? commitValidatorsFactory.forMergedCommits(perm, ctl) + : commitValidatorsFactory.forReceiveCommits(perm, ctl, sshInfo, repo, rw); + messages.addAll(validators.validate(receiveEvent)); } catch (CommitValidationException e) { logDebug("Commit validation failed on {}", c.name()); messages.addAll(e.getMessages()); @@ -2776,21 +2845,22 @@ return true; } - private void autoCloseChanges(final ReceiveCommand cmd) { + private void autoCloseChanges(ReceiveCommand cmd) { logDebug("Starting auto-closing of changes"); String refName = cmd.getRefName(); checkState( !MagicBranch.isMagicBranch(refName), "shouldn't be auto-closing changes on magic branch %s", refName); - RevWalk rw = rp.getRevWalk(); // TODO(dborowitz): Combine this BatchUpdate with the main one in // insertChangesAndPatchSets. try (BatchUpdate bu = batchUpdateFactory.create( db, projectControl.getProject().getNameKey(), user, TimeUtil.nowTs()); - ObjectInserter ins = repo.newObjectInserter()) { - bu.setRepository(repo, rp.getRevWalk(), ins).updateChangesInParallel(); + ObjectInserter ins = repo.newObjectInserter(); + ObjectReader reader = ins.newReader(); + RevWalk rw = new RevWalk(reader)) { + bu.setRepository(repo, rw, ins).updateChangesInParallel(); bu.setRequestId(receiveId); // TODO(dborowitz): Teach BatchUpdate to ignore missing changes. @@ -2840,7 +2910,7 @@ } } - for (final ReplaceRequest req : replaceAndClose) { + for (ReplaceRequest req : replaceAndClose) { Change.Id id = req.notes.getChangeId(); if (!req.validate(true)) { logDebug("Not closing {} because validation failed", id); @@ -2868,7 +2938,7 @@ bu.execute(); } catch (RestApiException e) { logError("Can't insert patchset", e); - } catch (IOException | OrmException | UpdateException e) { + } catch (IOException | OrmException | UpdateException | PermissionBackendException e) { logError("Can't scan for changes to close", e); } } @@ -2882,9 +2952,11 @@ return r; } - private void reject(ReceiveCommand cmd, String why) { - cmd.setResult(REJECTED_OTHER_REASON, why); - commandProgress.update(1); + private void reject(@Nullable ReceiveCommand cmd, String why) { + if (cmd != null) { + cmd.setResult(REJECTED_OTHER_REASON, why); + commandProgress.update(1); + } } private static boolean isHead(ReceiveCommand cmd) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutor.java index ddf24cde..a582564 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutor.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutor.java
@@ -18,8 +18,9 @@ import com.google.inject.BindingAnnotation; import java.lang.annotation.Retention; +import java.util.concurrent.ScheduledThreadPoolExecutor; -/** Marker on the global {@link WorkQueue.Executor} used by {@link ReceiveCommits}. */ +/** Marker on the global {@link ScheduledThreadPoolExecutor} used by {@link ReceiveCommits}. */ @Retention(RUNTIME) @BindingAnnotation public @interface ReceiveCommitsExecutor {}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java index affa44a..76f1369 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
@@ -24,6 +24,7 @@ import com.google.inject.Singleton; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.eclipse.jgit.lib.Config; @@ -36,7 +37,7 @@ @Provides @Singleton @ReceiveCommitsExecutor - public WorkQueue.Executor createReceiveCommitsExecutor( + public ScheduledExecutorService createReceiveCommitsExecutor( @GerritServerConfig Config config, WorkQueue queues) { int poolSize = config.getInt(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java index 063f395..3a82456 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
@@ -17,6 +17,7 @@ import static com.google.gerrit.common.data.GlobalCapability.BATCH_CHANGES_LIMIT; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.config.GerritServerConfig; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -28,19 +29,22 @@ final boolean checkReferencedObjectsAreReachable; final boolean allowDrafts; private final int systemMaxBatchChanges; + private final CapabilityControl.Factory capabilityFactory; @Inject - ReceiveConfig(@GerritServerConfig Config config) { + ReceiveConfig(@GerritServerConfig Config config, CapabilityControl.Factory capabilityFactory) { checkMagicRefs = config.getBoolean("receive", null, "checkMagicRefs", true); checkReferencedObjectsAreReachable = config.getBoolean("receive", null, "checkReferencedObjectsAreReachable", true); allowDrafts = config.getBoolean("change", null, "allowDrafts", true); systemMaxBatchChanges = config.getInt("receive", "maxBatchChanges", 0); + this.capabilityFactory = capabilityFactory; } public int getEffectiveMaxBatchChangesLimit(CurrentUser user) { - if (user.getCapabilities().canPerform(BATCH_CHANGES_LIMIT)) { - return user.getCapabilities().getRange(BATCH_CHANGES_LIMIT).getMax(); + CapabilityControl cap = capabilityFactory.create(user); + if (cap.hasExplicitRange(BATCH_CHANGES_LIMIT)) { + return cap.getRange(BATCH_CHANGES_LIMIT).getMax(); } return systemMaxBatchChanges; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java index 6ac5da1..50044bd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplaceOp.java
@@ -15,36 +15,41 @@ package com.google.gerrit.server.git; import static com.google.gerrit.common.FooterConstants.CHANGE_ID; +import static com.google.gerrit.server.ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET; import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters; import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromReviewers; import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER; +import static org.eclipse.jgit.lib.Constants.R_HEADS; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.client.ChangeKind; import com.google.gerrit.extensions.restapi.RestApiException; -import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; +import com.google.gerrit.reviewdb.client.Comment; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.PatchSetInfo; import com.google.gerrit.server.ApprovalCopier; import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.ChangeMessagesUtil; +import com.google.gerrit.server.CommentsUtil; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.change.ChangeKindCache; +import com.google.gerrit.server.change.EmailReviewComments; import com.google.gerrit.server.extensions.events.CommentAdded; -import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.extensions.events.RevisionCreated; import com.google.gerrit.server.git.ReceiveCommits.MagicBranchInput; import com.google.gerrit.server.mail.MailUtil.MailRecipients; import com.google.gerrit.server.mail.send.ReplacePatchSetSender; +import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ProjectControl; @@ -55,20 +60,18 @@ import com.google.gerrit.server.update.RepoContext; import com.google.gerrit.server.util.RequestScopePropagator; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import com.google.inject.util.Providers; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.RefDatabase; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.PushCertificate; @@ -83,9 +86,9 @@ Branch.NameKey dest, boolean checkMergedInto, @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId, - @Assisted("priorCommit") RevCommit priorCommit, + @Assisted("priorCommitId") ObjectId priorCommit, @Assisted("patchSetId") PatchSet.Id patchSetId, - @Assisted("commit") RevCommit commit, + @Assisted("commitId") ObjectId commitId, PatchSetInfo info, List<String> groups, @Nullable MagicBranchInput magicBranch, @@ -103,8 +106,9 @@ private final ChangeData.Factory changeDataFactory; private final ChangeKindCache changeKindCache; private final ChangeMessagesUtil cmUtil; + private final CommentsUtil commentsUtil; + private final EmailReviewComments.Factory emailCommentsFactory; private final ExecutorService sendEmailExecutor; - private final GitReferenceUpdated gitRefUpdated; private final RevisionCreated revisionCreated; private final CommentAdded commentAdded; private final MergedByPushOp.Factory mergedByPushOpFactory; @@ -115,9 +119,9 @@ private final Branch.NameKey dest; private final boolean checkMergedInto; private final PatchSet.Id priorPatchSetId; - private final RevCommit priorCommit; + private final ObjectId priorCommitId; private final PatchSet.Id patchSetId; - private final RevCommit commit; + private final ObjectId commitId; private final PatchSetInfo info; private final MagicBranchInput magicBranch; private final PushCertificate pushCertificate; @@ -125,16 +129,18 @@ private final Map<String, Short> approvals = new HashMap<>(); private final MailRecipients recipients = new MailRecipients(); - private Change change; + private RevCommit commit; + private ReceiveCommand cmd; + private ChangeNotes notes; private PatchSet newPatchSet; private ChangeKind changeKind; private ChangeMessage msg; + private List<Comment> comments = ImmutableList.of(); private String rejectMessage; private MergedByPushOp mergedByPushOp; private RequestScopePropagator requestScopePropagator; - private boolean updateRef; - @AssistedInject + @Inject ReplaceOp( AccountResolver accountResolver, ApprovalCopier approvalCopier, @@ -143,7 +149,8 @@ ChangeData.Factory changeDataFactory, ChangeKindCache changeKindCache, ChangeMessagesUtil cmUtil, - GitReferenceUpdated gitRefUpdated, + CommentsUtil commentsUtil, + EmailReviewComments.Factory emailCommentsFactory, RevisionCreated revisionCreated, CommentAdded commentAdded, MergedByPushOp.Factory mergedByPushOpFactory, @@ -154,9 +161,9 @@ @Assisted Branch.NameKey dest, @Assisted boolean checkMergedInto, @Assisted("priorPatchSetId") PatchSet.Id priorPatchSetId, - @Assisted("priorCommit") RevCommit priorCommit, + @Assisted("priorCommitId") ObjectId priorCommitId, @Assisted("patchSetId") PatchSet.Id patchSetId, - @Assisted("commit") RevCommit commit, + @Assisted("commitId") ObjectId commitId, @Assisted PatchSetInfo info, @Assisted List<String> groups, @Assisted @Nullable MagicBranchInput magicBranch, @@ -168,7 +175,8 @@ this.changeDataFactory = changeDataFactory; this.changeKindCache = changeKindCache; this.cmUtil = cmUtil; - this.gitRefUpdated = gitRefUpdated; + this.commentsUtil = commentsUtil; + this.emailCommentsFactory = emailCommentsFactory; this.revisionCreated = revisionCreated; this.commentAdded = commentAdded; this.mergedByPushOpFactory = mergedByPushOpFactory; @@ -180,45 +188,50 @@ this.dest = dest; this.checkMergedInto = checkMergedInto; this.priorPatchSetId = priorPatchSetId; - this.priorCommit = priorCommit; + this.priorCommitId = priorCommitId.copy(); this.patchSetId = patchSetId; - this.commit = commit; + this.commitId = commitId.copy(); this.info = info; this.groups = groups; this.magicBranch = magicBranch; this.pushCertificate = pushCertificate; - this.updateRef = true; } @Override public void updateRepo(RepoContext ctx) throws Exception { + commit = ctx.getRevWalk().parseCommit(commitId); + ctx.getRevWalk().parseBody(commit); changeKind = changeKindCache.getChangeKind( - projectControl.getProject().getNameKey(), ctx.getRepository(), priorCommit, commit); + projectControl.getProject().getNameKey(), + ctx.getRevWalk(), + ctx.getRepoView().getConfig(), + priorCommitId, + commitId); if (checkMergedInto) { - Ref mergedInto = findMergedInto(ctx, dest.get(), commit); + String mergedInto = findMergedInto(ctx, dest.get(), commit); if (mergedInto != null) { mergedByPushOp = - mergedByPushOpFactory.create(requestScopePropagator, patchSetId, mergedInto.getName()); + mergedByPushOpFactory.create(requestScopePropagator, patchSetId, mergedInto); } } - if (updateRef) { - ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), commit, patchSetId.toRefName())); - } + cmd = new ReceiveCommand(ObjectId.zeroId(), commitId, patchSetId.toRefName()); + ctx.addRefUpdate(cmd); } @Override public boolean updateChange(ChangeContext ctx) throws RestApiException, OrmException, IOException { - change = ctx.getChange(); + notes = ctx.getNotes(); + Change change = notes.getChange(); if (change == null || change.getStatus().isClosed()) { rejectMessage = CHANGE_IS_CLOSED; return false; } if (groups.isEmpty()) { - PatchSet prevPs = psUtil.current(ctx.getDb(), ctx.getNotes()); + PatchSet prevPs = psUtil.current(ctx.getDb(), notes); groups = prevPs != null ? prevPs.getGroups() : ImmutableList.<String>of(); } @@ -234,12 +247,30 @@ approvals.putAll(magicBranch.labels); Set<String> hashtags = magicBranch.hashtags; if (hashtags != null && !hashtags.isEmpty()) { - hashtags.addAll(ctx.getNotes().getHashtags()); + hashtags.addAll(notes.getHashtags()); update.setHashtags(hashtags); } if (magicBranch.topic != null && !magicBranch.topic.equals(ctx.getChange().getTopic())) { update.setTopic(magicBranch.topic); } + if (magicBranch.removePrivate) { + change.setPrivate(false); + update.setPrivate(false); + } else if (magicBranch.isPrivate) { + change.setPrivate(true); + update.setPrivate(true); + } + if (magicBranch.ready) { + change.setWorkInProgress(false); + change.setReviewStarted(true); + update.setWorkInProgress(false); + } else if (magicBranch.workInProgress) { + change.setWorkInProgress(true); + update.setWorkInProgress(true); + } + if (shouldPublishComments()) { + comments = publishComments(ctx); + } } boolean draft = magicBranch != null && magicBranch.draft; @@ -252,7 +283,7 @@ ctx.getRevWalk(), update, patchSetId, - commit, + commitId, draft, groups, pushCertificate != null ? pushCertificate.toTextWithSignature() : null, @@ -272,7 +303,13 @@ newPatchSet, ctx.getControl(), approvals); - approvalCopier.copy(ctx.getDb(), ctx.getControl(), newPatchSet, newApprovals); + approvalCopier.copyInReviewDb( + ctx.getDb(), + ctx.getControl(), + newPatchSet, + ctx.getRevWalk(), + ctx.getRepoView().getConfig(), + newApprovals); approvalsUtil.addReviewers( ctx.getDb(), update, @@ -292,6 +329,20 @@ recipients.add(oldRecipients); + msg = createChangeMessage(ctx, reviewMessage); + cmUtil.addChangeMessage(ctx.getDb(), update, msg); + + if (mergedByPushOp == null) { + resetChange(ctx); + } else { + mergedByPushOp.setPatchSetProvider(Providers.of(newPatchSet)).updateChange(ctx); + } + + return true; + } + + private ChangeMessage createChangeMessage(ChangeContext ctx, String reviewMessage) + throws OrmException, IOException { String approvalMessage = ApprovalsUtil.renderMessageWithApprovals( patchSetId.get(), approvals, scanLabels(ctx, approvals)); @@ -302,25 +353,21 @@ } else { message.append('.'); } + if (comments.size() == 1) { + message.append("\n\n(1 comment)"); + } else if (comments.size() > 1) { + message.append(String.format("\n\n(%d comments)", comments.size())); + } if (!Strings.isNullOrEmpty(reviewMessage)) { - message.append("\n").append(reviewMessage); + message.append("\n\n").append(reviewMessage); } - msg = - ChangeMessagesUtil.newMessage( - patchSetId, - ctx.getUser(), - ctx.getWhen(), - message.toString(), - ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET); - cmUtil.addChangeMessage(ctx.getDb(), update, msg); - - if (mergedByPushOp == null) { - resetChange(ctx); - } else { - mergedByPushOp.setPatchSetProvider(Providers.of(newPatchSet)).updateChange(ctx); - } - - return true; + boolean workInProgress = magicBranch != null && magicBranch.workInProgress; + return ChangeMessagesUtil.newMessage( + patchSetId, + ctx.getUser(), + ctx.getWhen(), + message.toString(), + ChangeMessagesUtil.uploadedPatchSetTag(workInProgress)); } private String changeKindMessage(ChangeKind changeKind) { @@ -338,13 +385,18 @@ } private Map<String, PatchSetApproval> scanLabels(ChangeContext ctx, Map<String, Short> approvals) - throws OrmException { + throws OrmException, IOException { Map<String, PatchSetApproval> current = new HashMap<>(); // We optimize here and only retrieve current when approvals provided if (!approvals.isEmpty()) { for (PatchSetApproval a : approvalsUtil.byPatchSetUser( - ctx.getDb(), ctx.getControl(), priorPatchSetId, ctx.getAccountId())) { + ctx.getDb(), + ctx.getControl(), + priorPatchSetId, + ctx.getAccountId(), + ctx.getRevWalk(), + ctx.getRepoView().getConfig())) { if (a.isLegacySubmit()) { continue; } @@ -376,66 +428,50 @@ List<String> idList = commit.getFooterLines(CHANGE_ID); if (idList.isEmpty()) { - change.setKey(new Change.Key("I" + commit.name())); + change.setKey(new Change.Key("I" + commitId.name())); } else { change.setKey(new Change.Key(idList.get(idList.size() - 1).trim())); } } + private List<Comment> publishComments(ChangeContext ctx) throws OrmException { + List<Comment> comments = + commentsUtil.draftByChangeAuthor(ctx.getDb(), ctx.getNotes(), ctx.getUser().getAccountId()); + commentsUtil.publish(ctx, patchSetId, comments, TAG_UPLOADED_PATCH_SET); + return comments; + } + @Override - public void postUpdate(final Context ctx) throws Exception { - // Normally the ref updated hook is fired by BatchUpdate, but ReplaceOp is - // special because its ref is actually updated by ReceiveCommits, so from - // BatchUpdate's perspective there is no ref update. Thus we have to fire it - // manually. - final Account account = ctx.getAccount(); - if (!updateRef) { - gitRefUpdated.fire( - ctx.getProject(), newPatchSet.getRefName(), ObjectId.zeroId(), commit, account); - } - + public void postUpdate(Context ctx) throws Exception { if (changeKind != ChangeKind.TRIVIAL_REBASE) { - Runnable sender = - new Runnable() { - @Override - public void run() { - try { - ReplacePatchSetSender cm = - replacePatchSetFactory.create( - projectControl.getProject().getNameKey(), change.getId()); - cm.setFrom(account.getId()); - cm.setPatchSet(newPatchSet, info); - cm.setChangeMessage(msg.getMessage(), ctx.getWhen()); - if (magicBranch != null) { - cm.setNotify(magicBranch.notify); - cm.setAccountsToNotify(magicBranch.getAccountsToNotify()); - } - cm.addReviewers(recipients.getReviewers()); - cm.addExtraCC(recipients.getCcOnly()); - cm.send(); - } catch (Exception e) { - log.error("Cannot send email for new patch set " + newPatchSet.getId(), e); - } - } - - @Override - public String toString() { - return "send-email newpatchset"; - } - }; - + // TODO(dborowitz): Merge email templates so we only have to send one. + Runnable e = new ReplaceEmailTask(ctx); if (requestScopePropagator != null) { @SuppressWarnings("unused") - Future<?> possiblyIgnoredError = - sendEmailExecutor.submit(requestScopePropagator.wrap(sender)); + Future<?> possiblyIgnoredError = sendEmailExecutor.submit(requestScopePropagator.wrap(e)); } else { - sender.run(); + e.run(); } } - NotifyHandling notify = - magicBranch != null && magicBranch.notify != null ? magicBranch.notify : NotifyHandling.ALL; - revisionCreated.fire(change, newPatchSet, ctx.getAccount(), ctx.getWhen(), notify); + NotifyHandling notify = magicBranch != null ? magicBranch.getNotify(notes) : NotifyHandling.ALL; + + if (shouldPublishComments()) { + emailCommentsFactory + .create( + notify, + magicBranch != null ? magicBranch.getAccountsToNotify() : ImmutableListMultimap.of(), + notes, + newPatchSet, + ctx.getUser().asIdentifiedUser(), + msg, + comments, + msg.getMessage(), + ImmutableList.of()) // TODO(dborowitz): Include labels. + .sendAsync(); + } + + revisionCreated.fire(notes.getChange(), newPatchSet, ctx.getAccount(), ctx.getWhen(), notify); try { fireCommentAddedEvent(ctx); } catch (Exception e) { @@ -446,6 +482,40 @@ } } + private class ReplaceEmailTask implements Runnable { + private final Context ctx; + + private ReplaceEmailTask(Context ctx) { + this.ctx = ctx; + } + + @Override + public void run() { + try { + ReplacePatchSetSender cm = + replacePatchSetFactory.create( + projectControl.getProject().getNameKey(), notes.getChangeId()); + cm.setFrom(ctx.getAccount().getId()); + cm.setPatchSet(newPatchSet, info); + cm.setChangeMessage(msg.getMessage(), ctx.getWhen()); + if (magicBranch != null) { + cm.setNotify(magicBranch.getNotify(notes)); + cm.setAccountsToNotify(magicBranch.getAccountsToNotify()); + } + cm.addReviewers(recipients.getReviewers()); + cm.addExtraCC(recipients.getCcOnly()); + cm.send(); + } catch (Exception e) { + log.error("Cannot send email for new patch set " + newPatchSet.getId(), e); + } + } + + @Override + public String toString() { + return "send-email newpatchset"; + } + } + private void fireCommentAddedEvent(Context ctx) throws OrmException { if (approvals.isEmpty()) { return; @@ -457,7 +527,7 @@ * show a transition from an oldValue of 0 to the new value. */ ChangeControl changeControl = - changeControlFactory.controlFor(ctx.getDb(), change, ctx.getUser()); + changeControlFactory.controlFor(ctx.getDb(), notes.getChange(), ctx.getUser()); List<LabelType> labels = changeControl.getLabelTypes().getLabelTypes(); Map<String, Short> allApprovals = new HashMap<>(); Map<String, Short> oldApprovals = new HashMap<>(); @@ -473,7 +543,13 @@ } commentAdded.fire( - change, newPatchSet, ctx.getAccount(), null, allApprovals, oldApprovals, ctx.getWhen()); + notes.getChange(), + newPatchSet, + ctx.getAccount(), + null, + allApprovals, + oldApprovals, + ctx.getWhen()); } public PatchSet getPatchSet() { @@ -481,16 +557,15 @@ } public Change getChange() { - return change; + return notes.getChange(); } public String getRejectMessage() { return rejectMessage; } - public ReplaceOp setUpdateRef(boolean updateRef) { - this.updateRef = updateRef; - return this; + public ReceiveCommand getCommand() { + return cmd; } public ReplaceOp setRequestScopePropagator(RequestScopePropagator requestScopePropagator) { @@ -498,18 +573,17 @@ return this; } - private Ref findMergedInto(Context ctx, String first, RevCommit commit) { + private static String findMergedInto(Context ctx, String first, RevCommit commit) { try { - RefDatabase refDatabase = ctx.getRepository().getRefDatabase(); - - Ref firstRef = refDatabase.exactRef(first); - if (firstRef != null && isMergedInto(ctx.getRevWalk(), commit, firstRef)) { - return firstRef; + RevWalk rw = ctx.getRevWalk(); + Optional<ObjectId> firstId = ctx.getRepoView().getRef(first); + if (firstId.isPresent() && rw.isMergedInto(commit, rw.parseCommit(firstId.get()))) { + return first; } - for (Ref ref : refDatabase.getRefs(Constants.R_HEADS).values()) { - if (isMergedInto(ctx.getRevWalk(), commit, ref)) { - return ref; + for (Map.Entry<String, ObjectId> e : ctx.getRepoView().getRefs(R_HEADS).entrySet()) { + if (rw.isMergedInto(commit, rw.parseCommit(e.getValue()))) { + return R_HEADS + e.getKey(); } } return null; @@ -519,7 +593,7 @@ } } - private static boolean isMergedInto(RevWalk rw, RevCommit commit, Ref ref) throws IOException { - return rw.isMergedInto(commit, rw.parseCommit(ref.getObjectId())); + private boolean shouldPublishComments() { + return magicBranch != null && magicBranch.shouldPublishComments(); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java index e7a86f1..6b2493a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepoRefCache.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.git; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -44,4 +45,9 @@ ids.put(refName, id); return id; } + + /** @return an unmodifiable view of the refs that have been cached by this instance. */ + public Map<String, Optional<ObjectId>> getCachedRefs() { + return Collections.unmodifiableMap(ids); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java index 45ec769..3a7a125 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
@@ -28,7 +28,7 @@ private static final long serialVersionUID = 1L; /** @param projectName name of the project that cannot be created */ - public RepositoryCaseMismatchException(final Project.NameKey projectName) { + public RepositoryCaseMismatchException(Project.NameKey projectName) { super("Name occupied in other case. Project " + projectName.get() + " cannot be created."); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SendEmailExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SendEmailExecutor.java index feb32fa..7d37e5a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SendEmailExecutor.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SendEmailExecutor.java
@@ -18,8 +18,9 @@ import com.google.inject.BindingAnnotation; import java.lang.annotation.Retention; +import java.util.concurrent.ScheduledThreadPoolExecutor; -/** Marker on the global {@link WorkQueue.Executor} used to send email. */ +/** Marker on the global {@link ScheduledThreadPoolExecutor} used to send email. */ @Retention(RUNTIME) @BindingAnnotation public @interface SendEmailExecutor {}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java index 5a3b4ff..a0422be 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java
@@ -14,15 +14,19 @@ package com.google.gerrit.server.git; -/** Indicates the gitlink's update cannot be processed at this time. */ +/** + * Indicates the gitlink's update cannot be processed at this time. + * + * <p>Message should be considered user-visible. + */ public class SubmoduleException extends Exception { private static final long serialVersionUID = 1L; - SubmoduleException(final String msg) { + SubmoduleException(String msg) { super(msg, null); } - SubmoduleException(final String msg, final Throwable why) { + SubmoduleException(String msg, Throwable why) { super(msg, why); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java index 56c0c44..eb9c024 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -35,8 +35,8 @@ import com.google.gerrit.server.update.RepoContext; import com.google.gerrit.server.update.RepoOnlyOp; import com.google.gerrit.server.update.UpdateException; -import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; +import com.google.inject.Inject; +import com.google.inject.Singleton; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; @@ -63,7 +63,6 @@ import org.eclipse.jgit.lib.Ref; 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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -82,14 +81,49 @@ public void updateRepo(RepoContext ctx) throws Exception { CodeReviewCommit c = composeGitlinksCommit(branch); if (c != null) { - ctx.addRefUpdate(new ReceiveCommand(c.getParent(0), c, branch.get())); + ctx.addRefUpdate(c.getParent(0), c, branch.get()); addBranchTip(branch, c); } } } - public interface Factory { - SubmoduleOp create(Set<Branch.NameKey> updatedBranches, MergeOpRepoManager orm); + @Singleton + public static class Factory { + private final GitModules.Factory gitmodulesFactory; + private final PersonIdent myIdent; + private final Config cfg; + private final ProjectCache projectCache; + private final ProjectState.Factory projectStateFactory; + private final BatchUpdate.Factory batchUpdateFactory; + + @Inject + Factory( + GitModules.Factory gitmodulesFactory, + @GerritPersonIdent PersonIdent myIdent, + @GerritServerConfig Config cfg, + ProjectCache projectCache, + ProjectState.Factory projectStateFactory, + BatchUpdate.Factory batchUpdateFactory) { + this.gitmodulesFactory = gitmodulesFactory; + this.myIdent = myIdent; + this.cfg = cfg; + this.projectCache = projectCache; + this.projectStateFactory = projectStateFactory; + this.batchUpdateFactory = batchUpdateFactory; + } + + public SubmoduleOp create(Set<Branch.NameKey> updatedBranches, MergeOpRepoManager orm) + throws SubmoduleException { + return new SubmoduleOp( + gitmodulesFactory, + myIdent, + cfg, + projectCache, + projectStateFactory, + batchUpdateFactory, + updatedBranches, + orm); + } } private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class); @@ -117,16 +151,15 @@ // map of superproject and its branches which has submodule subscriptions private final SetMultimap<Project.NameKey, Branch.NameKey> branchesByProject; - @AssistedInject - public SubmoduleOp( + private SubmoduleOp( GitModules.Factory gitmodulesFactory, - @GerritPersonIdent PersonIdent myIdent, - @GerritServerConfig Config cfg, + PersonIdent myIdent, + Config cfg, ProjectCache projectCache, ProjectState.Factory projectStateFactory, BatchUpdate.Factory batchUpdateFactory, - @Assisted Set<Branch.NameKey> updatedBranches, - @Assisted MergeOpRepoManager orm) + Set<Branch.NameKey> updatedBranches, + MergeOpRepoManager orm) throws SubmoduleException { this.gitmodulesFactory = gitmodulesFactory; this.myIdent = myIdent; @@ -349,7 +382,7 @@ } /** Create a separate gitlink commit */ - public CodeReviewCommit composeGitlinksCommit(final Branch.NameKey subscriber) + public CodeReviewCommit composeGitlinksCommit(Branch.NameKey subscriber) throws IOException, SubmoduleException { OpenRepo or; try { @@ -408,7 +441,7 @@ /** Amend an existing commit with gitlink updates */ public CodeReviewCommit composeGitlinksCommit( - final Branch.NameKey subscriber, CodeReviewCommit currentCommit) + Branch.NameKey subscriber, CodeReviewCommit currentCommit) throws IOException, SubmoduleException { OpenRepo or; try { @@ -435,7 +468,7 @@ commit.setTreeId(newTreeId); commit.setParentIds(currentCommit.getParents()); if (verboseSuperProject != VerboseSuperprojectUpdate.FALSE) { - //TODO:czhen handle cherrypick footer + // TODO(czhen): handle cherrypick footer commit.setMessage(currentCommit.getFullMessage() + "\n\n* submodules:\n" + msgbuf.toString()); } else { commit.setMessage(currentCommit.getFullMessage()); @@ -449,7 +482,7 @@ } private RevCommit updateSubmodule( - DirCache dc, DirCacheEditor ed, StringBuilder msgbuf, final SubmoduleSubscription s) + DirCache dc, DirCacheEditor ed, StringBuilder msgbuf, SubmoduleSubscription s) throws SubmoduleException, IOException { OpenRepo subOr; try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java index 4ac9071..204a0d5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
@@ -31,7 +31,7 @@ private final String maxObjectSizeLimitFormatted; @Inject - TransferConfig(@GerritServerConfig final Config cfg) { + TransferConfig(@GerritServerConfig Config cfg) { timeout = (int) ConfigUtil.getTimeUnit(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java index 4185141..448ab15 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
@@ -45,7 +45,7 @@ void error(ValidationError error); } - public static Sink createLoggerSink(final String message, final Logger log) { + public static Sink createLoggerSink(String message, Logger log) { return new ValidationError.Sink() { @Override public void error(ValidationError error) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java index 2b9151b..8b7df19 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -75,6 +75,7 @@ } protected RevCommit revision; + protected RevWalk rw; protected ObjectReader reader; protected ObjectInserter inserter; protected DirCache newTree; @@ -153,11 +154,13 @@ * @throws ConfigInvalidException */ public void load(RevWalk walk, ObjectId id) throws IOException, ConfigInvalidException { + this.rw = walk; this.reader = walk.getObjectReader(); try { revision = id != null ? walk.parseCommit(id) : null; onLoad(); } finally { + walk = null; reader = null; } } @@ -238,7 +241,7 @@ * @param update helper info about the update. * @throws IOException if the update failed. */ - public BatchMetaDataUpdate openUpdate(final MetaDataUpdate update) throws IOException { + public BatchMetaDataUpdate openUpdate(MetaDataUpdate update) throws IOException { final Repository db = update.getRepository(); reader = db.newObjectReader();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java index 47d416c..93aa361 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -22,23 +22,29 @@ import com.google.common.collect.ImmutableSet; import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.project.ProjectControl; +import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.query.change.ChangeData; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.assistedinject.Assisted; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefDatabase; @@ -53,50 +59,65 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook { private static final Logger log = LoggerFactory.getLogger(VisibleRefFilter.class); + public interface Factory { + VisibleRefFilter create(ProjectState projectState, Repository git); + } + private final TagCache tagCache; private final ChangeNotes.Factory changeNotesFactory; @Nullable private final SearchingChangeCacheImpl changeCache; - private final Repository db; - private final Project.NameKey projectName; - private final ProjectControl projectCtl; - private final ReviewDb reviewDb; - private final boolean showMetadata; + private final Provider<ReviewDb> db; + private final Provider<CurrentUser> user; + private final PermissionBackend permissionBackend; + private final ProjectState projectState; + private final Repository git; + private ProjectControl projectCtl; + private boolean showMetadata = true; private String userEditPrefix; - private Set<Change.Id> visibleChanges; + private Map<Change.Id, Branch.NameKey> visibleChanges; - public VisibleRefFilter( + @Inject + VisibleRefFilter( TagCache tagCache, ChangeNotes.Factory changeNotesFactory, @Nullable SearchingChangeCacheImpl changeCache, - Repository db, - ProjectControl projectControl, - ReviewDb reviewDb, - boolean showMetadata) { + Provider<ReviewDb> db, + Provider<CurrentUser> user, + PermissionBackend permissionBackend, + @Assisted ProjectState projectState, + @Assisted Repository git) { this.tagCache = tagCache; this.changeNotesFactory = changeNotesFactory; this.changeCache = changeCache; this.db = db; - this.projectName = projectControl.getProject().getNameKey(); - this.projectCtl = projectControl; - this.reviewDb = reviewDb; - this.showMetadata = showMetadata; + this.user = user; + this.permissionBackend = permissionBackend; + this.projectState = projectState; + this.git = git; + } + + /** Show change references. Default is {@code true}. */ + public VisibleRefFilter setShowMetadata(boolean show) { + showMetadata = show; + return this; } public Map<String, Ref> filter(Map<String, Ref> refs, boolean filterTagsSeparately) { - if (projectCtl.getProjectState().isAllUsers()) { + if (projectState.isAllUsers()) { refs = addUsersSelfSymref(refs); } + projectCtl = projectState.controlFor(user.get()); if (projectCtl.allRefsAreVisible(ImmutableSet.of(REFS_CONFIG))) { return fastHideRefsMetaConfig(refs); } Account.Id userId; boolean viewMetadata; - if (projectCtl.getUser().isIdentifiedUser()) { - IdentifiedUser user = projectCtl.getUser().asIdentifiedUser(); - userId = user.getAccountId(); - viewMetadata = user.getCapabilities().canAccessDatabase(); + if (user.get().isIdentifiedUser()) { + viewMetadata = permissionBackend.user(user).testOrFalse(GlobalPermission.ACCESS_DATABASE); + IdentifiedUser u = user.get().asIdentifiedUser(); + userId = u.getAccountId(); userEditPrefix = RefNames.refsEditPrefix(userId); } else { userId = null; @@ -110,8 +131,7 @@ String name = ref.getName(); Change.Id changeId; Account.Id accountId; - if (name.startsWith(REFS_CACHE_AUTOMERGE) - || (!showMetadata && isMetadata(projectCtl, name))) { + if (name.startsWith(REFS_CACHE_AUTOMERGE) || (!showMetadata && isMetadata(name))) { continue; } else if (RefNames.isRefsEdit(name)) { // Edits are visible only to the owning user, if change is visible. @@ -139,8 +159,7 @@ if (viewMetadata) { result.put(name, ref); } - } else if (projectCtl.getProjectState().isAllUsers() - && name.equals(RefNames.REFS_EXTERNAL_IDS)) { + } else if (projectState.isAllUsers() && name.equals(RefNames.REFS_EXTERNAL_IDS)) { // The notes branch with the external IDs of all users must not be exposed to normal users. if (viewMetadata) { result.put(name, ref); @@ -159,11 +178,11 @@ if (!deferredTags.isEmpty() && (!result.isEmpty() || filterTagsSeparately)) { TagMatcher tags = tagCache - .get(projectName) + .get(projectState.getProject().getNameKey()) .matcher( tagCache, - db, - filterTagsSeparately ? filter(db.getAllRefs()).values() : result.values()); + git, + filterTagsSeparately ? filter(git.getAllRefs()).values() : result.values()); for (Ref tag : deferredTags) { if (tags.isReachable(tag)) { result.put(tag.getName(), tag); @@ -184,8 +203,8 @@ } private Map<String, Ref> addUsersSelfSymref(Map<String, Ref> refs) { - if (projectCtl.getUser().isIdentifiedUser()) { - Ref r = refs.get(RefNames.refsUsers(projectCtl.getUser().getAccountId())); + if (user.get().isIdentifiedUser()) { + Ref r = refs.get(RefNames.refsUsers(user.get().getAccountId())); if (r != null) { SymbolicRef s = new SymbolicRef(REFS_USERS_SELF, r); refs = new HashMap<>(refs); @@ -221,26 +240,30 @@ visibleChanges = visibleChangesBySearch(); } } - return visibleChanges.contains(changeId); + return visibleChanges.containsKey(changeId); } private boolean visibleEdit(String name) { - if (userEditPrefix != null && name.startsWith(userEditPrefix)) { - Change.Id id = Change.Id.fromEditRefPart(name); - if (id != null) { - return visible(id); - } + Change.Id id = Change.Id.fromEditRefPart(name); + // Initialize if it wasn't yet + if (visibleChanges == null) { + visible(id); + } + if (id != null) { + return (userEditPrefix != null && name.startsWith(userEditPrefix) && visible(id)) + || (visibleChanges.containsKey(id) + && projectCtl.controlForRef(visibleChanges.get(id)).isEditVisible()); } return false; } - private Set<Change.Id> visibleChangesBySearch() { + private Map<Change.Id, Branch.NameKey> visibleChangesBySearch() { Project project = projectCtl.getProject(); try { - Set<Change.Id> visibleChanges = new HashSet<>(); - for (ChangeData cd : changeCache.getChangeData(reviewDb, project.getNameKey())) { - if (projectCtl.controlForIndexedChange(cd.change()).isVisible(reviewDb, cd)) { - visibleChanges.add(cd.getId()); + Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>(); + for (ChangeData cd : changeCache.getChangeData(db.get(), project.getNameKey())) { + if (projectCtl.controlForIndexedChange(cd.change()).isVisible(db.get(), cd)) { + visibleChanges.put(cd.getId(), cd.change().getDest()); } } return visibleChanges; @@ -250,31 +273,31 @@ + project.getName() + ", assuming no changes are visible", e); - return Collections.emptySet(); + return Collections.emptyMap(); } } - private Set<Change.Id> visibleChangesByScan() { + private Map<Change.Id, Branch.NameKey> visibleChangesByScan() { Project.NameKey project = projectCtl.getProject().getNameKey(); try { - Set<Change.Id> visibleChanges = new HashSet<>(); - for (ChangeNotes cn : changeNotesFactory.scan(db, reviewDb, project)) { - if (projectCtl.controlFor(cn).isVisible(reviewDb)) { - visibleChanges.add(cn.getChangeId()); + Map<Change.Id, Branch.NameKey> visibleChanges = new HashMap<>(); + for (ChangeNotes cn : changeNotesFactory.scan(git, db.get(), project)) { + if (projectCtl.controlFor(cn).isVisible(db.get())) { + visibleChanges.put(cn.getChangeId(), cn.getChange().getDest()); } } return visibleChanges; } catch (IOException | OrmException e) { log.error( "Cannot load changes for project " + project + ", assuming no changes are visible", e); - return Collections.emptySet(); + return Collections.emptyMap(); } } - private static boolean isMetadata(ProjectControl projectCtl, String name) { + private boolean isMetadata(String name) { return name.startsWith(REFS_CHANGES) || RefNames.isRefsEdit(name) - || (projectCtl.getProjectState().isAllUsers() && name.equals(RefNames.REFS_EXTERNAL_IDS)); + || (projectState.isAllUsers() && name.equals(RefNames.REFS_EXTERNAL_IDS)); } private static boolean isTag(Ref ref) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java index 99af21d..0adb45a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -35,6 +35,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.RunnableScheduledFuture; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; @@ -52,7 +53,7 @@ private final WorkQueue workQueue; @Inject - Lifecycle(final WorkQueue workQeueue) { + Lifecycle(WorkQueue workQeueue) { this.workQueue = workQeueue; } @@ -82,7 +83,7 @@ } }; - private Executor defaultQueue; + private ScheduledExecutorService defaultQueue; private int defaultQueueSize; private final IdGenerator idGenerator; private final CopyOnWriteArrayList<Executor> queues; @@ -99,7 +100,7 @@ } /** Get the default work queue, for miscellaneous tasks. */ - public synchronized Executor getDefaultQueue() { + public synchronized ScheduledExecutorService getDefaultQueue() { if (defaultQueue == null) { defaultQueue = createQueue(defaultQueueSize, "WorkQueue"); } @@ -107,18 +108,32 @@ } /** Create a new executor queue. */ - public Executor createQueue(int poolsize, String prefix) { - final Executor r = new Executor(poolsize, prefix); - r.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); - r.setExecuteExistingDelayedTasksAfterShutdownPolicy(true); - queues.add(r); - return r; + public ScheduledExecutorService createQueue(int poolsize, String prefix) { + return createQueue(poolsize, prefix, Thread.NORM_PRIORITY); + } + + public ScheduledThreadPoolExecutor createQueue(int poolsize, String prefix, int threadPriority) { + Executor executor = new Executor(poolsize, prefix); + executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); + executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(true); + queues.add(executor); + if (threadPriority != Thread.NORM_PRIORITY) { + ThreadFactory parent = executor.getThreadFactory(); + executor.setThreadFactory( + task -> { + Thread t = parent.newThread(task); + t.setPriority(threadPriority); + return t; + }); + } + + return executor; } /** Get all of the tasks currently scheduled in any work queue. */ public List<Task<?>> getTasks() { final List<Task<?>> r = new ArrayList<>(); - for (final Executor e : queues) { + for (Executor e : queues) { e.addAllTo(r); } return r; @@ -135,9 +150,9 @@ } /** Locate a task by its unique id, null if no task matches. */ - public Task<?> getTask(final int id) { + public Task<?> getTask(int id) { Task<?> result = null; - for (final Executor e : queues) { + for (Executor e : queues) { final Task<?> t = e.getTask(id); if (t != null) { if (result != null) { @@ -150,7 +165,7 @@ return result; } - public Executor getExecutor(String queueName) { + public ScheduledThreadPoolExecutor getExecutor(String queueName) { for (Executor e : queues) { if (e.queueName.equals(queueName)) { return e; @@ -160,7 +175,7 @@ } private void stop() { - for (final Executor p : queues) { + for (Executor p : queues) { p.shutdown(); boolean isTerminated; do { @@ -175,11 +190,11 @@ } /** An isolated queue. */ - public class Executor extends ScheduledThreadPoolExecutor { + private class Executor extends ScheduledThreadPoolExecutor { private final ConcurrentHashMap<Integer, Task<?>> all; private final String queueName; - Executor(int corePoolSize, final String prefix) { + Executor(int corePoolSize, String prefix) { super( corePoolSize, new ThreadFactory() { @@ -187,7 +202,7 @@ private final AtomicInteger tid = new AtomicInteger(1); @Override - public Thread newThread(final Runnable task) { + public Thread newThread(Runnable task) { final Thread t = parent.newThread(task); t.setName(prefix + "-" + tid.getAndIncrement()); t.setUncaughtExceptionHandler(LOG_UNCAUGHT_EXCEPTION); @@ -204,13 +219,15 @@ queueName = prefix; } - public void unregisterWorkQueue() { + @Override + protected void terminated() { + super.terminated(); queues.remove(this); } @Override protected <V> RunnableScheduledFuture<V> decorateTask( - final Runnable runnable, RunnableScheduledFuture<V> r) { + Runnable runnable, RunnableScheduledFuture<V> r) { r = super.decorateTask(runnable, r); for (; ; ) { final int id = idGenerator.next(); @@ -231,19 +248,19 @@ @Override protected <V> RunnableScheduledFuture<V> decorateTask( - final Callable<V> callable, final RunnableScheduledFuture<V> task) { + Callable<V> callable, RunnableScheduledFuture<V> task) { throw new UnsupportedOperationException("Callable not implemented"); } - void remove(final Task<?> task) { + void remove(Task<?> task) { all.remove(task.getTaskId(), task); } - Task<?> getTask(final int id) { + Task<?> getTask(int id) { return all.get(id); } - void addAllTo(final List<Task<?>> list) { + void addAllTo(List<Task<?>> list) { list.addAll(all.values()); // iterator is thread safe } @@ -428,8 +445,8 @@ @Override public String toString() { - //This is a workaround to be able to print a proper name when the task - //is wrapped into a TrustedListenableFutureTask. + // This is a workaround to be able to print a proper name when the task + // is wrapped into a TrustedListenableFutureTask. try { if (runnable .getClass()
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java index 96b5b55..49399ef 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -37,7 +37,6 @@ import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.transport.ReceiveCommand; public class CherryPick extends SubmitStrategy { @@ -95,7 +94,10 @@ // delta relative to that one parent and redoing that on the current merge // tip. args.rw.parseBody(toMerge); - psId = ChangeUtil.nextPatchSetId(args.repo, toMerge.change().currentPatchSetId()); + psId = + ChangeUtil.nextPatchSetIdFromChangeRefsMap( + ctx.getRepoView().getRefs(getId().toRefPrefix()), + toMerge.change().currentPatchSetId()); RevCommit mergeTip = args.mergeTip.getCurrentTip(); args.rw.parseBody(mergeTip); String cherryPickCmtMsg = args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip); @@ -105,8 +107,8 @@ try { newCommit = args.mergeUtil.createCherryPickFromCommit( - args.repo, - args.inserter, + ctx.getInserter(), + ctx.getRepoView().getConfig(), args.mergeTip.getCurrentTip(), toMerge, committer, @@ -132,7 +134,7 @@ args.mergeTip.moveTipTo(newCommit, newCommit); args.commitStatus.put(newCommit); - ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), newCommit, psId.toRefName())); + ctx.addRefUpdate(ObjectId.zeroId(), newCommit, psId.toRefName()); patchSetInfo = args.patchSetInfoFactory.get(ctx.getRevWalk(), newCommit, psId); } @@ -195,9 +197,9 @@ args.mergeUtil.mergeOneCommit( myIdent, myIdent, - args.repo, args.rw, - args.inserter, + ctx.getInserter(), + ctx.getRepoView().getConfig(), args.destBranch, mergeTip.getCurrentTip(), toMerge);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java index 7151486..38a193d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
@@ -29,8 +29,7 @@ @Override public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) throws IntegrationException { - List<CodeReviewCommit> sorted = - args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge, args.incoming); + List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge); List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size()); CodeReviewCommit newTipCommit = args.mergeUtil.getFirstFastForward(args.mergeTip.getInitialTip(), args.rw, sorted);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java index ce045f8..1664be4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
@@ -28,8 +28,7 @@ @Override public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) throws IntegrationException { - List<CodeReviewCommit> sorted = - args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge, args.incoming); + List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge); List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size()); if (args.mergeTip.getInitialTip() == null && !sorted.isEmpty()) { // The branch is unborn. Take a fast-forward resolution to
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java index e7db1a8..d30aab2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
@@ -28,8 +28,7 @@ @Override public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) throws IntegrationException { - List<CodeReviewCommit> sorted = - args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge, args.incoming); + List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge); List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size()); if (args.mergeTip.getInitialTip() == null
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java index 2a6680c..3c3812d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeOneOp.java
@@ -28,7 +28,8 @@ @Override public void updateRepoImpl(RepoContext ctx) throws IntegrationException, IOException { PersonIdent caller = - ctx.getIdentifiedUser().newCommitterIdent(ctx.getWhen(), ctx.getTimeZone()); + ctx.getIdentifiedUser() + .newCommitterIdent(args.serverIdent.getWhen(), args.serverIdent.getTimeZone()); if (args.mergeTip.getCurrentTip() == null) { throw new IllegalStateException( "cannot merge commit " @@ -36,16 +37,13 @@ + " onto a null tip; expected at least one fast-forward prior to" + " this operation"); } - // TODO(dborowitz): args.rw is needed because it's a CodeReviewRevWalk. - // When hoisting BatchUpdate into MergeOp, we will need to teach - // BatchUpdate how to produce CodeReviewRevWalks. CodeReviewCommit merged = args.mergeUtil.mergeOneCommit( caller, args.serverIdent, - ctx.getRepository(), args.rw, ctx.getInserter(), + ctx.getRepoView().getConfig(), args.destBranch, args.mergeTip.getCurrentTip(), toMerge);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java index 43ab01b..7f99228 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseSubmitStrategy.java
@@ -28,8 +28,7 @@ import com.google.gerrit.server.git.IntegrationException; import com.google.gerrit.server.git.MergeIdenticalTreeException; import com.google.gerrit.server.git.MergeTip; -import com.google.gerrit.server.git.RebaseSorter; -import com.google.gerrit.server.git.validators.CommitValidators; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.InvalidChangeOperationException; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.update.ChangeContext; @@ -42,8 +41,8 @@ import java.util.List; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.transport.ReceiveCommand; /** This strategy covers RebaseAlways and RebaseIfNecessary ones. */ public class RebaseSubmitStrategy extends SubmitStrategy { @@ -57,7 +56,12 @@ @Override public List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge) throws IntegrationException { - List<CodeReviewCommit> sorted = sort(toMerge); + List<CodeReviewCommit> sorted; + try { + sorted = args.rebaseSorter.sort(toMerge); + } catch (IOException e) { + throw new IntegrationException("Commit sorting failed", e); + } List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size()); boolean first = true; @@ -67,7 +71,7 @@ // MERGE_IF_NECESSARY semantics to avoid creating duplicate // commits. // - sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted, args.incoming); + sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted); break; } } @@ -115,10 +119,7 @@ @Override public void updateRepoImpl(RepoContext ctx) throws IntegrationException, InvalidChangeOperationException, RestApiException, IOException, - OrmException { - // TODO(dborowitz): args.rw is needed because it's a CodeReviewRevWalk. - // When hoisting BatchUpdate into MergeOp, we will need to teach - // BatchUpdate how to produce CodeReviewRevWalks. + OrmException, PermissionBackendException { if (args.mergeUtil.canFastForward( args.mergeSorter, args.mergeTip.getCurrentTip(), args.rw, toMerge)) { if (!rebaseAlways) { @@ -129,7 +130,10 @@ } // RebaseAlways means we modify commit message. args.rw.parseBody(toMerge); - newPatchSetId = ChangeUtil.nextPatchSetId(args.repo, toMerge.change().currentPatchSetId()); + newPatchSetId = + ChangeUtil.nextPatchSetIdFromChangeRefsMap( + ctx.getRepoView().getRefs(getId().toRefPrefix()), + toMerge.change().currentPatchSetId()); RevCommit mergeTip = args.mergeTip.getCurrentTip(); args.rw.parseBody(mergeTip); String cherryPickCmtMsg = args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip); @@ -138,8 +142,8 @@ try { newCommit = args.mergeUtil.createCherryPickFromCommit( - args.repo, - args.inserter, + ctx.getInserter(), + ctx.getRepoView().getConfig(), args.mergeTip.getCurrentTip(), toMerge, committer, @@ -156,27 +160,27 @@ toMerge.setStatusCode(SKIPPED_IDENTICAL_TREE); return; } - ctx.addRefUpdate( - new ReceiveCommand(ObjectId.zeroId(), newCommit, newPatchSetId.toRefName())); + ctx.addRefUpdate(ObjectId.zeroId(), newCommit, newPatchSetId.toRefName()); } else { // Stale read of patch set is ok; see comments in RebaseChangeOp. PatchSet origPs = args.psUtil.get(ctx.getDb(), toMerge.getControl().getNotes(), toMerge.getPatchsetId()); rebaseOp = args.rebaseFactory - .create(toMerge.getControl(), origPs, args.mergeTip.getCurrentTip().name()) + .create(toMerge.getControl(), origPs, args.mergeTip.getCurrentTip()) .setFireRevisionCreated(false) // Bypass approval copier since SubmitStrategyOp copy all approvals // later anyway. .setCopyApprovals(false) - .setValidatePolicy(CommitValidators.Policy.NONE) + .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); + .setPostMessage(false) + .setMatchAuthorToCommitterDate(args.project.isMatchAuthorToCommitterDate()); try { rebaseOp.updateRepo(ctx); } catch (MergeConflictException | NoSuchChangeException e) { @@ -269,9 +273,9 @@ args.mergeUtil.mergeOneCommit( caller, caller, - args.repo, args.rw, - args.inserter, + ctx.getInserter(), + ctx.getRepoView().getConfig(), args.destBranch, mergeTip.getCurrentTip(), toMerge); @@ -287,27 +291,15 @@ args.alreadyAccepted.add(mergeTip.getCurrentTip()); } - private List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort) - throws IntegrationException { - try { - return new RebaseSorter( - args.rw, - args.mergeTip.getInitialTip(), - args.alreadyAccepted, - args.canMergeFlag, - args.internalChangeQuery) - .sort(toSort); - } catch (IOException e) { - throw new IntegrationException("Commit sorting failed", e); - } - } - static boolean dryRun( - SubmitDryRun.Arguments args, CodeReviewCommit mergeTip, CodeReviewCommit toMerge) + SubmitDryRun.Arguments args, + Repository repo, + CodeReviewCommit mergeTip, + CodeReviewCommit toMerge) throws IntegrationException { // Test for merge instead of cherry pick to avoid false negatives // on commit chains. return !args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge) - && args.mergeUtil.canMerge(args.mergeSorter, args.repo, mergeTip, toMerge); + && args.mergeUtil.canMerge(args.mergeSorter, repo, mergeTip, toMerge); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java index d375b6e..0d012e5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitDryRun.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.git.strategy; import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.server.git.CodeReviewCommit; @@ -108,7 +109,7 @@ repo, rw, mergeUtilFactory.create(getProject(destBranch)), - new MergeSorter(rw, alreadyAccepted, canMerge)); + new MergeSorter(rw, alreadyAccepted, canMerge, ImmutableSet.of(toMergeCommit))); switch (submitType) { case CHERRY_PICK: @@ -120,9 +121,9 @@ case MERGE_IF_NECESSARY: return MergeIfNecessary.dryRun(args, tipCommit, toMergeCommit); case REBASE_IF_NECESSARY: - return RebaseIfNecessary.dryRun(args, tipCommit, toMergeCommit); + return RebaseIfNecessary.dryRun(args, repo, tipCommit, toMergeCommit); case REBASE_ALWAYS: - return RebaseAlways.dryRun(args, tipCommit, toMergeCommit); + return RebaseAlways.dryRun(args, repo, tipCommit, toMergeCommit); default: String errorMsg = "No submit strategy for: " + submitType; log.error(errorMsg);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java index f721978..9892b6e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -18,12 +18,13 @@ import com.google.common.collect.ListMultimap; import com.google.common.collect.Sets; -import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.ChangeMessagesUtil; @@ -32,6 +33,7 @@ import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.change.RebaseChangeOp; +import com.google.gerrit.server.change.Submit.TestSubmitInput; import com.google.gerrit.server.extensions.events.ChangeMerged; import com.google.gerrit.server.git.CodeReviewCommit; import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk; @@ -43,6 +45,7 @@ import com.google.gerrit.server.git.MergeSorter; import com.google.gerrit.server.git.MergeTip; import com.google.gerrit.server.git.MergeUtil; +import com.google.gerrit.server.git.RebaseSorter; import com.google.gerrit.server.git.SubmoduleOp; import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.validators.OnSubmitValidators; @@ -53,17 +56,15 @@ import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.util.RequestId; +import com.google.inject.Inject; import com.google.inject.Module; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; -import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; -import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevFlag; @@ -92,14 +93,12 @@ CodeReviewRevWalk rw, IdentifiedUser caller, MergeTip mergeTip, - ObjectInserter inserter, - Repository repo, RevFlag canMergeFlag, ReviewDb db, Set<RevCommit> alreadyAccepted, Set<CodeReviewCommit> incoming, RequestId submissionId, - NotifyHandling notifyHandling, + SubmitInput submitInput, ListMultimap<RecipientType, Account.Id> accountsToNotify, SubmoduleOp submoduleOp, boolean dryrun); @@ -107,7 +106,6 @@ final AccountCache accountCache; final ApprovalsUtil approvalsUtil; - final BatchUpdate.Factory batchUpdateFactory; final ChangeControl.GenericFactory changeControlFactory; final ChangeMerged changeMerged; final ChangeMessagesUtil cmUtil; @@ -128,28 +126,25 @@ final CommitStatus commitStatus; final IdentifiedUser caller; final MergeTip mergeTip; - final ObjectInserter inserter; - final Repository repo; final RevFlag canMergeFlag; final ReviewDb db; final Set<RevCommit> alreadyAccepted; - final Set<CodeReviewCommit> incoming; final RequestId submissionId; final SubmitType submitType; - final NotifyHandling notifyHandling; + final SubmitInput submitInput; final ListMultimap<RecipientType, Account.Id> accountsToNotify; final SubmoduleOp submoduleOp; final ProjectState project; final MergeSorter mergeSorter; + final RebaseSorter rebaseSorter; final MergeUtil mergeUtil; final boolean dryrun; - @AssistedInject + @Inject Arguments( AccountCache accountCache, ApprovalsUtil approvalsUtil, - BatchUpdate.Factory batchUpdateFactory, ChangeControl.GenericFactory changeControlFactory, ChangeMerged changeMerged, ChangeMessagesUtil cmUtil, @@ -170,21 +165,18 @@ @Assisted CodeReviewRevWalk rw, @Assisted IdentifiedUser caller, @Assisted MergeTip mergeTip, - @Assisted ObjectInserter inserter, - @Assisted Repository repo, @Assisted RevFlag canMergeFlag, @Assisted ReviewDb db, @Assisted Set<RevCommit> alreadyAccepted, @Assisted Set<CodeReviewCommit> incoming, @Assisted RequestId submissionId, @Assisted SubmitType submitType, - @Assisted NotifyHandling notifyHandling, + @Assisted SubmitInput submitInput, @Assisted ListMultimap<RecipientType, Account.Id> accountsToNotify, @Assisted SubmoduleOp submoduleOp, @Assisted boolean dryrun) { this.accountCache = accountCache; this.approvalsUtil = approvalsUtil; - this.batchUpdateFactory = batchUpdateFactory; this.changeControlFactory = changeControlFactory; this.changeMerged = changeMerged; this.mergedSenderFactory = mergedSenderFactory; @@ -204,15 +196,12 @@ this.rw = rw; this.caller = caller; this.mergeTip = mergeTip; - this.inserter = inserter; - this.repo = repo; this.canMergeFlag = canMergeFlag; this.db = db; this.alreadyAccepted = alreadyAccepted; - this.incoming = incoming; this.submissionId = submissionId; this.submitType = submitType; - this.notifyHandling = notifyHandling; + this.submitInput = submitInput; this.accountsToNotify = accountsToNotify; this.submoduleOp = submoduleOp; this.dryrun = dryrun; @@ -222,7 +211,15 @@ projectCache.get(destBranch.getParentKey()), "project not found: %s", destBranch.getParentKey()); - this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag); + this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag, incoming); + this.rebaseSorter = + new RebaseSorter( + rw, + mergeTip.getInitialTip(), + alreadyAccepted, + canMergeFlag, + internalChangeQuery, + incoming); this.mergeUtil = mergeUtilFactory.create(project); this.onSubmitValidatorsFactory = onSubmitValidatorsFactory; } @@ -260,12 +257,21 @@ List<CodeReviewCommit> difference = new ArrayList<>(Sets.difference(toMerge, added)); Collections.reverse(difference); for (CodeReviewCommit c : difference) { - bu.addOp(c.change().getId(), new ImplicitIntegrateOp(args, c)); + Change.Id id = c.change().getId(); + bu.addOp(id, new ImplicitIntegrateOp(args, c)); + maybeAddTestHelperOp(bu, id); } // Then ops for explicitly merged changes for (SubmitStrategyOp op : ops) { bu.addOp(op.getId(), op); + maybeAddTestHelperOp(bu, op.getId()); + } + } + + private void maybeAddTestHelperOp(BatchUpdate bu, Change.Id changeId) { + if (args.submitInput instanceof TestSubmitInput) { + bu.addOp(changeId, new TestHelperOp(changeId, args)); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java index fc4817d..7678623 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -15,8 +15,8 @@ package com.google.gerrit.server.git.strategy; import com.google.common.collect.ListMultimap; -import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.extensions.api.changes.SubmitInput; import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; @@ -32,8 +32,6 @@ import com.google.inject.Inject; import com.google.inject.Singleton; import java.util.Set; -import org.eclipse.jgit.lib.ObjectInserter; -import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevFlag; import org.slf4j.Logger; @@ -54,9 +52,7 @@ public SubmitStrategy create( SubmitType submitType, ReviewDb db, - Repository repo, CodeReviewRevWalk rw, - ObjectInserter inserter, RevFlag canMergeFlag, Set<RevCommit> alreadyAccepted, Set<CodeReviewCommit> incoming, @@ -65,7 +61,7 @@ MergeTip mergeTip, CommitStatus commitStatus, RequestId submissionId, - NotifyHandling notifyHandling, + SubmitInput submitInput, ListMultimap<RecipientType, Account.Id> accountsToNotify, SubmoduleOp submoduleOp, boolean dryrun) @@ -78,14 +74,12 @@ rw, caller, mergeTip, - inserter, - repo, canMergeFlag, db, alreadyAccepted, incoming, submissionId, - notifyHandling, + submitInput, accountsToNotify, submoduleOp, dryrun);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java index 89bd560..a3163c3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyOp.java
@@ -53,7 +53,6 @@ import com.google.gwtorm.server.OrmException; import java.io.IOException; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -62,7 +61,6 @@ import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.transport.ReceiveCommand; import org.slf4j.Logger; @@ -79,7 +77,8 @@ private ObjectId mergeResultRev; private PatchSet mergedPatchSet; private Change updatedChange; - private CodeReviewCommit alreadyMerged; + private CodeReviewCommit alreadyMergedCommit; + private boolean changeAlreadyMerged; protected SubmitStrategyOp(SubmitStrategy.Arguments args, CodeReviewCommit toMerge) { this.args = args; @@ -105,14 +104,20 @@ @Override public final void updateRepo(RepoContext ctx) throws Exception { logDebug("{}#updateRepo for change {}", getClass().getSimpleName(), toMerge.change().getId()); + checkState( + ctx.getRevWalk() == args.rw, + "SubmitStrategyOp requires callers to call BatchUpdate#setRepository with exactly the same" + + " CodeReviewRevWalk instance from the SubmitStrategy.Arguments: %s != %s", + ctx.getRevWalk(), + args.rw); // Run the submit strategy implementation and record the merge tip state so // we can create the ref update. CodeReviewCommit tipBefore = args.mergeTip.getCurrentTip(); - alreadyMerged = getAlreadyMergedCommit(ctx); - if (alreadyMerged == null) { + alreadyMergedCommit = getAlreadyMergedCommit(ctx); + if (alreadyMergedCommit == null) { updateRepoImpl(ctx); } else { - logDebug("Already merged as {}", alreadyMerged.name()); + logDebug("Already merged as {}", alreadyMergedCommit.name()); } CodeReviewCommit tipAfter = args.mergeTip.getCurrentTip(); @@ -162,19 +167,20 @@ } CodeReviewRevWalk rw = (CodeReviewRevWalk) ctx.getRevWalk(); Change.Id id = getId(); + String refPrefix = id.toRefPrefix(); - Collection<Ref> refs = ctx.getRepository().getRefDatabase().getRefs(id.toRefPrefix()).values(); + Map<String, ObjectId> refs = ctx.getRepoView().getRefs(refPrefix); List<CodeReviewCommit> commits = new ArrayList<>(refs.size()); - for (Ref ref : refs) { - PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName()); + for (Map.Entry<String, ObjectId> e : refs.entrySet()) { + PatchSet.Id psId = PatchSet.Id.fromRef(refPrefix + e.getKey()); if (psId == null) { continue; } try { - CodeReviewCommit c = rw.parseCommit(ref.getObjectId()); + CodeReviewCommit c = rw.parseCommit(e.getValue()); c.setPatchsetId(psId); commits.add(c); - } catch (MissingObjectException | IncorrectObjectTypeException e) { + } catch (MissingObjectException | IncorrectObjectTypeException ex) { continue; // Bogus ref, can't be merged into tip so we don't care. } } @@ -214,8 +220,23 @@ PatchSet.Id oldPsId = checkNotNull(toMerge.getPatchsetId()); PatchSet.Id newPsId; - if (alreadyMerged != null) { - alreadyMerged.setControl(ctx.getControl()); + if (ctx.getChange().getStatus() == Change.Status.MERGED) { + // Either another thread won a race, or we are retrying a whole topic submission after one + // repo failed with lock failure. + if (alreadyMergedCommit == null) { + logDebug( + "Change is already merged according to its status, but we were unable to find it" + + " merged into the current tip ({})", + args.mergeTip.getCurrentTip().name()); + } else { + logDebug("Change is already merged"); + } + changeAlreadyMerged = true; + return false; + } + + if (alreadyMergedCommit != null) { + alreadyMergedCommit.setControl(ctx.getControl()); mergedPatchSet = getOrCreateAlreadyMergedPatchSet(ctx); newPsId = mergedPatchSet.getId(); } else { @@ -258,12 +279,12 @@ setApproval(ctx, args.caller); mergeResultRev = - alreadyMerged == null + alreadyMergedCommit == null ? args.mergeTip.getMergeResults().get(commit) // Our fixup code is not smart enough to find a merge commit // corresponding to the merge result. This results in a different // ChangeMergedEvent in the fixup case, but we'll just live with that. - : alreadyMerged; + : alreadyMergedCommit; try { setMerged(ctx, message(ctx, commit, s)); } catch (OrmException err) { @@ -279,13 +300,13 @@ private PatchSet getOrCreateAlreadyMergedPatchSet(ChangeContext ctx) throws IOException, OrmException { - PatchSet.Id psId = alreadyMerged.getPatchsetId(); + PatchSet.Id psId = alreadyMergedCommit.getPatchsetId(); logDebug("Fixing up already-merged patch set {}", psId); PatchSet prevPs = args.psUtil.current(ctx.getDb(), ctx.getNotes()); - ctx.getRevWalk().parseBody(alreadyMerged); + ctx.getRevWalk().parseBody(alreadyMergedCommit); ctx.getChange() .setCurrentPatchSet( - psId, alreadyMerged.getShortMessage(), ctx.getChange().getOriginalSubject()); + psId, alreadyMergedCommit.getShortMessage(), ctx.getChange().getOriginalSubject()); PatchSet existing = args.psUtil.get(ctx.getDb(), ctx.getNotes(), psId); if (existing != null) { logDebug("Patch set row exists, only updating change"); @@ -295,20 +316,21 @@ // a patch set ref. Fix up the database. Note that this uses the current // user as the uploader, which is as good a guess as any. List<String> groups = - prevPs != null ? prevPs.getGroups() : GroupCollector.getDefaultGroups(alreadyMerged); + prevPs != null ? prevPs.getGroups() : GroupCollector.getDefaultGroups(alreadyMergedCommit); return args.psUtil.insert( ctx.getDb(), ctx.getRevWalk(), ctx.getUpdate(psId), psId, - alreadyMerged, + alreadyMergedCommit, false, groups, null, null); } - private void setApproval(ChangeContext ctx, IdentifiedUser user) throws OrmException { + private void setApproval(ChangeContext ctx, IdentifiedUser user) + throws OrmException, IOException { Change.Id id = ctx.getChange().getId(); List<SubmitRecord> records = args.commitStatus.getSubmitRecords(id); PatchSet.Id oldPsId = toMerge.getPatchsetId(); @@ -330,11 +352,12 @@ } private LabelNormalizer.Result approve(ChangeContext ctx, ChangeUpdate update) - throws OrmException { + throws OrmException, IOException { PatchSet.Id psId = update.getPatchSetId(); Map<PatchSetApproval.Key, PatchSetApproval> byKey = new HashMap<>(); for (PatchSetApproval psa : - args.approvalsUtil.byPatchSet(ctx.getDb(), ctx.getControl(), psId)) { + args.approvalsUtil.byPatchSet( + ctx.getDb(), ctx.getControl(), psId, ctx.getRevWalk(), ctx.getRepoView().getConfig())) { byKey.put(psa.getKey(), psa); } @@ -477,6 +500,18 @@ @Override public final void postUpdate(Context ctx) throws Exception { + if (changeAlreadyMerged) { + // TODO(dborowitz): This is suboptimal behavior in the presence of retries: postUpdate steps + // will never get run for changes that submitted successfully on any but the final attempt. + // This is primarily a temporary workaround for the fact that the submitter field is not + // populated in the changeAlreadyMerged case. + // + // If we naively execute postUpdate even if the change is already merged when updateChange + // being, then we are subject to a race where postUpdate steps are run twice if two submit + // processes run at the same time. + logDebug("Skipping post-update steps for change {}", getId()); + return; + } postUpdateImpl(ctx); if (command != null) { @@ -503,7 +538,7 @@ ctx.getProject(), getId(), submitter.getAccountId(), - args.notifyHandling, + args.submitInput.notify, args.accountsToNotify) .sendAsync(); } catch (Exception e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/TestHelperOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/TestHelperOp.java new file mode 100644 index 0000000..8d95045 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/TestHelperOp.java
@@ -0,0 +1,58 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.git.strategy; + +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.server.change.Submit.TestSubmitInput; +import com.google.gerrit.server.update.BatchUpdateOp; +import com.google.gerrit.server.update.RepoContext; +import com.google.gerrit.server.util.RequestId; +import java.io.IOException; +import java.util.Queue; +import org.eclipse.jgit.lib.ObjectId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class TestHelperOp implements BatchUpdateOp { + private static final Logger log = LoggerFactory.getLogger(TestHelperOp.class); + + private final Change.Id changeId; + private final TestSubmitInput input; + private final RequestId submissionId; + + TestHelperOp(Change.Id changeId, SubmitStrategy.Arguments args) { + this.changeId = changeId; + this.input = (TestSubmitInput) args.submitInput; + this.submissionId = args.submissionId; + } + + @Override + public void updateRepo(RepoContext ctx) throws IOException { + Queue<Boolean> q = input.generateLockFailures; + if (q != null && !q.isEmpty() && q.remove()) { + logDebug("Adding bogus ref update to trigger lock failure, via change {}", changeId); + ctx.addRefUpdate( + ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), + ObjectId.zeroId(), + "refs/test/" + getClass().getSimpleName()); + } + } + + private void logDebug(String msg, Object... args) { + if (log.isDebugEnabled()) { + log.debug(submissionId + msg, args); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java index 24ff379..bffe382 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java
@@ -14,30 +14,35 @@ package com.google.gerrit.server.git.validators; +import com.google.common.collect.ImmutableList; import com.google.gerrit.server.validators.ValidationException; -import java.util.Collections; import java.util.List; public class CommitValidationException extends ValidationException { private static final long serialVersionUID = 1L; - private final List<CommitValidationMessage> messages; + private final ImmutableList<CommitValidationMessage> messages; + + public CommitValidationException(String reason, CommitValidationMessage message) { + super(reason); + this.messages = ImmutableList.of(message); + } public CommitValidationException(String reason, List<CommitValidationMessage> messages) { super(reason); - this.messages = messages; + this.messages = ImmutableList.copyOf(messages); } public CommitValidationException(String reason) { super(reason); - this.messages = Collections.emptyList(); + this.messages = ImmutableList.of(); } public CommitValidationException(String reason, Throwable why) { super(reason, why); - this.messages = Collections.emptyList(); + this.messages = ImmutableList.of(); } - public List<CommitValidationMessage> getMessages() { + public ImmutableList<CommitValidationMessage> getMessages() { return messages; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java index 630dd32..bdb0db9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -17,18 +17,23 @@ import static com.google.gerrit.reviewdb.client.Change.CHANGE_ID_PATTERN; import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG; import static com.google.gerrit.server.git.ReceiveCommits.NEW_PATCHSET; +import static java.util.stream.Collectors.toList; import com.google.common.base.CharMatcher; import com.google.common.collect.ImmutableList; import com.google.gerrit.common.FooterConstants; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.PageLinks; +import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo; import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.AccountConfig; import com.google.gerrit.server.account.WatchConfig; +import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker; import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.GerritServerConfig; @@ -36,7 +41,11 @@ import com.google.gerrit.server.git.BanCommit; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.git.ValidationError; +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.ProjectControl; +import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.project.RefControl; import com.google.gerrit.server.ssh.SshInfo; import com.google.gerrit.server.util.MagicBranch; @@ -49,9 +58,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.regex.Pattern; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.notes.NoteMap; @@ -59,6 +70,7 @@ import org.eclipse.jgit.revwalk.FooterLine; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.util.SystemReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,26 +78,13 @@ public class CommitValidators { private static final Logger log = LoggerFactory.getLogger(CommitValidators.class); - public enum Policy { - /** Use {@link Factory#forGerritCommits}. */ - GERRIT, - - /** Use {@link Factory#forReceiveCommits}. */ - RECEIVE_COMMITS, - - /** Use {@link Factory#forMergedCommits}. */ - MERGED, - - /** Do not validate commits. */ - NONE - } - @Singleton public static class Factory { private final PersonIdent gerritIdent; private final String canonicalWebUrl; private final DynamicSet<CommitValidationListener> pluginValidators; private final AllUsersName allUsers; + private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker; private final String installCommitMsgHookCommand; @Inject @@ -94,67 +93,59 @@ @CanonicalWebUrl @Nullable String canonicalWebUrl, @GerritServerConfig Config cfg, DynamicSet<CommitValidationListener> pluginValidators, - AllUsersName allUsers) { + AllUsersName allUsers, + ExternalIdsConsistencyChecker externalIdsConsistencyChecker) { this.gerritIdent = gerritIdent; this.canonicalWebUrl = canonicalWebUrl; this.pluginValidators = pluginValidators; this.allUsers = allUsers; + this.externalIdsConsistencyChecker = externalIdsConsistencyChecker; this.installCommitMsgHookCommand = cfg != null ? cfg.getString("gerrit", null, "installCommitMsgHookCommand") : null; } - public CommitValidators create( - Policy policy, RefControl refControl, SshInfo sshInfo, Repository repo) throws IOException { - switch (policy) { - case RECEIVE_COMMITS: - return forReceiveCommits(refControl, sshInfo, repo); - case GERRIT: - return forGerritCommits(refControl, sshInfo, repo); - case MERGED: - return forMergedCommits(refControl); - case NONE: - return none(); - default: - throw new IllegalArgumentException("unspported policy: " + policy); - } - } - - private CommitValidators forReceiveCommits( - RefControl refControl, SshInfo sshInfo, Repository repo) throws IOException { - try (RevWalk rw = new RevWalk(repo)) { - NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(repo, rw); - return new CommitValidators( - ImmutableList.of( - new UploadMergesPermissionValidator(refControl), - new AmendedGerritMergeCommitValidationListener(refControl, gerritIdent), - new AuthorUploaderValidator(refControl, canonicalWebUrl), - new CommitterUploaderValidator(refControl, canonicalWebUrl), - new SignedOffByValidator(refControl), - new ChangeIdValidator( - refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo), - new ConfigValidator(refControl, repo, allUsers), - new BannedCommitsValidator(rejectCommits), - new PluginCommitValidationListener(pluginValidators), - new BlockExternalIdUpdateListener(allUsers))); - } - } - - private CommitValidators forGerritCommits( - RefControl refControl, SshInfo sshInfo, Repository repo) { + public CommitValidators forReceiveCommits( + PermissionBackend.ForRef perm, + RefControl refctl, + SshInfo sshInfo, + Repository repo, + RevWalk rw) + throws IOException { + NoteMap rejectCommits = BanCommit.loadRejectCommitsMap(repo, rw); + IdentifiedUser user = refctl.getUser().asIdentifiedUser(); return new CommitValidators( ImmutableList.of( - new UploadMergesPermissionValidator(refControl), - new AmendedGerritMergeCommitValidationListener(refControl, gerritIdent), - new AuthorUploaderValidator(refControl, canonicalWebUrl), - new SignedOffByValidator(refControl), - new ChangeIdValidator( - refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo), - new ConfigValidator(refControl, repo, allUsers), + new UploadMergesPermissionValidator(perm), + new AmendedGerritMergeCommitValidationListener(perm, gerritIdent), + new AuthorUploaderValidator(user, perm, canonicalWebUrl), + new CommitterUploaderValidator(user, perm, canonicalWebUrl), + new SignedOffByValidator(user, perm, refctl.getProjectControl().getProjectState()), + new ChangeIdValidator(refctl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo), + new ConfigValidator(refctl, rw, allUsers), + new BannedCommitsValidator(rejectCommits), new PluginCommitValidationListener(pluginValidators), - new BlockExternalIdUpdateListener(allUsers))); + new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker), + new AccountValidator(allUsers))); } - private CommitValidators forMergedCommits(RefControl refControl) { + public CommitValidators forGerritCommits( + PermissionBackend.ForRef perm, RefControl refctl, SshInfo sshInfo, RevWalk rw) { + IdentifiedUser user = refctl.getUser().asIdentifiedUser(); + return new CommitValidators( + ImmutableList.of( + new UploadMergesPermissionValidator(perm), + new AmendedGerritMergeCommitValidationListener(perm, gerritIdent), + new AuthorUploaderValidator(user, perm, canonicalWebUrl), + new SignedOffByValidator(user, perm, refctl.getProjectControl().getProjectState()), + new ChangeIdValidator(refctl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo), + new ConfigValidator(refctl, rw, allUsers), + new PluginCommitValidationListener(pluginValidators), + new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker), + new AccountValidator(allUsers))); + } + + public CommitValidators forMergedCommits(PermissionBackend.ForRef perm, RefControl refControl) { + IdentifiedUser user = refControl.getUser().asIdentifiedUser(); // Generally only include validators that are based on permissions of the // user creating a change for a merged commit; generally exclude // validators that would require amending the change in order to correct. @@ -170,13 +161,9 @@ // formats, so we play it safe and exclude them. return new CommitValidators( ImmutableList.of( - new UploadMergesPermissionValidator(refControl), - new AuthorUploaderValidator(refControl, canonicalWebUrl), - new CommitterUploaderValidator(refControl, canonicalWebUrl))); - } - - private CommitValidators none() { - return new CommitValidators(ImmutableList.<CommitValidationListener>of()); + new UploadMergesPermissionValidator(perm), + new AuthorUploaderValidator(user, perm, canonicalWebUrl), + new CommitterUploaderValidator(user, perm, canonicalWebUrl))); } } @@ -282,8 +269,7 @@ || NEW_PATCHSET.matcher(event.command.getRefName()).matches(); } - private CommitValidationMessage getMissingChangeIdErrorMsg( - final String errMsg, final RevCommit c) { + private CommitValidationMessage getMissingChangeIdErrorMsg(String errMsg, RevCommit c) { StringBuilder sb = new StringBuilder(); sb.append("ERROR: ").append(errMsg); @@ -354,12 +340,12 @@ /** If this is the special project configuration branch, validate the config. */ public static class ConfigValidator implements CommitValidationListener { private final RefControl refControl; - private final Repository repo; + private final RevWalk rw; private final AllUsersName allUsers; - public ConfigValidator(RefControl refControl, Repository repo, AllUsersName allUsers) { + public ConfigValidator(RefControl refControl, RevWalk rw, AllUsersName allUsers) { this.refControl = refControl; - this.repo = repo; + this.rw = rw; this.allUsers = allUsers; } @@ -373,7 +359,7 @@ try { ProjectConfig cfg = new ProjectConfig(receiveEvent.project.getNameKey()); - cfg.load(repo, receiveEvent.command.getNewId()); + cfg.load(rw, receiveEvent.command.getNewId()); if (!cfg.getValidationErrors().isEmpty()) { addError("Invalid project configuration:", messages); for (ValidationError err : cfg.getValidationErrors()) { @@ -401,7 +387,7 @@ if (accountId != null) { try { WatchConfig wc = new WatchConfig(accountId); - wc.load(repo, receiveEvent.command.getNewId()); + wc.load(rw, receiveEvent.command.getNewId()); if (!wc.getValidationErrors().isEmpty()) { addError("Invalid project configuration:", messages); for (ValidationError err : wc.getValidationErrors()) { @@ -427,21 +413,29 @@ } } - /** Require permission to upload merges. */ + /** Require permission to upload merge commits. */ public static class UploadMergesPermissionValidator implements CommitValidationListener { - private final RefControl refControl; + private final PermissionBackend.ForRef perm; - public UploadMergesPermissionValidator(RefControl refControl) { - this.refControl = refControl; + public UploadMergesPermissionValidator(PermissionBackend.ForRef perm) { + this.perm = perm; } @Override public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent) throws CommitValidationException { - if (receiveEvent.commit.getParentCount() > 1 && !refControl.canUploadMerges()) { - throw new CommitValidationException("you are not allowed to upload merges"); + if (receiveEvent.commit.getParentCount() <= 1) { + return Collections.emptyList(); } - return Collections.emptyList(); + try { + perm.check(RefPermission.MERGE); + return Collections.emptyList(); + } catch (AuthException e) { + throw new CommitValidationException("you are not allowed to upload merges"); + } catch (PermissionBackendException e) { + log.error("cannot check MERGE", e); + throw new CommitValidationException("internal auth error"); + } } } @@ -472,37 +466,50 @@ } public static class SignedOffByValidator implements CommitValidationListener { - private final RefControl refControl; + private final IdentifiedUser user; + private final PermissionBackend.ForRef perm; + private final ProjectState state; - public SignedOffByValidator(RefControl refControl) { - this.refControl = refControl; + public SignedOffByValidator( + IdentifiedUser user, PermissionBackend.ForRef perm, ProjectState state) { + this.user = user; + this.perm = perm; + this.state = state; } @Override public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent) throws CommitValidationException { - IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser(); - final PersonIdent committer = receiveEvent.commit.getCommitterIdent(); - final PersonIdent author = receiveEvent.commit.getAuthorIdent(); - final ProjectControl projectControl = refControl.getProjectControl(); + if (!state.isUseSignedOffBy()) { + return Collections.emptyList(); + } - if (projectControl.getProjectState().isUseSignedOffBy()) { - boolean sboAuthor = false; - boolean sboCommitter = false; - boolean sboMe = false; - for (final FooterLine footer : receiveEvent.commit.getFooterLines()) { - if (footer.matches(FooterKey.SIGNED_OFF_BY)) { - final String e = footer.getEmailAddress(); - if (e != null) { - sboAuthor |= author.getEmailAddress().equals(e); - sboCommitter |= committer.getEmailAddress().equals(e); - sboMe |= currentUser.hasEmailAddress(e); - } + RevCommit commit = receiveEvent.commit; + PersonIdent committer = commit.getCommitterIdent(); + PersonIdent author = commit.getAuthorIdent(); + + boolean sboAuthor = false; + boolean sboCommitter = false; + boolean sboMe = false; + for (FooterLine footer : commit.getFooterLines()) { + if (footer.matches(FooterKey.SIGNED_OFF_BY)) { + String e = footer.getEmailAddress(); + if (e != null) { + sboAuthor |= author.getEmailAddress().equals(e); + sboCommitter |= committer.getEmailAddress().equals(e); + sboMe |= user.hasEmailAddress(e); } } - if (!sboAuthor && !sboCommitter && !sboMe && !refControl.canForgeCommitter()) { + } + if (!sboAuthor && !sboCommitter && !sboMe) { + try { + perm.check(RefPermission.FORGE_COMMITTER); + } catch (AuthException denied) { throw new CommitValidationException( "not Signed-off-by author/committer/uploader in commit message footer"); + } catch (PermissionBackendException e) { + log.error("cannot check FORGE_COMMITTER", e); + throw new CommitValidationException("internal auth error"); } } return Collections.emptyList(); @@ -511,56 +518,69 @@ /** Require that author matches the uploader. */ public static class AuthorUploaderValidator implements CommitValidationListener { - private final RefControl refControl; + private final IdentifiedUser user; + private final PermissionBackend.ForRef perm; private final String canonicalWebUrl; - public AuthorUploaderValidator(RefControl refControl, String canonicalWebUrl) { - this.refControl = refControl; + public AuthorUploaderValidator( + IdentifiedUser user, PermissionBackend.ForRef perm, String canonicalWebUrl) { + this.user = user; + this.perm = perm; this.canonicalWebUrl = canonicalWebUrl; } @Override public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent) throws CommitValidationException { - IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser(); - final PersonIdent author = receiveEvent.commit.getAuthorIdent(); - - if (!currentUser.hasEmailAddress(author.getEmailAddress()) && !refControl.canForgeAuthor()) { - List<CommitValidationMessage> messages = new ArrayList<>(); - - messages.add( - getInvalidEmailError( - receiveEvent.commit, "author", author, currentUser, canonicalWebUrl)); - throw new CommitValidationException("invalid author", messages); + PersonIdent author = receiveEvent.commit.getAuthorIdent(); + if (user.hasEmailAddress(author.getEmailAddress())) { + return Collections.emptyList(); } - return Collections.emptyList(); + try { + perm.check(RefPermission.FORGE_AUTHOR); + return Collections.emptyList(); + } catch (AuthException e) { + throw new CommitValidationException( + "invalid author", + invalidEmail(receiveEvent.commit, "author", author, user, canonicalWebUrl)); + } catch (PermissionBackendException e) { + log.error("cannot check FORGE_AUTHOR", e); + throw new CommitValidationException("internal auth error"); + } } } /** Require that committer matches the uploader. */ public static class CommitterUploaderValidator implements CommitValidationListener { - private final RefControl refControl; + private final IdentifiedUser user; + private final PermissionBackend.ForRef perm; private final String canonicalWebUrl; - public CommitterUploaderValidator(RefControl refControl, String canonicalWebUrl) { - this.refControl = refControl; + public CommitterUploaderValidator( + IdentifiedUser user, PermissionBackend.ForRef perm, String canonicalWebUrl) { + this.user = user; + this.perm = perm; this.canonicalWebUrl = canonicalWebUrl; } @Override public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent) throws CommitValidationException { - IdentifiedUser currentUser = refControl.getUser().asIdentifiedUser(); - final PersonIdent committer = receiveEvent.commit.getCommitterIdent(); - if (!currentUser.hasEmailAddress(committer.getEmailAddress()) - && !refControl.canForgeCommitter()) { - List<CommitValidationMessage> messages = new ArrayList<>(); - messages.add( - getInvalidEmailError( - receiveEvent.commit, "committer", committer, currentUser, canonicalWebUrl)); - throw new CommitValidationException("invalid committer", messages); + PersonIdent committer = receiveEvent.commit.getCommitterIdent(); + if (user.hasEmailAddress(committer.getEmailAddress())) { + return Collections.emptyList(); } - return Collections.emptyList(); + try { + perm.check(RefPermission.FORGE_COMMITTER); + return Collections.emptyList(); + } catch (AuthException e) { + throw new CommitValidationException( + "invalid committer", + invalidEmail(receiveEvent.commit, "committer", committer, user, canonicalWebUrl)); + } catch (PermissionBackendException e) { + log.error("cannot check FORGE_COMMITTER", e); + throw new CommitValidationException("internal auth error"); + } } } @@ -570,25 +590,30 @@ */ public static class AmendedGerritMergeCommitValidationListener implements CommitValidationListener { + private final PermissionBackend.ForRef perm; private final PersonIdent gerritIdent; - private final RefControl refControl; public AmendedGerritMergeCommitValidationListener( - final RefControl refControl, final PersonIdent gerritIdent) { - this.refControl = refControl; + PermissionBackend.ForRef perm, PersonIdent gerritIdent) { + this.perm = perm; this.gerritIdent = gerritIdent; } @Override public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent) throws CommitValidationException { - final PersonIdent author = receiveEvent.commit.getAuthorIdent(); - + PersonIdent author = receiveEvent.commit.getAuthorIdent(); if (receiveEvent.commit.getParentCount() > 1 && author.getName().equals(gerritIdent.getName()) - && author.getEmailAddress().equals(gerritIdent.getEmailAddress()) - && !refControl.canForgeGerritServerIdentity()) { - throw new CommitValidationException("do not amend merges not made by you"); + && author.getEmailAddress().equals(gerritIdent.getEmailAddress())) { + try { + perm.check(RefPermission.FORGE_SERVER); + } catch (AuthException denied) { + throw new CommitValidationException("do not amend merges not made by you"); + } catch (PermissionBackendException e) { + log.error("cannot check FORGE_SERVER", e); + throw new CommitValidationException("internal auth error"); + } } return Collections.emptyList(); } @@ -619,11 +644,14 @@ } } - /** Blocks any update to refs/meta/external-ids */ - public static class BlockExternalIdUpdateListener implements CommitValidationListener { + /** Validates updates to refs/meta/external-ids. */ + public static class ExternalIdUpdateListener implements CommitValidationListener { private final AllUsersName allUsers; + private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker; - public BlockExternalIdUpdateListener(AllUsersName allUsers) { + public ExternalIdUpdateListener( + AllUsersName allUsers, ExternalIdsConsistencyChecker externalIdsConsistencyChecker) { + this.externalIdsConsistencyChecker = externalIdsConsistencyChecker; this.allUsers = allUsers; } @@ -632,13 +660,86 @@ throws CommitValidationException { if (allUsers.equals(receiveEvent.project.getNameKey()) && RefNames.REFS_EXTERNAL_IDS.equals(receiveEvent.refName)) { - throw new CommitValidationException("not allowed to update " + RefNames.REFS_EXTERNAL_IDS); + try { + List<ConsistencyProblemInfo> problems = + externalIdsConsistencyChecker.check(receiveEvent.commit); + List<CommitValidationMessage> msgs = + problems + .stream() + .map( + p -> + new CommitValidationMessage( + p.message, p.status == ConsistencyProblemInfo.Status.ERROR)) + .collect(toList()); + if (msgs.stream().anyMatch(m -> m.isError())) { + throw new CommitValidationException("invalid external IDs", msgs); + } + return msgs; + } catch (IOException e) { + String m = "error validating external IDs"; + log.warn(m, e); + throw new CommitValidationException(m, e); + } } return Collections.emptyList(); } } - private static CommitValidationMessage getInvalidEmailError( + /** Rejects updates to 'account.config' in user branches. */ + public static class AccountValidator implements CommitValidationListener { + private final AllUsersName allUsers; + + public AccountValidator(AllUsersName allUsers) { + this.allUsers = allUsers; + } + + @Override + public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent) + throws CommitValidationException { + if (!allUsers.equals(receiveEvent.project.getNameKey())) { + return Collections.emptyList(); + } + + if (receiveEvent.command.getRefName().startsWith(MagicBranch.NEW_CHANGE)) { + // no validation on push for review, will be checked on submit by + // MergeValidators.AccountValidator + return Collections.emptyList(); + } + + Account.Id accountId = Account.Id.fromRef(receiveEvent.refName); + if (accountId == null) { + return Collections.emptyList(); + } + + try { + ObjectId newBlobId = getAccountConfigBlobId(receiveEvent.revWalk, receiveEvent.commit); + + ObjectId oldId = receiveEvent.command.getOldId(); + ObjectId oldBlobId = + !ObjectId.zeroId().equals(oldId) + ? getAccountConfigBlobId(receiveEvent.revWalk, oldId) + : null; + if (!Objects.equals(oldBlobId, newBlobId)) { + throw new CommitValidationException("account update not allowed"); + } + } catch (IOException e) { + String m = String.format("Validating update for account %s failed", accountId.get()); + log.error(m, e); + throw new CommitValidationException(m, e); + } + return Collections.emptyList(); + } + + private ObjectId getAccountConfigBlobId(RevWalk rw, ObjectId id) throws IOException { + RevCommit commit = rw.parseCommit(id); + try (TreeWalk tw = + TreeWalk.forPath(rw.getObjectReader(), AccountConfig.ACCOUNT_CONFIG, commit.getTree())) { + return tw != null ? tw.getObjectId(0) : null; + } + } + } + + private static CommitValidationMessage invalidEmail( RevCommit c, String type, PersonIdent who,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java index 150965c..af9f6d5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -19,27 +19,43 @@ import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.registration.DynamicMap.Entry; import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.AccountConfig; import com.google.gerrit.server.config.AllProjectsName; +import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.config.PluginConfig; import com.google.gerrit.server.config.ProjectConfigEntry; import com.google.gerrit.server.git.CodeReviewCommit; import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; 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.gwtorm.server.OrmException; import com.google.inject.Inject; +import com.google.inject.Provider; import java.io.IOException; import java.util.List; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Repository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class MergeValidators { + private static final Logger log = LoggerFactory.getLogger(MergeValidators.class); + private final DynamicSet<MergeValidationListener> mergeValidationListeners; private final ProjectConfigValidator.Factory projectConfigValidatorFactory; + private final AccountValidator.Factory accountValidatorFactory; public interface Factory { MergeValidators create(); @@ -48,9 +64,11 @@ @Inject MergeValidators( DynamicSet<MergeValidationListener> mergeValidationListeners, - ProjectConfigValidator.Factory projectConfigValidatorFactory) { + ProjectConfigValidator.Factory projectConfigValidatorFactory, + AccountValidator.Factory accountValidatorFactory) { this.mergeValidationListeners = mergeValidationListeners; this.projectConfigValidatorFactory = projectConfigValidatorFactory; + this.accountValidatorFactory = accountValidatorFactory; } public void validatePreMerge( @@ -64,7 +82,8 @@ List<MergeValidationListener> validators = ImmutableList.of( new PluginMergeValidationListener(mergeValidationListeners), - projectConfigValidatorFactory.create()); + projectConfigValidatorFactory.create(), + accountValidatorFactory.create()); for (MergeValidationListener validator : validators) { validator.onPreMerge(repo, commit, destProject, destBranch, patchSetId, caller); @@ -93,6 +112,7 @@ private final AllProjectsName allProjectsName; private final ProjectCache projectCache; + private final PermissionBackend permissionBackend; private final DynamicMap<ProjectConfigEntry> pluginConfigEntries; public interface Factory { @@ -103,9 +123,11 @@ public ProjectConfigValidator( AllProjectsName allProjectsName, ProjectCache projectCache, + PermissionBackend permissionBackend, DynamicMap<ProjectConfigEntry> pluginConfigEntries) { this.allProjectsName = allProjectsName; this.projectCache = projectCache; + this.permissionBackend = permissionBackend; this.pluginConfigEntries = pluginConfigEntries; } @@ -132,8 +154,13 @@ } } else { if (!oldParent.equals(newParent)) { - if (!caller.getCapabilities().canAdministrateServer()) { + try { + permissionBackend.user(caller).check(GlobalPermission.ADMINISTRATE_SERVER); + } catch (AuthException e) { throw new MergeValidationException(SET_BY_ADMIN); + } catch (PermissionBackendException e) { + log.warn("Cannot check ADMINISTRATE_SERVER", e); + throw new MergeValidationException("validation unavailable"); } if (projectCache.get(newParent) == null) { @@ -194,4 +221,59 @@ } } } + + public static class AccountValidator implements MergeValidationListener { + public interface Factory { + AccountValidator create(); + } + + private final Provider<ReviewDb> dbProvider; + private final AllUsersName allUsersName; + private final ChangeData.Factory changeDataFactory; + + @Inject + public AccountValidator( + Provider<ReviewDb> dbProvider, + AllUsersName allUsersName, + ChangeData.Factory changeDataFactory) { + this.dbProvider = dbProvider; + this.allUsersName = allUsersName; + this.changeDataFactory = changeDataFactory; + } + + @Override + public void onPreMerge( + Repository repo, + CodeReviewCommit commit, + ProjectState destProject, + Branch.NameKey destBranch, + PatchSet.Id patchSetId, + IdentifiedUser caller) + throws MergeValidationException { + if (!allUsersName.equals(destProject.getProject().getNameKey()) + || Account.Id.fromRef(destBranch.get()) == null) { + return; + } + + if (commit.getParentCount() > 1) { + // for merge commits we cannot ensure that the 'account.config' file is not modified, since + // for merge commits file modifications that come in through the merge don't appear in the + // file list that is returned by ChangeData#currentFilePaths() + throw new MergeValidationException("cannot submit merge commit to user branch"); + } + + ChangeData cd = + changeDataFactory.create( + dbProvider.get(), destProject.getProject().getNameKey(), patchSetId.getParentKey()); + try { + if (cd.currentFilePaths().contains(AccountConfig.ACCOUNT_CONFIG)) { + throw new MergeValidationException( + String.format("update of %s not allowed", AccountConfig.ACCOUNT_CONFIG)); + } + } catch (OrmException e) { + log.error("Cannot validate account update", e); + throw new MergeValidationException("account validation unavailable"); + } + } + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java index da3c123..a626998 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java
@@ -11,15 +11,20 @@ // WITHOUT 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.git.validators; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableMap; import com.google.gerrit.extensions.annotations.ExtensionPoint; import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.reviewdb.client.Project.NameKey; +import com.google.gerrit.server.git.RefCache; +import com.google.gerrit.server.update.ChainedReceiveCommands; import com.google.gerrit.server.validators.ValidationException; -import java.util.Map; -import org.eclipse.jgit.lib.ObjectReader; -import org.eclipse.jgit.lib.Repository; +import java.io.IOException; +import java.util.Optional; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.ReceiveCommand; @@ -37,41 +42,58 @@ public interface OnSubmitValidationListener { class Arguments { private Project.NameKey project; - private Repository repository; - private ObjectReader objectReader; - private Map<String, ReceiveCommand> commands; + private RevWalk rw; + private ImmutableMap<String, ReceiveCommand> commands; + private RefCache refs; - public Arguments( - NameKey project, - Repository repository, - ObjectReader objectReader, - Map<String, ReceiveCommand> commands) { - this.project = project; - this.repository = repository; - this.objectReader = objectReader; - this.commands = commands; + /** + * @param project project. + * @param rw revwalk that can read unflushed objects from {@code refs}. + * @param commands commands to be executed. + */ + Arguments(Project.NameKey project, RevWalk rw, ChainedReceiveCommands commands) { + this.project = checkNotNull(project); + this.rw = checkNotNull(rw); + this.refs = checkNotNull(commands); + this.commands = ImmutableMap.copyOf(commands.getCommands()); } + /** Get the project name for this operation. */ public Project.NameKey getProject() { return project; } - /** @return a read only repository */ - public Repository getRepository() { - return repository; - } - - public RevWalk newRevWalk() { - return new RevWalk(objectReader); + /** + * Get a revwalk for this operation. + * + * <p>This instance is able to read all objects mentioned in {@link #getCommands()} and {@link + * #getRef(String)}. + * + * @return open revwalk. + */ + public RevWalk getRevWalk() { + return rw; } /** - * @return a map from ref to op on it covering all ref ops to be performed on this repository as - * part of ongoing submit operation. + * @return a map from ref to commands covering all ref operations to be performed on this + * repository as part of the ongoing submit operation. */ - public Map<String, ReceiveCommand> getCommands() { + public ImmutableMap<String, ReceiveCommand> getCommands() { return commands; } + + /** + * Get a ref from the repository. + * + * @param name ref name; can be any ref, not just the ones mentioned in {@link #getCommands()}. + * @return latest value of a ref in the repository, as if all commands from {@link + * #getCommands()} had already been applied. + * @throws IOException if an error occurred reading the ref. + */ + public Optional<ObjectId> getRef(String name) throws IOException { + return refs.get(name); + } } /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java index 55935d1..460889c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java
@@ -11,18 +11,18 @@ // WITHOUT 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.git.validators; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.git.IntegrationException; import com.google.gerrit.server.git.validators.OnSubmitValidationListener.Arguments; +import com.google.gerrit.server.update.ChainedReceiveCommands; import com.google.gerrit.server.validators.ValidationException; import com.google.inject.Inject; -import java.util.Map; import org.eclipse.jgit.lib.ObjectReader; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.transport.ReceiveCommand; +import org.eclipse.jgit.revwalk.RevWalk; public class OnSubmitValidators { public interface Factory { @@ -37,14 +37,12 @@ } public void validate( - Project.NameKey project, - Repository repo, - ObjectReader objectReader, - Map<String, ReceiveCommand> commands) + Project.NameKey project, ObjectReader objectReader, ChainedReceiveCommands commands) throws IntegrationException { - try { - for (OnSubmitValidationListener listener : this.listeners) { - listener.preBranchUpdate(new Arguments(project, repo, objectReader, commands)); + try (RevWalk rw = new RevWalk(objectReader)) { + Arguments args = new Arguments(project, rw, commands); + for (OnSubmitValidationListener listener : listeners) { + listener.preBranchUpdate(args); } } catch (ValidationException e) { throw new IntegrationException(e.getMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java index 80792c3..8047a99a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -14,11 +14,19 @@ package com.google.gerrit.server.git.validators; import com.google.common.base.Predicate; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.gerrit.extensions.registration.DynamicSet; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.events.RefReceivedEvent; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.validators.ValidationException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -42,15 +50,21 @@ update.getExpectedOldObjectId(), update.getNewObjectId(), update.getName(), type); } - private final RefReceivedEvent event; + private final PermissionBackend.WithUser perm; + private final AllUsersName allUsersName; private final DynamicSet<RefOperationValidationListener> refOperationValidationListeners; + private final RefReceivedEvent event; @Inject RefOperationValidators( + PermissionBackend permissionBackend, + AllUsersName allUsersName, DynamicSet<RefOperationValidationListener> refOperationValidationListeners, @Assisted Project project, @Assisted IdentifiedUser user, @Assisted ReceiveCommand cmd) { + this.perm = permissionBackend.user(user); + this.allUsersName = allUsersName; this.refOperationValidationListeners = refOperationValidationListeners; event = new RefReceivedEvent(); event.command = cmd; @@ -59,11 +73,13 @@ } public List<ValidationMessage> validateForRefOperation() throws RefOperationValidationException { - List<ValidationMessage> messages = new ArrayList<>(); boolean withException = false; + List<RefOperationValidationListener> listeners = new ArrayList<>(); + listeners.add(new DisallowCreationAndDeletionOfUserBranches(perm, allUsersName)); + refOperationValidationListeners.forEach(l -> listeners.add(l)); try { - for (RefOperationValidationListener listener : refOperationValidationListeners) { + for (RefOperationValidationListener listener : listeners) { messages.addAll(listener.onRefOperation(event)); } } catch (ValidationException e) { @@ -95,4 +111,44 @@ return input.isError(); } } + + private static class DisallowCreationAndDeletionOfUserBranches + implements RefOperationValidationListener { + private final PermissionBackend.WithUser perm; + private final AllUsersName allUsersName; + + DisallowCreationAndDeletionOfUserBranches( + PermissionBackend.WithUser perm, AllUsersName allUsersName) { + this.perm = perm; + this.allUsersName = allUsersName; + } + + @Override + public List<ValidationMessage> onRefOperation(RefReceivedEvent refEvent) + throws ValidationException { + if (refEvent.project.getNameKey().equals(allUsersName) + && (refEvent.command.getRefName().startsWith(RefNames.REFS_USERS) + && !refEvent.command.getRefName().equals(RefNames.REFS_USERS_DEFAULT))) { + if (refEvent.command.getType().equals(ReceiveCommand.Type.CREATE)) { + try { + perm.check(GlobalPermission.ACCESS_DATABASE); + } catch (AuthException | PermissionBackendException e) { + throw new ValidationException("Not allowed to create user branch."); + } + if (Account.Id.fromRef(refEvent.command.getRefName()) == null) { + throw new ValidationException( + String.format( + "Not allowed to create non-user branch under %s.", RefNames.REFS_USERS)); + } + } else if (refEvent.command.getType().equals(ReceiveCommand.Type.DELETE)) { + try { + perm.check(GlobalPermission.ACCESS_DATABASE); + } catch (AuthException | PermissionBackendException e) { + throw new ValidationException("Not allowed to delete user branch."); + } + } + } + return ImmutableList.of(); + } + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java index 5c1a292..04be41e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -52,6 +52,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton public class AddMembers implements RestModifyView<GroupResource, Input> { @@ -115,7 +116,7 @@ @Override public List<AccountInfo> apply(GroupResource resource, Input input) throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException, - IOException { + IOException, ConfigInvalidException { AccountGroup internalGroup = resource.toAccountGroup(); if (internalGroup == null) { throw new MethodNotAllowedException(); @@ -143,7 +144,8 @@ } Account findAccount(String nameOrEmailOrId) - throws AuthException, UnprocessableEntityException, OrmException, IOException { + throws AuthException, UnprocessableEntityException, OrmException, IOException, + ConfigInvalidException { try { return accounts.parse(nameOrEmailOrId).getAccount(); } catch (UnprocessableEntityException e) { @@ -235,7 +237,7 @@ @Override public AccountInfo apply(GroupResource resource, PutMember.Input input) throws AuthException, MethodNotAllowedException, ResourceNotFoundException, OrmException, - IOException { + IOException, ConfigInvalidException { AddMembers.Input in = new AddMembers.Input(); in._oneMember = id; try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java index 4d78a7d..af92acf 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
@@ -16,6 +16,7 @@ import com.google.common.base.MoreObjects; import com.google.common.base.Strings; +import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.GroupDescription; import com.google.gerrit.common.data.GroupDescriptions; @@ -54,6 +55,7 @@ import java.util.Collections; import java.util.List; import java.util.Locale; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.PersonIdent; @@ -114,7 +116,7 @@ @Override public GroupInfo apply(TopLevelResource resource, GroupInput input) throws AuthException, BadRequestException, UnprocessableEntityException, - ResourceConflictException, OrmException, IOException { + ResourceConflictException, OrmException, IOException, ConfigInvalidException { if (input == null) { input = new GroupInput(); } @@ -188,7 +190,8 @@ GroupUUID.make( createGroupArgs.getGroupName(), self.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone())); - AccountGroup group = new AccountGroup(createGroupArgs.getGroup(), groupId, uuid); + AccountGroup group = + new AccountGroup(createGroupArgs.getGroup(), groupId, uuid, TimeUtil.nowTs()); group.setVisibleToAll(createGroupArgs.visibleToAll); if (createGroupArgs.ownerGroupId != null) { AccountGroup ownerGroup = groupCache.get(createGroupArgs.ownerGroupId);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java index f88460b..ce287d0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DbGroupMemberAuditListener.java
@@ -123,7 +123,7 @@ public void onDeleteGroupsFromGroup(Account.Id me, Collection<AccountGroupById> removed) { final List<AccountGroupByIdAud> auditUpdates = new ArrayList<>(); try (ReviewDb db = schema.open()) { - for (final AccountGroupById g : removed) { + for (AccountGroupById g : removed) { AccountGroupByIdAud audit = null; for (AccountGroupByIdAud a : db.accountGroupByIdAud().byGroupInclude(g.getGroupId(), g.getIncludeUUID())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java index 9f612bf..1e1008c7a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
@@ -75,7 +75,7 @@ getIncludedGroups(internalGroup.getId()); final List<AccountGroupById> toRemove = new ArrayList<>(); - for (final String includedGroup : input.groups) { + for (String includedGroup : input.groups) { GroupDescription.Basic d = groupsCollection.parse(includedGroup); if (!control.canRemoveGroup()) { throw new AuthException(String.format("Cannot delete group: %s", d.getName())); @@ -90,7 +90,7 @@ if (!toRemove.isEmpty()) { writeAudits(toRemove); db.get().accountGroupById().delete(toRemove); - for (final AccountGroupById g : toRemove) { + for (AccountGroupById g : toRemove) { groupIncludeCache.evictParentGroupsOf(g.getIncludeUUID()); } groupIncludeCache.evictSubgroupsOf(internalGroup.getGroupUUID()); @@ -99,7 +99,7 @@ return Response.none(); } - private Map<AccountGroup.UUID, AccountGroupById> getIncludedGroups(final AccountGroup.Id groupId) + private Map<AccountGroup.UUID, AccountGroupById> getIncludedGroups(AccountGroup.Id groupId) throws OrmException { final Map<AccountGroup.UUID, AccountGroupById> groups = new HashMap<>(); for (AccountGroupById g : db.get().accountGroupById().byGroup(groupId)) { @@ -108,7 +108,7 @@ return groups; } - private void writeAudits(final List<AccountGroupById> toRemoved) { + private void writeAudits(List<AccountGroupById> toRemoved) { final Account.Id me = self.get().getAccountId(); auditService.dispatchDeleteGroupsFromGroup(me, toRemoved); } @@ -121,7 +121,7 @@ private final Provider<DeleteIncludedGroups> delete; @Inject - DeleteIncludedGroup(final Provider<DeleteIncludedGroups> delete) { + DeleteIncludedGroup(Provider<DeleteIncludedGroups> delete) { this.delete = delete; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java index e365ce3..6be46d6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
@@ -38,6 +38,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.eclipse.jgit.errors.ConfigInvalidException; @Singleton public class DeleteMembers implements RestModifyView<GroupResource, Input> { @@ -64,7 +65,7 @@ @Override public Response<?> apply(GroupResource resource, Input input) throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException, - IOException { + IOException, ConfigInvalidException { AccountGroup internalGroup = resource.toAccountGroup(); if (internalGroup == null) { throw new MethodNotAllowedException(); @@ -75,7 +76,7 @@ final Map<Account.Id, AccountGroupMember> members = getMembers(internalGroup.getId()); final List<AccountGroupMember> toRemove = new ArrayList<>(); - for (final String nameOrEmail : input.members) { + for (String nameOrEmail : input.members) { Account a = accounts.parse(nameOrEmail).getAccount(); if (!control.canRemoveMember()) { @@ -90,22 +91,22 @@ writeAudits(toRemove); db.get().accountGroupMembers().delete(toRemove); - for (final AccountGroupMember m : toRemove) { + for (AccountGroupMember m : toRemove) { accountCache.evict(m.getAccountId()); } return Response.none(); } - private void writeAudits(final List<AccountGroupMember> toRemove) { + private void writeAudits(List<AccountGroupMember> toRemove) { final Account.Id me = self.get().getAccountId(); auditService.dispatchDeleteAccountsFromGroup(me, toRemove); } - private Map<Account.Id, AccountGroupMember> getMembers(final AccountGroup.Id groupId) + private Map<Account.Id, AccountGroupMember> getMembers(AccountGroup.Id groupId) throws OrmException { final Map<Account.Id, AccountGroupMember> members = new HashMap<>(); - for (final AccountGroupMember m : db.get().accountGroupMembers().byGroup(groupId)) { + for (AccountGroupMember m : db.get().accountGroupMembers().byGroup(groupId)) { members.put(m.getAccountId(), m); } return members; @@ -118,14 +119,14 @@ private final Provider<DeleteMembers> delete; @Inject - DeleteMember(final Provider<DeleteMembers> delete) { + DeleteMember(Provider<DeleteMembers> delete) { this.delete = delete; } @Override public Response<?> apply(MemberResource resource, Input input) throws AuthException, MethodNotAllowedException, UnprocessableEntityException, OrmException, - IOException { + IOException, ConfigInvalidException { AddMembers.Input in = new AddMembers.Input(); in._oneMember = resource.getMember().getAccountId().toString(); return delete.get().apply(resource, in);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java index 43e70ff..0be167d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
@@ -107,6 +107,7 @@ info.owner = o.getName(); } } + info.createdOn = g.getCreatedOn(); } return info;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java index 9bf14e7..d32afa2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -253,9 +253,9 @@ List<AccountGroup> groupList; if (!projects.isEmpty()) { Map<AccountGroup.UUID, AccountGroup> groups = new HashMap<>(); - for (final ProjectControl projectControl : projects) { + for (ProjectControl projectControl : projects) { final Set<GroupReference> groupsRefs = projectControl.getAllGroups(); - for (final GroupReference groupRef : groupsRefs) { + for (GroupReference groupRef : groupsRefs) { final AccountGroup group = groupCache.get(groupRef.getUUID()); if (group != null) { groups.put(group.getGroupUUID(), group); @@ -294,7 +294,7 @@ limit <= 0 ? 10 : Math.min(limit, 10))); List<GroupInfo> groupInfos = Lists.newArrayListWithCapacity(groupRefs.size()); - for (final GroupReference ref : groupRefs) { + for (GroupReference ref : groupRefs) { GroupDescription.Basic desc = groupBackend.get(ref.getUUID()); if (desc != null) { groupInfos.add(json.addOptions(options).format(desc)); @@ -356,7 +356,6 @@ private List<AccountGroup> filterGroups(Collection<AccountGroup> groups) { List<AccountGroup> filteredGroups = new ArrayList<>(groups.size()); - boolean isAdmin = identifiedUser.get().getCapabilities().canAdministrateServer(); for (AccountGroup group : groups) { if (!Strings.isNullOrEmpty(matchSubstring)) { if (!group @@ -372,13 +371,11 @@ if (!groupsToInspect.isEmpty() && !groupsToInspect.contains(group.getGroupUUID())) { continue; } - if (!isAdmin) { - GroupControl c = groupControlFactory.controlFor(group); - if (!c.isVisible()) { - continue; - } + + GroupControl c = groupControlFactory.controlFor(group); + if (c.isVisible()) { + filteredGroups.add(group); } - filteredGroups.add(group); } Collections.sort(filteredGroups, new GroupComparator()); return filteredGroups;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java index 8e2c925..b24d094 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
@@ -61,7 +61,7 @@ } @Override - public List<AccountInfo> apply(final GroupResource resource) + public List<AccountInfo> apply(GroupResource resource) throws MethodNotAllowedException, OrmException { if (resource.toAccountGroup() == null) { throw new MethodNotAllowedException(); @@ -83,7 +83,7 @@ } private Map<Account.Id, AccountInfo> getMembers( - final AccountGroup.UUID groupUUID, final HashSet<AccountGroup.UUID> seenGroups) + final AccountGroup.UUID groupUUID, HashSet<AccountGroup.UUID> seenGroups) throws OrmException { seenGroups.add(groupUUID); @@ -103,7 +103,7 @@ } if (groupDetail.members != null) { - for (final AccountGroupMember m : groupDetail.members) { + for (AccountGroupMember m : groupDetail.members) { if (!members.containsKey(m.getAccountId())) { members.put(m.getAccountId(), accountLoader.get(m.getAccountId())); } @@ -112,7 +112,7 @@ if (recursive) { if (groupDetail.includes != null) { - for (final AccountGroupById includedGroup : groupDetail.includes) { + for (AccountGroupById includedGroup : groupDetail.includes) { if (!seenGroups.contains(includedGroup.getIncludeUUID())) { members.putAll(getMembers(includedGroup.getIncludeUUID(), seenGroups)); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java index 8f4d65e..dbc0676 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
@@ -32,6 +32,8 @@ 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; @Singleton public class MembersCollection @@ -63,7 +65,8 @@ @Override public MemberResource parse(GroupResource parent, IdString id) - throws MethodNotAllowedException, AuthException, ResourceNotFoundException, OrmException { + throws MethodNotAllowedException, AuthException, ResourceNotFoundException, OrmException, + IOException, ConfigInvalidException { if (parent.toAccountGroup() == null) { throw new MethodNotAllowedException(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java index a368190..5c3cdf2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexConfig.java
@@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkArgument; import com.google.auto.value.AutoValue; +import java.util.function.IntConsumer; import org.eclipse.jgit.lib.Config; /** @@ -30,29 +31,61 @@ private static final int DEFAULT_MAX_TERMS = 1024; public static IndexConfig createDefault() { - return create(0, 0, DEFAULT_MAX_TERMS); + return builder().build(); } - public static IndexConfig fromConfig(Config cfg) { - return create( - cfg.getInt("index", null, "maxLimit", 0), - cfg.getInt("index", null, "maxPages", 0), - cfg.getInt("index", null, "maxTerms", 0)); + public static Builder fromConfig(Config cfg) { + Builder b = builder(); + setIfPresent(cfg, "maxLimit", b::maxLimit); + setIfPresent(cfg, "maxPages", b::maxPages); + setIfPresent(cfg, "maxTerms", b::maxTerms); + return b; } - public static IndexConfig create(int maxLimit, int maxPages, int maxTerms) { - return new AutoValue_IndexConfig( - checkLimit(maxLimit, "maxLimit", Integer.MAX_VALUE), - checkLimit(maxPages, "maxPages", Integer.MAX_VALUE), - checkLimit(maxTerms, "maxTerms", DEFAULT_MAX_TERMS)); - } - - private static int checkLimit(int limit, String name, int defaultValue) { - if (limit == 0) { - return defaultValue; + private static void setIfPresent(Config cfg, String name, IntConsumer setter) { + int n = cfg.getInt("index", null, name, 0); + if (n != 0) { + setter.accept(n); } + } + + public static Builder builder() { + return new AutoValue_IndexConfig.Builder() + .maxLimit(Integer.MAX_VALUE) + .maxPages(Integer.MAX_VALUE) + .maxTerms(DEFAULT_MAX_TERMS) + .separateChangeSubIndexes(false); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder maxLimit(int maxLimit); + + abstract int maxLimit(); + + public abstract Builder maxPages(int maxPages); + + abstract int maxPages(); + + public abstract Builder maxTerms(int maxTerms); + + abstract int maxTerms(); + + public abstract Builder separateChangeSubIndexes(boolean separate); + + abstract IndexConfig autoBuild(); + + public IndexConfig build() { + IndexConfig cfg = autoBuild(); + checkLimit(cfg.maxLimit(), "maxLimit"); + checkLimit(cfg.maxPages(), "maxPages"); + checkLimit(cfg.maxTerms(), "maxTerms"); + return cfg; + } + } + + private static void checkLimit(int limit, String name) { checkArgument(limit > 0, "%s must be positive: %s", name, limit); - return limit; } /** @@ -71,4 +104,9 @@ * for performance reasons. */ public abstract int maxTerms(); + + /** + * @return whether different subsets of changes may be stored in different physical sub-indexes. + */ + public abstract boolean separateChangeSubIndexes(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java index a8b423a..636cce6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.lifecycle.LifecycleModule; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.git.WorkQueue; @@ -108,6 +109,8 @@ bind(GroupIndexCollection.class); listener().to(GroupIndexCollection.class); factory(GroupIndexerImpl.Factory.class); + + DynamicSet.setOf(binder(), OnlineUpgradeListener.class); } @Provides
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java index e40015a..8d14931 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineReindexer.java
@@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.Lists; +import com.google.gerrit.extensions.registration.DynamicSet; import java.io.IOException; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @@ -26,16 +27,26 @@ public class OnlineReindexer<K, V, I extends Index<K, V>> { private static final Logger log = LoggerFactory.getLogger(OnlineReindexer.class); + private final String name; private final IndexCollection<K, V, I> indexes; private final SiteIndexer<K, V, I> batchIndexer; - private final int version; + private final int oldVersion; + private final int newVersion; + private final DynamicSet<OnlineUpgradeListener> listeners; private I index; private final AtomicBoolean running = new AtomicBoolean(); - public OnlineReindexer(IndexDefinition<K, V, I> def, int version) { + public OnlineReindexer( + IndexDefinition<K, V, I> def, + int oldVersion, + int newVersion, + DynamicSet<OnlineUpgradeListener> listeners) { + this.name = def.getName(); this.indexes = def.getIndexCollection(); this.batchIndexer = def.getSiteIndexer(); - this.version = version; + this.oldVersion = oldVersion; + this.newVersion = newVersion; + this.listeners = listeners; } public void start() { @@ -44,14 +55,21 @@ new Thread() { @Override public void run() { + boolean ok = false; try { reindex(); + ok = true; } finally { running.set(false); + if (!ok) { + for (OnlineUpgradeListener listener : listeners) { + listener.onFailure(name, oldVersion, newVersion); + } + } } } }; - t.setName(String.format("Reindex v%d-v%d", version(indexes.getSearchIndex()), version)); + t.setName(String.format("Reindex v%d-v%d", version(indexes.getSearchIndex()), newVersion)); t.start(); } } @@ -61,7 +79,7 @@ } public int getVersion() { - return version; + return newVersion; } private static int version(Index<?, ?> i) { @@ -69,9 +87,14 @@ } private void reindex() { + for (OnlineUpgradeListener listener : listeners) { + listener.onStart(name, oldVersion, newVersion); + } index = checkNotNull( - indexes.getWriteIndex(version), "not an active write schema version: %s", version); + indexes.getWriteIndex(newVersion), + "not an active write schema version: %s", + newVersion); log.info( "Starting online reindex from schema version {} to {}", version(indexes.getSearchIndex()), @@ -88,6 +111,9 @@ } log.info("Reindex to version {} complete", version(index)); activateIndex(); + for (OnlineUpgradeListener listener : listeners) { + listener.onSuccess(name, oldVersion, newVersion); + } } public void activateIndex() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgradeListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgradeListener.java new file mode 100644 index 0000000..a2d13fe --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgradeListener.java
@@ -0,0 +1,45 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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; + +/** Listener for online schema upgrade events. */ +public interface OnlineUpgradeListener { + /** + * Called before starting upgrading a single index. + * + * @param name index definition name. + * @param oldVersion old schema version. + * @param newVersion new schema version. + */ + void onStart(String name, int oldVersion, int newVersion); + + /** + * Called after successfully upgrading a single index. + * + * @param name index definition name. + * @param oldVersion old schema version. + * @param newVersion new schema version. + */ + void onSuccess(String name, int oldVersion, int newVersion); + + /** + * Called after failing to upgrade a single index. + * + * @param name index definition name. + * @param oldVersion old schema version. + * @param newVersion new schema version. + */ + void onFailure(String name, int oldVersion, int newVersion); +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgrader.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgrader.java new file mode 100644 index 0000000..9fc3aa9 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/OnlineUpgrader.java
@@ -0,0 +1,39 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 com.google.gerrit.extensions.events.LifecycleListener; +import com.google.inject.Inject; + +/** Listener to handle upgrading index schema versions at startup. */ +public class OnlineUpgrader implements LifecycleListener { + private final VersionManager versionManager; + + @Inject + OnlineUpgrader(VersionManager versionManager) { + this.versionManager = versionManager; + } + + @Override + public void start() { + versionManager.startOnlineUpgrade(); + } + + @Override + public void stop() { + // Do nothing; reindexing threadpools are shut down in another listener, and indexes are closed + // on demand by IndexCollection. + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java index 5e06242..0fae9c9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/Schema.java
@@ -176,7 +176,7 @@ * @param fillArgs arguments for filling fields. * @return all non-null field values from the object. */ - public final Iterable<Values<T>> buildFields(final T obj, final FillArgs fillArgs) { + public final Iterable<Values<T>> buildFields(T obj, FillArgs fillArgs) { return FluentIterable.from(fields.values()) .transform( new Function<FieldDef<T, ?>, Values<T>>() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/SchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SchemaDefinitions.java index 2bcf03a..261734d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/SchemaDefinitions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SchemaDefinitions.java
@@ -18,6 +18,8 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Iterables; +import com.google.gerrit.common.Nullable; /** * Definitions of the various schema versions over a given Gerrit data type. @@ -53,4 +55,12 @@ public final Schema<V> getLatest() { return schemas.lastEntry().getValue(); } + + @Nullable + public final Schema<V> getPrevious() { + if (schemas.size() <= 1) { + return null; + } + return Iterables.get(schemas.descendingMap().values(), 1); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java index 69e1cf1..0d84be7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/SiteIndexer.java
@@ -22,6 +22,7 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.util.concurrent.ExecutionException; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jgit.lib.ProgressMonitor; @@ -104,6 +105,9 @@ public void run() { try { future.get(); + } catch (RejectedExecutionException e) { + // Server shutdown, don't spam the logs. + failSilently(); } catch (ExecutionException | InterruptedException e) { fail(e); } catch (RuntimeException e) { @@ -119,6 +123,10 @@ } } + private void failSilently() { + ok.set(false); + } + private void fail(Throwable t) { log.error("Failed to index " + desc, t); ok.set(false);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java similarity index 75% rename from gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java rename to gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java index 33cca1e..697c9c2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/AbstractVersionManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/VersionManager.java
@@ -15,11 +15,13 @@ package com.google.gerrit.server.index; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.gerrit.extensions.events.LifecycleListener; -import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.index.IndexDefinition.IndexFactory; import com.google.inject.ProvisionException; @@ -31,7 +33,11 @@ import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Config; -public abstract class AbstractVersionManager implements LifecycleListener { +public abstract class VersionManager implements LifecycleListener { + public static boolean getOnlineUpgrade(Config cfg) { + return cfg.getBoolean("index", null, "onlineUpgrade", true); + } + public static class Version<V> { public final Schema<V> schema; public final int version; @@ -48,23 +54,29 @@ protected final boolean onlineUpgrade; protected final String runReindexMsg; protected final SitePaths sitePaths; + + private final DynamicSet<OnlineUpgradeListener> listeners; + + // The following fields must be accessed synchronized on this. protected final Map<String, IndexDefinition<?, ?, ?>> defs; protected final Map<String, OnlineReindexer<?, ?, ?>> reindexers; - protected AbstractVersionManager( - @GerritServerConfig Config cfg, + protected VersionManager( SitePaths sitePaths, - Collection<IndexDefinition<?, ?, ?>> defs) { + DynamicSet<OnlineUpgradeListener> listeners, + Collection<IndexDefinition<?, ?, ?>> defs, + boolean onlineUpgrade) { this.sitePaths = sitePaths; + this.listeners = listeners; this.defs = Maps.newHashMapWithExpectedSize(defs.size()); for (IndexDefinition<?, ?, ?> def : defs) { this.defs.put(def.getName(), def); } - reindexers = Maps.newHashMapWithExpectedSize(defs.size()); - onlineUpgrade = cfg.getBoolean("index", null, "onlineUpgrade", true); - runReindexMsg = - "No index versions ready; run java -jar " + this.reindexers = Maps.newHashMapWithExpectedSize(defs.size()); + this.onlineUpgrade = onlineUpgrade; + this.runReindexMsg = + "No index versions for index '%s' ready; run java -jar " + sitePaths.gerrit_war.toAbsolutePath() + " reindex"; } @@ -142,7 +154,7 @@ } } if (search == null) { - throw new ProvisionException(runReindexMsg); + throw new ProvisionException(String.format(runReindexMsg, def.getName())); } IndexFactory<K, V, I> factory = def.getIndexFactory(); @@ -162,11 +174,37 @@ synchronized (this) { if (!reindexers.containsKey(def.getName())) { int latest = write.get(0).version; - OnlineReindexer<K, V, I> reindexer = new OnlineReindexer<>(def, latest); + OnlineReindexer<K, V, I> reindexer = + new OnlineReindexer<>(def, search.version, latest, listeners); reindexers.put(def.getName(), reindexer); - if (onlineUpgrade && latest != search.version) { - reindexer.start(); - } + } + } + } + + synchronized void startOnlineUpgrade() { + checkState(onlineUpgrade, "online upgrade not enabled"); + for (IndexDefinition<?, ?, ?> def : defs.values()) { + String name = def.getName(); + IndexCollection<?, ?, ?> indexes = def.getIndexCollection(); + Index<?, ?> search = indexes.getSearchIndex(); + checkState( + 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()); + checkState( + !write.isEmpty(), + "no write indexes set for %s; should have been initialized at startup", + name); + int latestWriteVersion = write.get(0).getSchema().getVersion(); + + if (latestWriteVersion != searchVersion) { + OnlineReindexer<?, ?, ?> reindexer = reindexers.get(name); + checkState( + reindexer != null, + "no reindexer found for %s; should have been initialized at startup", + name); + reindexer.start(); } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java index 96aec3f..9258913 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountField.java
@@ -24,7 +24,7 @@ import com.google.common.collect.FluentIterable; import com.google.common.collect.Iterables; import com.google.gerrit.server.account.AccountState; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.index.FieldDef; import com.google.gerrit.server.index.SchemaUtil; import java.sql.Timestamp; @@ -76,6 +76,14 @@ .transform(String::toLowerCase) .toSet()); + public static final FieldDef<AccountState, String> PREFERRED_EMAIL = + prefix("preferredemail") + .build( + a -> { + String preferredEmail = a.getAccount().getPreferredEmail(); + return preferredEmail != null ? preferredEmail.toLowerCase() : null; + }); + public static final FieldDef<AccountState, Timestamp> REGISTERED = timestamp("registered").build(a -> a.getAccount().getRegisteredOn());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java index 011f1d1..279b36d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AccountSchemaDefinitions.java
@@ -22,21 +22,19 @@ public class AccountSchemaDefinitions extends SchemaDefinitions<AccountState> { @Deprecated - static final Schema<AccountState> V1 = + static final Schema<AccountState> V4 = schema( - AccountField.ID, AccountField.ACTIVE, AccountField.EMAIL, AccountField.EXTERNAL_ID, + AccountField.FULL_NAME, + AccountField.ID, AccountField.NAME_PART, AccountField.REGISTERED, - AccountField.USERNAME); + AccountField.USERNAME, + AccountField.WATCHED_PROJECT); - @Deprecated static final Schema<AccountState> V2 = schema(V1, AccountField.WATCHED_PROJECT); - - @Deprecated static final Schema<AccountState> V3 = schema(V2, AccountField.FULL_NAME); - - static final Schema<AccountState> V4 = schema(V3); + static final Schema<AccountState> V5 = schema(V4, AccountField.PREFERRED_EMAIL); public static final AccountSchemaDefinitions INSTANCE = new AccountSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java index 1b84e8e..a8cc8aa 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/account/AllAccountsIndexer.java
@@ -21,19 +21,17 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.account.Accounts; import com.google.gerrit.server.index.IndexExecutor; import com.google.gerrit.server.index.SiteIndexer; -import com.google.gwtorm.server.OrmException; -import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.Singleton; +import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -46,29 +44,29 @@ public class AllAccountsIndexer extends SiteIndexer<Account.Id, AccountState, AccountIndex> { private static final Logger log = LoggerFactory.getLogger(AllAccountsIndexer.class); - private final SchemaFactory<ReviewDb> schemaFactory; private final ListeningExecutorService executor; + private final Accounts accounts; private final AccountCache accountCache; @Inject AllAccountsIndexer( - SchemaFactory<ReviewDb> schemaFactory, @IndexExecutor(BATCH) ListeningExecutorService executor, + Accounts accounts, AccountCache accountCache) { - this.schemaFactory = schemaFactory; this.executor = executor; + this.accounts = accounts; this.accountCache = accountCache; } @Override - public SiteIndexer.Result indexAll(final AccountIndex index) { + public SiteIndexer.Result indexAll(AccountIndex index) { ProgressMonitor progress = new TextProgressMonitor(new PrintWriter(progressOut)); progress.start(2); Stopwatch sw = Stopwatch.createStarted(); List<Account.Id> ids; try { ids = collectAccounts(progress); - } catch (OrmException e) { + } catch (IOException e) { log.error("Error collecting accounts", e); return new SiteIndexer.Result(sw, false, 0, 0); } @@ -76,31 +74,28 @@ } private SiteIndexer.Result reindexAccounts( - final AccountIndex index, List<Account.Id> ids, ProgressMonitor progress) { + AccountIndex index, List<Account.Id> ids, ProgressMonitor progress) { progress.beginTask("Reindexing accounts", ids.size()); List<ListenableFuture<?>> futures = new ArrayList<>(ids.size()); AtomicBoolean ok = new AtomicBoolean(true); - final AtomicInteger done = new AtomicInteger(); - final AtomicInteger failed = new AtomicInteger(); + AtomicInteger done = new AtomicInteger(); + AtomicInteger failed = new AtomicInteger(); Stopwatch sw = Stopwatch.createStarted(); - for (final Account.Id id : ids) { - final String desc = "account " + id; + for (Account.Id id : ids) { + String desc = "account " + id; ListenableFuture<?> future = executor.submit( - new Callable<Void>() { - @Override - public Void call() throws Exception { - try { - accountCache.evict(id); - index.replace(accountCache.get(id)); - verboseWriter.println("Reindexed " + desc); - done.incrementAndGet(); - } catch (Exception e) { - failed.incrementAndGet(); - throw e; - } - return null; + () -> { + try { + accountCache.evict(id); + index.replace(accountCache.get(id)); + verboseWriter.println("Reindexed " + desc); + done.incrementAndGet(); + } catch (Exception e) { + failed.incrementAndGet(); + throw e; } + return null; }); addErrorListener(future, desc, progress, ok); futures.add(future); @@ -117,13 +112,12 @@ return new SiteIndexer.Result(sw, ok.get(), done.get(), failed.get()); } - private List<Account.Id> collectAccounts(ProgressMonitor progress) throws OrmException { + private List<Account.Id> collectAccounts(ProgressMonitor progress) throws IOException { progress.beginTask("Collecting accounts", ProgressMonitor.UNKNOWN); List<Account.Id> ids = new ArrayList<>(); - try (ReviewDb db = schemaFactory.open()) { - for (Account account : db.accounts().all()) { - ids.add(account.getId()); - } + for (Account.Id accountId : accounts.allIds()) { + ids.add(accountId); + progress.update(1); } progress.endTask(); return ids;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java index 638ff4c..35953b0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.index.change; +import static com.google.common.base.Preconditions.checkState; import static com.google.common.util.concurrent.Futures.successfulAsList; import static com.google.common.util.concurrent.Futures.transform; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; @@ -29,6 +30,7 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MultiProgressMonitor; @@ -43,14 +45,15 @@ import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Constants; @@ -95,16 +98,17 @@ } private static class ProjectHolder implements Comparable<ProjectHolder> { - private Project.NameKey name; - private int size; + final Project.NameKey name; + private final long size; - ProjectHolder(Project.NameKey name, int size) { + ProjectHolder(Project.NameKey name, long size) { this.name = name; this.size = size; } @Override public int compareTo(ProjectHolder other) { + // Sort projects based on size first to maximize utilization of threads early on. return ComparisonChain.start() .compare(other.size, size) .compare(other.name.get(), name.get()) @@ -121,7 +125,7 @@ Stopwatch sw = Stopwatch.createStarted(); for (Project.NameKey name : projectCache.all()) { try (Repository repo = repoManager.openRepository(name)) { - int size = ChangeNotes.Factory.scan(repo).size(); + long size = estimateSize(repo); changeCount += size; projects.add(new ProjectHolder(name, size)); } catch (IOException e) { @@ -136,23 +140,32 @@ return indexAll(index, projects); } - public SiteIndexer.Result indexAll(ChangeIndex index, Iterable<ProjectHolder> projects) { + private long estimateSize(Repository repo) throws IOException { + // Estimate size based on IDs that show up in ref names. This is not perfect, since patch set + // refs may exist for changes whose metadata was never successfully stored. But that's ok, as + // the estimate is just used as a heuristic for sorting projects. + return repo.getRefDatabase() + .getRefs(RefNames.REFS_CHANGES) + .values() + .stream() + .map(r -> Change.Id.fromRef(r.getName())) + .filter(Objects::nonNull) + .distinct() + .count(); + } + + private SiteIndexer.Result indexAll(ChangeIndex index, SortedSet<ProjectHolder> projects) { Stopwatch sw = Stopwatch.createStarted(); - final MultiProgressMonitor mpm = new MultiProgressMonitor(progressOut, "Reindexing changes"); - final Task projTask = - mpm.beginSubTask( - "projects", - (projects instanceof Collection) - ? ((Collection<?>) projects).size() - : MultiProgressMonitor.UNKNOWN); - final Task doneTask = - mpm.beginSubTask(null, totalWork >= 0 ? totalWork : MultiProgressMonitor.UNKNOWN); - final Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN); + MultiProgressMonitor mpm = new MultiProgressMonitor(progressOut, "Reindexing changes"); + Task projTask = mpm.beginSubTask("projects", projects.size()); + checkState(totalWork >= 0); + Task doneTask = mpm.beginSubTask(null, totalWork); + Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN); - final List<ListenableFuture<?>> futures = new ArrayList<>(); - final AtomicBoolean ok = new AtomicBoolean(true); + List<ListenableFuture<?>> futures = new ArrayList<>(); + AtomicBoolean ok = new AtomicBoolean(true); - for (final ProjectHolder project : projects) { + for (ProjectHolder project : projects) { ListenableFuture<?> future = executor.submit( reindexProject( @@ -195,11 +208,11 @@ } public Callable<Void> reindexProject( - final ChangeIndexer indexer, - final Project.NameKey project, - final Task done, - final Task failed, - final PrintWriter verboseWriter) { + ChangeIndexer indexer, + Project.NameKey project, + Task done, + Task failed, + PrintWriter verboseWriter) { return new Callable<Void>() { @Override public Void call() throws Exception { @@ -296,6 +309,9 @@ indexer.index(cd); done.update(1); verboseWriter.println("Reindexed change " + cd.getId()); + } catch (RejectedExecutionException e) { + // Server shutdown, don't spam the logs. + failSilently(); } catch (Exception e) { fail("Failed to index change " + cd.getId(), true, e); } @@ -322,5 +338,9 @@ verboseWriter.println(error); } + + private void failSilently() { + this.failed.update(1); + } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java index b8acadc..7a8cc72 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -40,12 +40,12 @@ import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; -import com.google.gerrit.reviewdb.client.Comment; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.server.OutputFormat; +import com.google.gerrit.server.ReviewerByEmailSet; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.StarredChangesUtil; import com.google.gerrit.server.index.FieldDef; @@ -53,6 +53,7 @@ import com.google.gerrit.server.index.SchemaUtil; import com.google.gerrit.server.index.change.StalenessChecker.RefState; import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern; +import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.notedb.ReviewerStateInternal; @@ -73,8 +74,12 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Objects; import java.util.Set; import java.util.function.Function; +import java.util.stream.Stream; +import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.revwalk.FooterLine; /** @@ -184,6 +189,24 @@ public static final FieldDef<ChangeData, Iterable<String>> REVIEWER = exact("reviewer2").stored().buildRepeatable(cd -> getReviewerFieldValues(cd.reviewers())); + /** Reviewer(s) associated with the change that do not have a gerrit account. */ + public static final FieldDef<ChangeData, Iterable<String>> REVIEWER_BY_EMAIL = + exact("reviewer_by_email") + .stored() + .buildRepeatable(cd -> getReviewerByEmailFieldValues(cd.reviewersByEmail())); + + /** Reviewer(s) modified during change's current WIP phase. */ + public static final FieldDef<ChangeData, Iterable<String>> PENDING_REVIEWER = + exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER) + .stored() + .buildRepeatable(cd -> getReviewerFieldValues(cd.pendingReviewers())); + + /** Reviewer(s) by email modified during change's current WIP phase. */ + public static final FieldDef<ChangeData, Iterable<String>> PENDING_REVIEWER_BY_EMAIL = + exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER_BY_EMAIL) + .stored() + .buildRepeatable(cd -> getReviewerByEmailFieldValues(cd.pendingReviewersByEmail())); + @VisibleForTesting static List<String> getReviewerFieldValues(ReviewerSet reviewers) { List<String> r = new ArrayList<>(reviewers.asTable().size() * 2); @@ -200,6 +223,27 @@ return state.toString() + ',' + id; } + @VisibleForTesting + static List<String> getReviewerByEmailFieldValues(ReviewerByEmailSet reviewersByEmail) { + List<String> r = new ArrayList<>(reviewersByEmail.asTable().size() * 2); + for (Table.Cell<ReviewerStateInternal, Address, Timestamp> c : + reviewersByEmail.asTable().cellSet()) { + String v = getReviewerByEmailFieldValue(c.getRowKey(), c.getColumnKey()); + r.add(v); + if (c.getColumnKey().getName() != null) { + // Add another entry without the name to provide search functionality on the email + Address emailOnly = new Address(c.getColumnKey().getEmail()); + r.add(getReviewerByEmailFieldValue(c.getRowKey(), emailOnly)); + } + r.add(v + ',' + c.getValue().getTime()); + } + return r; + } + + public static String getReviewerByEmailFieldValue(ReviewerStateInternal state, Address adr) { + return state.toString() + ',' + adr; + } + public static ReviewerSet parseReviewerFieldValues(Iterable<String> values) { ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Timestamp> b = ImmutableTable.builder(); @@ -220,6 +264,25 @@ return ReviewerSet.fromTable(b.build()); } + public static ReviewerByEmailSet parseReviewerByEmailFieldValues(Iterable<String> values) { + ImmutableTable.Builder<ReviewerStateInternal, Address, Timestamp> b = ImmutableTable.builder(); + for (String v : values) { + int f = v.indexOf(','); + if (f < 0) { + continue; + } + int l = v.lastIndexOf(','); + if (l == f) { + continue; + } + b.put( + ReviewerStateInternal.valueOf(v.substring(0, f)), + Address.parse(v.substring(f + 1, l)), + new Timestamp(Long.valueOf(v.substring(l + 1, v.length())))); + } + return ReviewerByEmailSet.fromTable(b.build()); + } + /** Commit ID of any patch set on the change, using prefix match. */ public static final FieldDef<ChangeData, Iterable<String>> COMMIT = prefix(ChangeQueryBuilder.FIELD_COMMIT).buildRepeatable(ChangeField::getRevisions); @@ -250,13 +313,8 @@ return Sets.newHashSet(a.trackingFooters.extract(footers).values()); }); - /** List of labels on the current patch set. */ - @Deprecated - public static final FieldDef<ChangeData, Iterable<String>> LABEL = - exact(ChangeQueryBuilder.FIELD_LABEL).buildRepeatable(cd -> getLabels(cd, false)); - /** List of labels on the current patch set including change owner votes. */ - public static final FieldDef<ChangeData, Iterable<String>> LABEL2 = + public static final FieldDef<ChangeData, Iterable<String>> LABEL = exact("label2").buildRepeatable(cd -> getLabels(cd, true)); private static Iterable<String> getLabels(ChangeData cd, boolean owners) throws OrmException { @@ -280,10 +338,36 @@ return SchemaUtil.getPersonParts(cd.getAuthor()); } + public static Set<String> getAuthorNameAndEmail(ChangeData cd) throws OrmException, IOException { + return getNameAndEmail(cd.getAuthor()); + } + public static Set<String> getCommitterParts(ChangeData cd) throws OrmException, IOException { return SchemaUtil.getPersonParts(cd.getCommitter()); } + public static Set<String> getCommitterNameAndEmail(ChangeData cd) + throws OrmException, IOException { + return getNameAndEmail(cd.getCommitter()); + } + + private static Set<String> getNameAndEmail(PersonIdent person) { + if (person == null) { + return ImmutableSet.of(); + } + + String name = person.getName().toLowerCase(Locale.US); + String email = person.getEmailAddress().toLowerCase(Locale.US); + + StringBuilder nameEmailBuilder = new StringBuilder(); + PersonIdent.appendSanitized(nameEmailBuilder, name); + nameEmailBuilder.append(" <"); + PersonIdent.appendSanitized(nameEmailBuilder, email); + nameEmailBuilder.append('>'); + + return ImmutableSet.of(name, email, nameEmailBuilder.toString()); + } + /** * The exact email address, or any part of the author name or email address, in the current patch * set. @@ -291,6 +375,11 @@ public static final FieldDef<ChangeData, Iterable<String>> AUTHOR = fullText(ChangeQueryBuilder.FIELD_AUTHOR).buildRepeatable(ChangeField::getAuthorParts); + /** The exact name, email address and NameEmail of the author. */ + public static final FieldDef<ChangeData, Iterable<String>> EXACT_AUTHOR = + exact(ChangeQueryBuilder.FIELD_EXACTAUTHOR) + .buildRepeatable(ChangeField::getAuthorNameAndEmail); + /** * The exact email address, or any part of the committer name or email address, in the current * patch set. @@ -298,6 +387,11 @@ public static final FieldDef<ChangeData, Iterable<String>> COMMITTER = fullText(ChangeQueryBuilder.FIELD_COMMITTER).buildRepeatable(ChangeField::getCommitterParts); + /** The exact name, email address, and NameEmail of the committer. */ + public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMITTER = + exact(ChangeQueryBuilder.FIELD_EXACTCOMMITTER) + .buildRepeatable(ChangeField::getCommitterNameAndEmail); + public static final ProtobufCodec<Change> CHANGE_CODEC = CodecFactory.encoder(Change.class); /** Serialized change object, used for pre-populating results. */ @@ -338,16 +432,11 @@ public static final FieldDef<ChangeData, Iterable<String>> COMMENT = fullText(ChangeQueryBuilder.FIELD_COMMENT) .buildRepeatable( - cd -> { - Set<String> r = new HashSet<>(); - for (Comment c : cd.publishedComments()) { - r.add(c.message); - } - for (ChangeMessage m : cd.messages()) { - r.add(m.getMessage()); - } - return r; - }); + cd -> + Stream.concat( + cd.publishedComments().stream().map(c -> c.message), + cd.messages().stream().map(ChangeMessage::getMessage)) + .collect(toSet())); /** Number of unresolved comments of the change. */ public static final FieldDef<ChangeData, Integer> UNRESOLVED_COMMENT_COUNT = @@ -385,22 +474,30 @@ intRange(ChangeQueryBuilder.FIELD_DELTA) .build(cd -> cd.changedLines().map(c -> c.insertions + c.deletions).orElse(null)); + /** Determines if this change is private. */ + public static final FieldDef<ChangeData, String> PRIVATE = + exact(ChangeQueryBuilder.FIELD_PRIVATE).build(cd -> cd.change().isPrivate() ? "1" : "0"); + + /** Determines if this change is work in progress. */ + public static final FieldDef<ChangeData, String> WIP = + exact(ChangeQueryBuilder.FIELD_WIP).build(cd -> cd.change().isWorkInProgress() ? "1" : "0"); + + /** Determines if this change has started review. */ + public static final FieldDef<ChangeData, String> STARTED = + exact(ChangeQueryBuilder.FIELD_STARTED) + .build(cd -> cd.change().hasReviewStarted() ? "1" : "0"); + /** Users who have commented on this change. */ public static final FieldDef<ChangeData, Iterable<Integer>> COMMENTBY = integer(ChangeQueryBuilder.FIELD_COMMENTBY) .buildRepeatable( - cd -> { - Set<Integer> r = new HashSet<>(); - for (ChangeMessage m : cd.messages()) { - if (m.getAuthor() != null) { - r.add(m.getAuthor().get()); - } - } - for (Comment c : cd.publishedComments()) { - r.add(c.author.getId().get()); - } - return r; - }); + cd -> + Stream.concat( + cd.messages().stream().map(ChangeMessage::getAuthor), + cd.publishedComments().stream().map(c -> c.author.getId())) + .filter(Objects::nonNull) + .map(Account.Id::get) + .collect(toSet())); /** Star labels on this change in the format: <account-id>:<label> */ public static final FieldDef<ChangeData, Iterable<String>> STAR = @@ -423,13 +520,8 @@ public static final FieldDef<ChangeData, Iterable<String>> GROUP = exact(ChangeQueryBuilder.FIELD_GROUP) .buildRepeatable( - cd -> { - Set<String> r = Sets.newHashSetWithExpectedSize(1); - for (PatchSet ps : cd.patchSets()) { - r.addAll(ps.getGroups()); - } - return r; - }); + cd -> + cd.patchSets().stream().flatMap(ps -> ps.getGroups().stream()).collect(toSet())); public static final ProtobufCodec<PatchSet> PATCH_SET_CODEC = CodecFactory.encoder(PatchSet.class); @@ -469,11 +561,7 @@ if (reviewedBy.isEmpty()) { return ImmutableSet.of(NOT_REVIEWED); } - List<Integer> result = new ArrayList<>(reviewedBy.size()); - for (Account.Id id : reviewedBy) { - result.add(id.get()); - } - return result; + return reviewedBy.stream().map(Account.Id::get).collect(toList()); }); // Submit rule options in this class should never use fastEvalLabels. This
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java index a9e1362..e1513b3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -20,10 +20,12 @@ import com.google.common.collect.Sets; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change.Status; +import com.google.gerrit.server.index.FieldDef; import com.google.gerrit.server.index.IndexConfig; import com.google.gerrit.server.index.IndexPredicate; import com.google.gerrit.server.index.IndexRewriter; import com.google.gerrit.server.index.QueryOptions; +import com.google.gerrit.server.index.Schema; import com.google.gerrit.server.query.AndPredicate; import com.google.gerrit.server.query.LimitPredicate; import com.google.gerrit.server.query.NotPredicate; @@ -188,6 +190,9 @@ // and included that in their limit computation. return new LimitPredicate<>(ChangeQueryBuilder.FIELD_LIMIT, opts.limit()); } else if (!isRewritePossible(in)) { + if (in instanceof IndexPredicate) { + throw new QueryParseException("Unsupported index predicate: " + in.toString()); + } return null; // magic to indicate "in" cannot be rewritten } @@ -226,7 +231,10 @@ return false; } IndexPredicate<ChangeData> p = (IndexPredicate<ChangeData>) in; - return index.getSchema().hasField(p.getField()); + + FieldDef<ChangeData, ?> def = p.getField(); + Schema<ChangeData> schema = index.getSchema(); + return schema.hasField(def); } private Predicate<ChangeData> partitionChildren(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java index 2ff04ae..ee45324 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexer.java
@@ -19,7 +19,6 @@ import com.google.common.base.Function; import com.google.common.util.concurrent.Atomics; -import com.google.common.util.concurrent.CheckedFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; @@ -73,7 +72,8 @@ ChangeIndexer create(ListeningExecutorService executor, ChangeIndexCollection indexes); } - public static CheckedFuture<?, IOException> allAsList( + @SuppressWarnings("deprecation") + public static com.google.common.util.concurrent.CheckedFuture<?, IOException> allAsList( List<? extends ListenableFuture<?>> futures) { // allAsList propagates the first seen exception, wrapped in // ExecutionException, so we can reuse the same mapper as for a single @@ -173,7 +173,9 @@ * @param id change to index. * @return future for the indexing task. */ - public CheckedFuture<?, IOException> indexAsync(Project.NameKey project, Change.Id id) { + @SuppressWarnings("deprecation") + public com.google.common.util.concurrent.CheckedFuture<?, IOException> indexAsync( + Project.NameKey project, Change.Id id) { return submit(new IndexTask(project, id)); } @@ -183,7 +185,8 @@ * @param ids changes to index. * @return future for completing indexing of all changes. */ - public CheckedFuture<?, IOException> indexAsync( + @SuppressWarnings("deprecation") + public com.google.common.util.concurrent.CheckedFuture<?, IOException> indexAsync( Project.NameKey project, Collection<Change.Id> ids) { List<ListenableFuture<?>> futures = new ArrayList<>(ids.size()); for (Change.Id id : ids) { @@ -277,7 +280,8 @@ * @param id change to delete. * @return future for the deleting task. */ - public CheckedFuture<?, IOException> deleteAsync(Change.Id id) { + @SuppressWarnings("deprecation") + public com.google.common.util.concurrent.CheckedFuture<?, IOException> deleteAsync(Change.Id id) { return submit(new DeleteTask(id)); } @@ -300,7 +304,9 @@ * @param id ID of the change to index. * @return future for reindexing the change; returns true if the change was stale. */ - public CheckedFuture<Boolean, IOException> reindexIfStale(Project.NameKey project, Change.Id id) { + @SuppressWarnings("deprecation") + public com.google.common.util.concurrent.CheckedFuture<Boolean, IOException> reindexIfStale( + Project.NameKey project, Change.Id id) { return submit(new ReindexIfStaleTask(project, id), batchExecutor); } @@ -324,11 +330,14 @@ return indexes != null ? indexes.getWriteIndexes() : Collections.singleton(index); } - private <T> CheckedFuture<T, IOException> submit(Callable<T> task) { + @SuppressWarnings("deprecation") + private <T> com.google.common.util.concurrent.CheckedFuture<T, IOException> submit( + Callable<T> task) { return submit(task, executor); } - private static <T> CheckedFuture<T, IOException> submit( + @SuppressWarnings("deprecation") + private static <T> com.google.common.util.concurrent.CheckedFuture<T, IOException> submit( Callable<T> task, ListeningExecutorService executor) { return Futures.makeChecked(Futures.nonCancellationPropagating(executor.submit(task)), MAPPER); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java index d988612..bb0118b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -22,75 +22,68 @@ public class ChangeSchemaDefinitions extends SchemaDefinitions<ChangeData> { @Deprecated - static final Schema<ChangeData> V32 = + static final Schema<ChangeData> V39 = schema( - ChangeField.LEGACY_ID, + ChangeField.ADDED, + ChangeField.APPROVAL, + ChangeField.ASSIGNEE, + ChangeField.AUTHOR, + ChangeField.CHANGE, + ChangeField.COMMENT, + ChangeField.COMMENTBY, + ChangeField.COMMIT, + ChangeField.COMMITTER, + ChangeField.COMMIT_MESSAGE, + ChangeField.DELETED, + ChangeField.DELTA, + ChangeField.DRAFTBY, + ChangeField.EDITBY, + ChangeField.EXACT_COMMIT, + ChangeField.EXACT_TOPIC, + ChangeField.FILE_PART, + ChangeField.FUZZY_TOPIC, + ChangeField.GROUP, + ChangeField.HASHTAG, + ChangeField.HASHTAG_CASE_AWARE, ChangeField.ID, - ChangeField.STATUS, + ChangeField.LABEL, + ChangeField.LEGACY_ID, + ChangeField.MERGEABLE, + ChangeField.OWNER, + ChangeField.PATCH_SET, + ChangeField.PATH, ChangeField.PROJECT, ChangeField.PROJECTS, ChangeField.REF, - ChangeField.EXACT_TOPIC, - ChangeField.FUZZY_TOPIC, - ChangeField.UPDATED, - ChangeField.FILE_PART, - ChangeField.PATH, - ChangeField.OWNER, - ChangeField.COMMIT, - ChangeField.TR, - ChangeField.LABEL, - ChangeField.COMMIT_MESSAGE, - ChangeField.COMMENT, - ChangeField.CHANGE, - ChangeField.APPROVAL, - ChangeField.MERGEABLE, - ChangeField.ADDED, - ChangeField.DELETED, - ChangeField.DELTA, - ChangeField.HASHTAG, - ChangeField.COMMENTBY, - ChangeField.PATCH_SET, - ChangeField.GROUP, - ChangeField.SUBMISSIONID, - ChangeField.EDITBY, + ChangeField.REF_STATE, + ChangeField.REF_STATE_PATTERN, ChangeField.REVIEWEDBY, - ChangeField.EXACT_COMMIT, - ChangeField.AUTHOR, - ChangeField.COMMITTER, - ChangeField.DRAFTBY, - ChangeField.HASHTAG_CASE_AWARE, + ChangeField.REVIEWER, ChangeField.STAR, ChangeField.STARBY, - ChangeField.REVIEWER); - - @Deprecated static final Schema<ChangeData> V33 = schema(V32, ChangeField.ASSIGNEE); - - @Deprecated - static final Schema<ChangeData> V34 = - new Schema.Builder<ChangeData>() - .add(V33) - .remove(ChangeField.LABEL) - .add(ChangeField.LABEL2) - .build(); - - @Deprecated - static final Schema<ChangeData> V35 = - schema( - V34, - ChangeField.SUBMIT_RECORD, + ChangeField.STATUS, ChangeField.STORED_SUBMIT_RECORD_LENIENT, - ChangeField.STORED_SUBMIT_RECORD_STRICT); + ChangeField.STORED_SUBMIT_RECORD_STRICT, + ChangeField.SUBMISSIONID, + ChangeField.SUBMIT_RECORD, + ChangeField.TR, + ChangeField.UNRESOLVED_COMMENT_COUNT, + ChangeField.UPDATED); + + @Deprecated static final Schema<ChangeData> V40 = schema(V39, ChangeField.PRIVATE); + @Deprecated static final Schema<ChangeData> V41 = schema(V40, ChangeField.REVIEWER_BY_EMAIL); + @Deprecated static final Schema<ChangeData> V42 = schema(V41, ChangeField.WIP); @Deprecated - static final Schema<ChangeData> V36 = - schema(V35, ChangeField.REF_STATE, ChangeField.REF_STATE_PATTERN); + static final Schema<ChangeData> V43 = + schema(V42, ChangeField.EXACT_AUTHOR, ChangeField.EXACT_COMMITTER); - @Deprecated static final Schema<ChangeData> V37 = schema(V36); - - @Deprecated - static final Schema<ChangeData> V38 = schema(V37, ChangeField.UNRESOLVED_COMMENT_COUNT); - - static final Schema<ChangeData> V39 = schema(V38); + static final Schema<ChangeData> V44 = + schema( + V43, + ChangeField.STARTED, + ChangeField.PENDING_REVIEWER, + ChangeField.PENDING_REVIEWER_BY_EMAIL); public static final String NAME = "changes"; public static final ChangeSchemaDefinitions INSTANCE = new ChangeSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java similarity index 82% rename from gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java rename to gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java index 2f6f898..2ab5c55 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterUpdate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ReindexAfterRefUpdate.java
@@ -21,11 +21,15 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.gerrit.extensions.events.GitReferenceUpdatedListener; +import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.account.AccountCache; +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.notedb.ChangeNotes; @@ -41,38 +45,60 @@ import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.Future; +import org.eclipse.jgit.lib.Config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class ReindexAfterUpdate implements GitReferenceUpdatedListener { - private static final Logger log = LoggerFactory.getLogger(ReindexAfterUpdate.class); +public class ReindexAfterRefUpdate implements GitReferenceUpdatedListener { + private static final Logger log = LoggerFactory.getLogger(ReindexAfterRefUpdate.class); private final OneOffRequestContext requestContext; private final Provider<InternalChangeQuery> queryProvider; private final ChangeIndexer.Factory indexerFactory; private final ChangeIndexCollection indexes; private final ChangeNotes.Factory notesFactory; + private final AllUsersName allUsersName; + private final AccountCache accountCache; private final ListeningExecutorService executor; + private final boolean enabled; @Inject - ReindexAfterUpdate( + ReindexAfterRefUpdate( + @GerritServerConfig Config cfg, OneOffRequestContext requestContext, Provider<InternalChangeQuery> queryProvider, ChangeIndexer.Factory indexerFactory, ChangeIndexCollection indexes, ChangeNotes.Factory notesFactory, + AllUsersName allUsersName, + AccountCache accountCache, @IndexExecutor(QueueType.BATCH) ListeningExecutorService executor) { this.requestContext = requestContext; this.queryProvider = queryProvider; this.indexerFactory = indexerFactory; this.indexes = indexes; this.notesFactory = notesFactory; + this.allUsersName = allUsersName; + this.accountCache = accountCache; this.executor = executor; + this.enabled = cfg.getBoolean("index", null, "reindexAfterRefUpdate", true); } @Override - public void onGitReferenceUpdated(final Event event) { - if (event.getRefName().startsWith(RefNames.REFS_CHANGES) + public void onGitReferenceUpdated(Event event) { + if (allUsersName.get().equals(event.getProjectName())) { + Account.Id accountId = Account.Id.fromRef(event.getRefName()); + if (accountId != null) { + try { + accountCache.evict(accountId); + } catch (IOException e) { + log.error(String.format("Reindex account %s failed.", accountId), e); + } + } + } + + if (!enabled + || event.getRefName().startsWith(RefNames.REFS_CHANGES) || event.getRefName().startsWith(RefNames.REFS_DRAFT_COMMENTS) || event.getRefName().startsWith(RefNames.REFS_USERS)) { return;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java index 872dfaf..63d5f9a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/StalenessChecker.java
@@ -16,6 +16,7 @@ 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 java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.joining; @@ -27,6 +28,7 @@ import com.google.common.collect.MultimapBuilder; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; +import com.google.common.collect.Streams; import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; @@ -45,7 +47,6 @@ import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; -import java.util.stream.StreamSupport; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.Ref; @@ -135,15 +136,24 @@ @VisibleForTesting static boolean reviewDbChangeIsStale(Change indexChange, @Nullable Change reviewDbChange) { + checkNotNull(indexChange); + PrimaryStorage storageFromIndex = PrimaryStorage.of(indexChange); + PrimaryStorage storageFromReviewDb = PrimaryStorage.of(reviewDbChange); if (reviewDbChange == null) { - return false; // Nothing the caller can do. + if (storageFromIndex == PrimaryStorage.REVIEW_DB) { + return true; // Index says it should have been in ReviewDb, but it wasn't. + } + return false; // Not in ReviewDb, but that's ok. } checkArgument( indexChange.getId().equals(reviewDbChange.getId()), "mismatched change ID: %s != %s", indexChange.getId(), reviewDbChange.getId()); - if (PrimaryStorage.of(reviewDbChange) != PrimaryStorage.REVIEW_DB) { + if (storageFromIndex != storageFromReviewDb) { + return true; // Primary storage differs, definitely stale. + } + if (storageFromReviewDb != PrimaryStorage.REVIEW_DB) { return false; // Not a ReviewDb change, don't check rowVersion. } return reviewDbChange.getRowVersion() != indexChange.getRowVersion(); @@ -265,7 +275,7 @@ // Quote everything except the '*'s, which become ".*". String regex = - StreamSupport.stream(Splitter.on('*').split(pattern).spliterator(), false) + Streams.stream(Splitter.on('*').split(pattern)) .map(Pattern::quote) .collect(joining(".*", "^", "$")); return new AutoValue_StalenessChecker_RefStatePattern(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java index ec486b5..4014102 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/AllGroupsIndexer.java
@@ -32,7 +32,6 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -79,30 +78,27 @@ progress.beginTask("Reindexing groups", uuids.size()); List<ListenableFuture<?>> futures = new ArrayList<>(uuids.size()); AtomicBoolean ok = new AtomicBoolean(true); - final AtomicInteger done = new AtomicInteger(); - final AtomicInteger failed = new AtomicInteger(); + AtomicInteger done = new AtomicInteger(); + AtomicInteger failed = new AtomicInteger(); Stopwatch sw = Stopwatch.createStarted(); - for (final AccountGroup.UUID uuid : uuids) { - final String desc = "group " + uuid; + for (AccountGroup.UUID uuid : uuids) { + String desc = "group " + uuid; ListenableFuture<?> future = executor.submit( - new Callable<Void>() { - @Override - public Void call() throws Exception { - try { - AccountGroup oldGroup = groupCache.get(uuid); - if (oldGroup != null) { - groupCache.evict(oldGroup); - } - index.replace(groupCache.get(uuid)); - verboseWriter.println("Reindexed " + desc); - done.incrementAndGet(); - } catch (Exception e) { - failed.incrementAndGet(); - throw e; + () -> { + try { + AccountGroup oldGroup = groupCache.get(uuid); + if (oldGroup != null) { + groupCache.evict(oldGroup); } - return null; + index.replace(groupCache.get(uuid)); + verboseWriter.println("Reindexed " + desc); + done.incrementAndGet(); + } catch (Exception e) { + failed.incrementAndGet(); + throw e; } + return null; }); addErrorListener(future, desc, progress, ok); futures.add(future);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java index 5e72327..70bdb3f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupField.java
@@ -18,10 +18,12 @@ import static com.google.gerrit.server.index.FieldDef.fullText; import static com.google.gerrit.server.index.FieldDef.integer; import static com.google.gerrit.server.index.FieldDef.prefix; +import static com.google.gerrit.server.index.FieldDef.timestamp; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.index.FieldDef; import com.google.gerrit.server.index.SchemaUtil; +import java.sql.Timestamp; /** Secondary index schemas for groups. */ public class GroupField { @@ -37,6 +39,10 @@ public static final FieldDef<AccountGroup, String> OWNER_UUID = exact("owner_uuid").build(g -> g.getOwnerGroupUUID().get()); + /** Timestamp indicating when this group was created. */ + public static final FieldDef<AccountGroup, Timestamp> CREATED_ON = + timestamp("created_on").build(AccountGroup::getCreatedOn); + /** Group name. */ public static final FieldDef<AccountGroup, String> NAME = exact("name").build(AccountGroup::getName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java index 6ba46cb..cebde7e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupSchemaDefinitions.java
@@ -22,17 +22,17 @@ public class GroupSchemaDefinitions extends SchemaDefinitions<AccountGroup> { @Deprecated - static final Schema<AccountGroup> V1 = + static final Schema<AccountGroup> V2 = schema( + GroupField.DESCRIPTION, GroupField.ID, - GroupField.UUID, - GroupField.OWNER_UUID, + GroupField.IS_VISIBLE_TO_ALL, GroupField.NAME, GroupField.NAME_PART, - GroupField.DESCRIPTION, - GroupField.IS_VISIBLE_TO_ALL); + GroupField.OWNER_UUID, + GroupField.UUID); - static final Schema<AccountGroup> V2 = schema(V1); + static final Schema<AccountGroup> V3 = schema(V2, GroupField.CREATED_ON); public static final GroupSchemaDefinitions INSTANCE = new GroupSchemaDefinitions();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java index 5f77877..c7f2ecd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java
@@ -41,7 +41,7 @@ public class BasicSerialization { private static final byte[] NO_BYTES = {}; - private static int safeRead(final InputStream input) throws IOException { + private static int safeRead(InputStream input) throws IOException { final int b = input.read(); if (b == -1) { throw new EOFException(); @@ -50,20 +50,20 @@ } /** Read a fixed-width 64 bit integer in network byte order (big-endian). */ - public static long readFixInt64(final InputStream input) throws IOException { + public static long readFixInt64(InputStream input) throws IOException { final long h = readFixInt32(input); final long l = readFixInt32(input) & 0xFFFFFFFFL; return (h << 32) | l; } /** Write a fixed-width 64 bit integer in network byte order (big-endian). */ - public static void writeFixInt64(final OutputStream output, final long val) throws IOException { + public static void writeFixInt64(OutputStream output, long val) throws IOException { writeFixInt32(output, (int) (val >>> 32)); writeFixInt32(output, (int) (val & 0xFFFFFFFFL)); } /** Read a fixed-width 32 bit integer in network byte order (big-endian). */ - public static int readFixInt32(final InputStream input) throws IOException { + public static int readFixInt32(InputStream input) throws IOException { final int b1 = safeRead(input); final int b2 = safeRead(input); final int b3 = safeRead(input); @@ -72,7 +72,7 @@ } /** Write a fixed-width 32 bit integer in network byte order (big-endian). */ - public static void writeFixInt32(final OutputStream output, final int val) throws IOException { + public static void writeFixInt32(OutputStream output, int val) throws IOException { output.write((val >>> 24) & 0xFF); output.write((val >>> 16) & 0xFF); output.write((val >>> 8) & 0xFF); @@ -80,7 +80,7 @@ } /** Read a varint from the input, one byte at a time. */ - public static int readVarInt32(final InputStream input) throws IOException { + public static int readVarInt32(InputStream input) throws IOException { int result = 0; int offset = 0; for (; offset < 32; offset += 7) { @@ -94,7 +94,7 @@ } /** Write a varint; value is treated as an unsigned value. */ - public static void writeVarInt32(final OutputStream output, int value) throws IOException { + public static void writeVarInt32(OutputStream output, int value) throws IOException { while (true) { if ((value & ~0x7F) == 0) { output.write(value); @@ -106,7 +106,7 @@ } /** Read a fixed length byte array whose length is specified as a varint. */ - public static byte[] readBytes(final InputStream input) throws IOException { + public static byte[] readBytes(InputStream input) throws IOException { final int len = readVarInt32(input); if (len == 0) { return NO_BYTES; @@ -117,20 +117,19 @@ } /** Write a byte array prefixed by its length in a varint. */ - public static void writeBytes(final OutputStream output, final byte[] data) throws IOException { + public static void writeBytes(OutputStream output, byte[] data) throws IOException { writeBytes(output, data, 0, data.length); } /** Write a byte array prefixed by its length in a varint. */ - public static void writeBytes( - final OutputStream output, final byte[] data, final int offset, final int len) + public static void writeBytes(final OutputStream output, byte[] data, int offset, int len) throws IOException { writeVarInt32(output, len); output.write(data, offset, len); } /** Read a UTF-8 string, prefixed by its byte length in a varint. */ - public static String readString(final InputStream input) throws IOException { + public static String readString(InputStream input) throws IOException { final byte[] bin = readBytes(input); if (bin.length == 0) { return null; @@ -139,7 +138,7 @@ } /** Write a UTF-8 string, prefixed by its byte length in a varint. */ - public static void writeString(final OutputStream output, final String s) throws IOException { + public static void writeString(OutputStream output, String s) throws IOException { if (s == null) { writeVarInt32(output, 0); } else { @@ -148,8 +147,7 @@ } /** Read an enum whose code is stored as a varint. */ - public static <T extends CodedEnum> T readEnum(final InputStream input, final T[] all) - throws IOException { + public static <T extends CodedEnum> T readEnum(InputStream input, T[] all) throws IOException { final int val = readVarInt32(input); for (T t : all) { if (t.getCode() == val) { @@ -160,8 +158,7 @@ } /** Write an enum whose code is stored as a varint. */ - public static <T extends CodedEnum> void writeEnum(final OutputStream output, final T e) - throws IOException { + public static <T extends CodedEnum> void writeEnum(OutputStream output, T e) throws IOException { writeVarInt32(output, e.getCode()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java index c96e808..10ad33b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java
@@ -33,7 +33,7 @@ * output. As only non-printable characters in the column text are ever escaped, the column * separator must be a non-printable character if the output needs to be unambiguously parsed. */ - public ColumnFormatter(final PrintWriter out, final char columnSeparator) { + public ColumnFormatter(PrintWriter out, char columnSeparator) { this.out = out; this.columnSeparator = columnSeparator; this.firstColumn = true; @@ -45,7 +45,7 @@ * * @param content the string to add. */ - public void addColumn(final String content) { + public void addColumn(String content) { if (!firstColumn) { out.print(columnSeparator); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java index f3b08fb..e91f3f3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -17,7 +17,7 @@ import com.google.gerrit.server.mail.send.EmailHeader; public class Address { - public static Address parse(final String in) { + public static Address parse(String in) { final int lt = in.indexOf('<'); final int gt = in.indexOf('>'); final int at = in.indexOf("@"); @@ -42,6 +42,14 @@ throw new IllegalArgumentException("Invalid email address: " + in); } + public static Address tryParse(String in) { + try { + return parse(in); + } catch (IllegalArgumentException e) { + return null; + } + } + final String name; final String email; @@ -102,7 +110,7 @@ return true; } - private static String quotedPhrase(final String name) { + private static String quotedPhrase(String name) { if (EmailHeader.needsQuotedPrintable(name)) { return EmailHeader.quotedPrintable(name); } @@ -115,7 +123,7 @@ return name; } - private static String wrapInQuotes(final String name) { + private static String wrapInQuotes(String name) { final StringBuilder r = new StringBuilder(2 + name.length()); r.append('"'); for (int i = 0; i < name.length(); i++) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java index b6d7fa8..5dae659 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
@@ -78,7 +78,7 @@ return a.getId(); } - private static boolean isReviewer(final FooterLine candidateFooterLine) { + private static boolean isReviewer(FooterLine candidateFooterLine) { return candidateFooterLine.matches(FooterKey.SIGNED_OFF_BY) || candidateFooterLine.matches(FooterKey.ACKED_BY) || candidateFooterLine.matches(FooterConstants.REVIEWED_BY) @@ -94,17 +94,17 @@ this.cc = new HashSet<>(); } - public MailRecipients(final Set<Account.Id> reviewers, final Set<Account.Id> cc) { + public MailRecipients(Set<Account.Id> reviewers, Set<Account.Id> cc) { this.reviewers = new HashSet<>(reviewers); this.cc = new HashSet<>(cc); } - public void add(final MailRecipients recipients) { + public void add(MailRecipients recipients) { reviewers.addAll(recipients.reviewers); cc.addAll(recipients.cc); } - public void remove(final Account.Id toRemove) { + public void remove(Account.Id toRemove) { reviewers.remove(toRemove); cc.remove(toRemove); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java index f282c2d..a190a45f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/HtmlParser.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.mail.receive; import com.google.common.base.Strings; +import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.common.collect.PeekingIterator; import com.google.gerrit.reviewdb.client.Comment; @@ -99,21 +100,46 @@ content = ParserUtil.trimQuotation(content); // TODO(hiesel) Add more sanitizer if (!Strings.isNullOrEmpty(content)) { - parsedComments.add( - new MailComment(content, null, null, MailComment.CommentType.CHANGE_MESSAGE)); + appendOrAddNewComment( + new MailComment(content, null, null, MailComment.CommentType.CHANGE_MESSAGE), + parsedComments); } } else if (lastEncounteredComment == null) { - parsedComments.add( + appendOrAddNewComment( new MailComment( - content, lastEncounteredFileName, null, MailComment.CommentType.FILE_COMMENT)); + content, lastEncounteredFileName, null, MailComment.CommentType.FILE_COMMENT), + parsedComments); } else { - parsedComments.add( + appendOrAddNewComment( new MailComment( - content, null, lastEncounteredComment, MailComment.CommentType.INLINE_COMMENT)); + content, null, lastEncounteredComment, MailComment.CommentType.INLINE_COMMENT), + parsedComments); } } } } return parsedComments; } + + /** + * When parsing HTML content, we need to append comments prematurely since we are parsing + * block-by-block and never know what comes next. This can result in a comment being parsed as two + * comments when it spans multiple blocks. This method takes care of merging those blocks or + * adding a new comment to the list of appropriate. + */ + private static void appendOrAddNewComment(MailComment comment, List<MailComment> comments) { + if (comments.isEmpty()) { + comments.add(comment); + return; + } + MailComment lastComment = Iterables.getLast(comments); + + if (comment.isSameCommentPath(lastComment)) { + // Merge the two comments + lastComment.message += "\n\n" + comment.message; + return; + } + + comments.add(comment); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailComment.java index 8afbe81..f7804b33 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailComment.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailComment.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.mail.receive; import com.google.gerrit.reviewdb.client.Comment; +import java.util.Objects; /** A comment parsed from inbound email */ public class MailComment { @@ -37,4 +38,14 @@ this.inReplyTo = inReplyTo; this.type = type; } + + /** + * Checks if the provided comment concerns the same exact spot in the change. This is basically an + * equals method except that the message is not checked. + */ + public boolean isSameCommentPath(MailComment c) { + return Objects.equals(fileName, c.fileName) + && Objects.equals(inReplyTo, c.inReplyTo) + && Objects.equals(type, c.type); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java index accf7ba..a7b5501 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -50,6 +50,7 @@ import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; import com.google.gerrit.server.update.Context; +import com.google.gerrit.server.update.RetryHelper; import com.google.gerrit.server.update.UpdateException; import com.google.gerrit.server.util.ManualRequestContext; import com.google.gerrit.server.util.OneOffRequestContext; @@ -72,7 +73,7 @@ private static final Logger log = LoggerFactory.getLogger(MailProcessor.class); private final AccountByEmailCache accountByEmailCache; - private final BatchUpdate.Factory buf; + private final RetryHelper retryHelper; private final ChangeMessagesUtil changeMessagesUtil; private final CommentsUtil commentsUtil; private final OneOffRequestContext oneOffRequestContext; @@ -89,7 +90,7 @@ @Inject public MailProcessor( AccountByEmailCache accountByEmailCache, - BatchUpdate.Factory buf, + RetryHelper retryHelper, ChangeMessagesUtil changeMessagesUtil, CommentsUtil commentsUtil, OneOffRequestContext oneOffRequestContext, @@ -103,7 +104,7 @@ AccountCache accountCache, @CanonicalWebUrl Provider<String> canonicalUrl) { this.accountByEmailCache = accountByEmailCache; - this.buf = buf; + this.retryHelper = retryHelper; this.changeMessagesUtil = changeMessagesUtil; this.commentsUtil = commentsUtil; this.oneOffRequestContext = oneOffRequestContext; @@ -122,9 +123,17 @@ * Parse comments from MailMessage and persist them on the change. * * @param message MailMessage to process. - * @throws OrmException */ - public void process(MailMessage message) throws OrmException { + public void process(MailMessage message) throws RestApiException, UpdateException { + retryHelper.execute( + buf -> { + processImpl(buf, message); + return null; + }); + } + + private void processImpl(BatchUpdate.Factory buf, MailMessage message) + throws OrmException, UpdateException, RestApiException { for (DynamicMap.Entry<MailFilter> filter : mailFilters) { if (!filter.getProvider().get().shouldProcessMessage(message)) { log.warn( @@ -200,11 +209,7 @@ Op o = new Op(new PatchSet.Id(cd.getId(), metadata.patchSet), parsedComments, message.id()); BatchUpdate batchUpdate = buf.create(cd.db(), project, ctx.getUser(), TimeUtil.nowTs()); batchUpdate.addOp(cd.getId(), o); - try { - batchUpdate.execute(); - } catch (UpdateException | RestApiException e) { - throw new OrmException(e); - } + batchUpdate.execute(); } } @@ -320,7 +325,13 @@ // Get previous approvals from this user Map<String, Short> approvals = new HashMap<>(); approvalsUtil - .byPatchSetUser(ctx.getDb(), changeControl, psId, ctx.getAccountId()) + .byPatchSetUser( + ctx.getDb(), + changeControl, + psId, + ctx.getAccountId(), + ctx.getRevWalk(), + ctx.getRepoView().getConfig()) .forEach(a -> approvals.put(a.getLabel(), a.getValue())); // Fire Gerrit event. Note that approvals can't be granted via email, so old and new approvals // are always the same here.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailReceiver.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailReceiver.java index 5068985..7c05478 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailReceiver.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailReceiver.java
@@ -16,10 +16,11 @@ import com.google.common.annotations.VisibleForTesting; import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.lifecycle.LifecycleModule; import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.mail.EmailSettings; -import com.google.gwtorm.server.OrmException; +import com.google.gerrit.server.update.UpdateException; import com.google.inject.Inject; import java.util.Collections; import java.util.HashSet; @@ -132,7 +133,7 @@ try { mailProcessor.process(m); requestDeletion(m.id()); - } catch (OrmException e) { + } catch (RestApiException | UpdateException e) { log.error("Mail: Can't process message " + m.id() + " . Won't delete.", e); } }); @@ -141,7 +142,7 @@ try { mailProcessor.process(m); requestDeletion(m.id()); - } catch (OrmException e) { + } catch (RestApiException | UpdateException e) { log.error("Mail: Can't process messages. Won't delete.", e); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AddKeySender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AddKeySender.java index 0c09639..30938f1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AddKeySender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/AddKeySender.java
@@ -17,9 +17,13 @@ import com.google.common.base.Joiner; import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.AccountSshKey; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.mail.Address; +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.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import java.util.List; @@ -31,6 +35,7 @@ AddKeySender create(IdentifiedUser user, List<String> gpgKey); } + private final PermissionBackend permissionBackend; private final IdentifiedUser callingUser; private final IdentifiedUser user; private final AccountSshKey sshKey; @@ -39,10 +44,12 @@ @AssistedInject public AddKeySender( EmailArguments ea, + PermissionBackend permissionBackend, IdentifiedUser callingUser, @Assisted IdentifiedUser user, @Assisted AccountSshKey sshKey) { super(ea, "addkey"); + this.permissionBackend = permissionBackend; this.callingUser = callingUser; this.user = user; this.sshKey = sshKey; @@ -52,10 +59,12 @@ @AssistedInject public AddKeySender( EmailArguments ea, + PermissionBackend permissionBackend, IdentifiedUser callingUser, @Assisted IdentifiedUser user, @Assisted List<String> gpgKeys) { super(ea, "addkey"); + this.permissionBackend = permissionBackend; this.callingUser = callingUser; this.user = user; this.sshKey = null; @@ -71,12 +80,25 @@ @Override protected boolean shouldSendMessage() { - /* - * Don't send an email if no keys are added, or an admin is adding a key to - * a user. - */ - return (sshKey != null || gpgKeys.size() > 0) - && (user.equals(callingUser) || !callingUser.getCapabilities().canAdministrateServer()); + if (sshKey == null && (gpgKeys == null || gpgKeys.isEmpty())) { + // Don't email if no keys were added. + return false; + } + + if (user.equals(callingUser)) { + // Send email if the user self-added a key; this notification is necessary to alert + // the user if their account was compromised and a key was unexpectedly added. + return true; + } + + try { + // Don't email if an administrator added a key on behalf of the user. + permissionBackend.user(callingUser).check(GlobalPermission.ADMINISTRATE_SERVER); + return false; + } catch (AuthException | PermissionBackendException e) { + // Send email if a non-administrator modified the keys, e.g. by MODIFY_ACCOUNT. + return true; + } } @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java index 3753334..15a4b13 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -14,10 +14,12 @@ package com.google.gerrit.server.mail.send; +import com.google.common.base.Splitter; import com.google.common.collect.ListMultimap; import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.RecipientType; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Change; @@ -36,9 +38,13 @@ import com.google.gerrit.server.patch.PatchListEntry; import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.query.change.ChangeData; import com.google.gwtorm.server.OrmException; +import com.google.template.soy.data.SoyListData; +import com.google.template.soy.data.SoyMapData; import java.io.IOException; import java.sql.Timestamp; import java.text.MessageFormat; @@ -87,25 +93,29 @@ } @Override - public void setFrom(final Account.Id id) { + public void setFrom(Account.Id id) { super.setFrom(id); /** Is the from user in an email squelching group? */ - final IdentifiedUser user = args.identifiedUserFactory.create(id); - emailOnlyAuthors = !user.getCapabilities().canEmailReviewers(); + try { + IdentifiedUser user = args.identifiedUserFactory.create(id); + args.permissionBackend.user(user).check(GlobalPermission.EMAIL_REVIEWERS); + } catch (AuthException | PermissionBackendException e) { + emailOnlyAuthors = true; + } } - public void setPatchSet(final PatchSet ps) { + public void setPatchSet(PatchSet ps) { patchSet = ps; } - public void setPatchSet(final PatchSet ps, final PatchSetInfo psi) { + public void setPatchSet(PatchSet ps, PatchSetInfo psi) { patchSet = ps; patchSetInfo = psi; } @Deprecated - public void setChangeMessage(final ChangeMessage cm) { + public void setChangeMessage(ChangeMessage cm) { setChangeMessage(cm.getMessage(), cm.getWrittenOn()); } @@ -180,6 +190,18 @@ setHeader("X-Gerrit-Change-Number", "" + change.getChangeId()); setChangeUrlHeader(); setCommitIdHeader(); + + if (notify.ordinal() >= NotifyHandling.OWNER_REVIEWERS.ordinal()) { + try { + addByEmail( + RecipientType.CC, changeData.reviewersByEmail().byState(ReviewerStateInternal.CC)); + addByEmail( + RecipientType.CC, + changeData.reviewersByEmail().byState(ReviewerStateInternal.REVIEWER)); + } catch (OrmException e) { + throw new EmailException("Failed to add unregistered CCs " + change.getChangeId(), e); + } + } } private void setChangeUrlHeader() { @@ -304,8 +326,8 @@ } /** TO or CC all vested parties (change owner, patch set uploader, author). */ - protected void rcptToAuthors(final RecipientType rt) { - for (final Account.Id id : authors) { + protected void rcptToAuthors(RecipientType rt) { + for (Account.Id id : authors) { add(rt, id); } } @@ -376,14 +398,14 @@ } @Override - protected void add(final RecipientType rt, final Account.Id to) { + protected void add(RecipientType rt, Account.Id to) { if (!emailOnlyAuthors || authors.contains(to)) { super.add(rt, to); } } @Override - protected boolean isVisibleTo(final Account.Id to) throws OrmException { + protected boolean isVisibleTo(Account.Id to) throws OrmException { return projectState == null || projectState .controlFor(args.identifiedUserFactory.create(to)) @@ -411,7 +433,7 @@ authors.add(patchSetInfo.getCommitter().getAccount()); } } - //$FALL-THROUGH$ + // $FALL-THROUGH$ case OWNER_REVIEWERS: case OWNER: authors.add(change.getOwner()); @@ -440,6 +462,7 @@ soyContext.put("coverLetter", getCoverLetter()); soyContext.put("fromName", getNameFor(fromId)); soyContext.put("fromEmail", getNameEmailFor(fromId)); + soyContext.put("diffLines", getDiffTemplateData()); soyContextEmailData.put("unifiedDiff", getUnifiedDiff()); soyContextEmailData.put("changeDetail", getChangeDetail()); @@ -539,4 +562,37 @@ } } } + + /** + * Generate a Soy list of maps representing each line of the unified diff. The line maps will have + * a 'type' key which maps to one of 'common', 'add' or 'remove' and a 'text' key which maps to + * the line's content. + */ + private SoyListData getDiffTemplateData() { + SoyListData result = new SoyListData(); + Splitter lineSplitter = Splitter.on(System.getProperty("line.separator")); + for (String diffLine : lineSplitter.split(getUnifiedDiff())) { + SoyMapData lineData = new SoyMapData(); + lineData.put("text", diffLine); + + // Skip empty lines and lines that look like diff headers. + if (diffLine.isEmpty() || diffLine.startsWith("---") || diffLine.startsWith("+++")) { + lineData.put("type", "common"); + } else { + switch (diffLine.charAt(0)) { + case '+': + lineData.put("type", "add"); + break; + case '-': + lineData.put("type", "remove"); + break; + default: + lineData.put("type", "common"); + break; + } + } + result.add(lineData); + } + return result; + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java index b572e8d..21e9ad5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -155,7 +155,9 @@ } if (notify.compareTo(NotifyHandling.ALL) >= 0) { bccStarredBy(); - includeWatchers(NotifyType.ALL_COMMENTS, !patchSet.isDraft()); + includeWatchers( + NotifyType.ALL_COMMENTS, + !patchSet.isDraft() && !change.isWorkInProgress() && !change.isPrivate()); } removeUsersThatIgnoredTheChange(); @@ -588,6 +590,7 @@ footers.add("Gerrit-Comment-Date: " + getCommentTimestamp()); footers.add("Gerrit-HasComments: " + (hasComments ? "Yes" : "No")); + footers.add("Gerrit-HasLabels: " + (labels.isEmpty() ? "No" : "Yes")); } private String getLine(PatchFile fileInfo, short side, int lineNbr) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java index 3e9e62c..8757a28 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/CreateChangeSender.java
@@ -48,9 +48,13 @@ super.init(); boolean isDraft = change.getStatus() == Change.Status.DRAFT; + try { // Try to mark interested owners with TO and CC or BCC line. - Watchers matching = getWatchers(NotifyType.NEW_CHANGES, !isDraft); + Watchers matching = + getWatchers( + NotifyType.NEW_CHANGES, + !isDraft && !change.isWorkInProgress() && !change.isPrivate()); for (Account.Id user : Iterables.concat(matching.to.accounts, matching.cc.accounts, matching.bcc.accounts)) { if (isOwnerOfProjectOrBranch(user)) { @@ -69,7 +73,8 @@ log.warn("Cannot notify watchers for new change", err); } - includeWatchers(NotifyType.NEW_PATCHSETS, !isDraft); + includeWatchers( + NotifyType.NEW_PATCHSETS, !isDraft && !change.isWorkInProgress() && !change.isPrivate()); } private boolean isOwnerOfProjectOrBranch(Account.Id user) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java index a563846..0fea7ce 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/DeleteReviewerSender.java
@@ -20,6 +20,7 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.account.WatchConfig.NotifyType; +import com.google.gerrit.server.mail.Address; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; @@ -32,6 +33,7 @@ /** Let users know that a reviewer and possibly her review have been removed. */ public class DeleteReviewerSender extends ReplyToChangeSender { private final Set<Account.Id> reviewers = new HashSet<>(); + private final Set<Address> reviewersByEmail = new HashSet<>(); public interface Factory extends ReplyToChangeSender.Factory<DeleteReviewerSender> { @Override @@ -49,6 +51,10 @@ reviewers.addAll(cc); } + public void addReviewersByEmail(Collection<Address> cc) { + reviewersByEmail.addAll(cc); + } + @Override protected void init() throws EmailException { super.init(); @@ -58,6 +64,7 @@ ccExistingReviewers(); includeWatchers(NotifyType.ALL_COMMENTS); add(RecipientType.TO, reviewers); + addByEmail(RecipientType.TO, reviewersByEmail); removeUsersThatIgnoredTheChange(); } @@ -70,13 +77,16 @@ } public List<String> getReviewerNames() { - if (reviewers.isEmpty()) { + if (reviewers.isEmpty() && reviewersByEmail.isEmpty()) { return null; } List<String> names = new ArrayList<>(); for (Account.Id id : reviewers) { names.add(getNameFor(id)); } + for (Address a : reviewersByEmail) { + names.add(a.toString()); + } return names; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailArguments.java index 9306c7a..3bf5db1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailArguments.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailArguments.java
@@ -24,7 +24,6 @@ import com.google.gerrit.server.IdentifiedUser.GenericFactory; import com.google.gerrit.server.StarredChangesUtil; import com.google.gerrit.server.account.AccountCache; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.GroupIncludeCache; import com.google.gerrit.server.config.AllProjectsName; @@ -36,6 +35,7 @@ import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.patch.PatchListCache; import com.google.gerrit.server.patch.PatchSetInfoFactory; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gerrit.server.query.change.ChangeData; @@ -52,6 +52,7 @@ public class EmailArguments { final GitRepositoryManager server; final ProjectCache projectCache; + final PermissionBackend permissionBackend; final GroupBackend groupBackend; final GroupIncludeCache groupIncludes; final AccountCache accountCache; @@ -61,7 +62,6 @@ final EmailSender emailSender; final PatchSetInfoFactory patchSetInfoFactory; final IdentifiedUser.GenericFactory identifiedUserFactory; - final CapabilityControl.Factory capabilityControlFactory; final ChangeNotes.Factory changeNotesFactory; final AnonymousUser anonymousUser; final String anonymousCowardName; @@ -80,11 +80,13 @@ final DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners; final StarredChangesUtil starredChangesUtil; final Provider<InternalAccountQuery> accountQueryProvider; + final OutgoingEmailValidator validator; @Inject EmailArguments( GitRepositoryManager server, ProjectCache projectCache, + PermissionBackend permissionBackend, GroupBackend groupBackend, GroupIncludeCache groupIncludes, AccountCache accountCache, @@ -94,7 +96,6 @@ EmailSender emailSender, PatchSetInfoFactory patchSetInfoFactory, GenericFactory identifiedUserFactory, - CapabilityControl.Factory capabilityControlFactory, ChangeNotes.Factory changeNotesFactory, AnonymousUser anonymousUser, @AnonymousCowardName String anonymousCowardName, @@ -111,9 +112,11 @@ SitePaths site, DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners, StarredChangesUtil starredChangesUtil, - Provider<InternalAccountQuery> accountQueryProvider) { + Provider<InternalAccountQuery> accountQueryProvider, + OutgoingEmailValidator validator) { this.server = server; this.projectCache = projectCache; + this.permissionBackend = permissionBackend; this.groupBackend = groupBackend; this.groupIncludes = groupIncludes; this.accountCache = accountCache; @@ -123,7 +126,6 @@ this.emailSender = emailSender; this.patchSetInfoFactory = patchSetInfoFactory; this.identifiedUserFactory = identifiedUserFactory; - this.capabilityControlFactory = capabilityControlFactory; this.changeNotesFactory = changeNotesFactory; this.anonymousUser = anonymousUser; this.anonymousCowardName = anonymousCowardName; @@ -141,5 +143,6 @@ this.outgoingEmailValidationListeners = outgoingEmailValidationListeners; this.starredChangesUtil = starredChangesUtil; this.accountQueryProvider = accountQueryProvider; + this.validator = validator; } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailHeader.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailHeader.java index e2b5894..0bfe428 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailHeader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/EmailHeader.java
@@ -83,7 +83,7 @@ return false; } - static boolean needsQuotedPrintableWithinPhrase(final int cp) { + static boolean needsQuotedPrintableWithinPhrase(int cp) { switch (cp) { case '!': case '*': @@ -202,7 +202,7 @@ int len = 8; boolean firstAddress = true; boolean needComma = false; - for (final Address addr : list) { + for (Address addr : list) { java.lang.String s = addr.toHeaderString(); if (firstAddress) { firstAddress = false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java index db52626..c2c6834 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProvider.java
@@ -41,11 +41,10 @@ @Inject FromAddressGeneratorProvider( - @GerritServerConfig final Config cfg, - @AnonymousCowardName final String anonymousCowardName, - @GerritPersonIdent final PersonIdent myIdent, - final AccountCache accountCache) { - + @GerritServerConfig Config cfg, + @AnonymousCowardName String anonymousCowardName, + @GerritPersonIdent PersonIdent myIdent, + AccountCache accountCache) { final String from = cfg.getString("sendemail", null, "from"); final Address srvAddr = toAddress(myIdent); @@ -73,7 +72,7 @@ } } - private static Address toAddress(final PersonIdent myIdent) { + private static Address toAddress(PersonIdent myIdent) { return new Address(myIdent.getName(), myIdent.getEmailAddress()); } @@ -119,7 +118,7 @@ } @Override - public Address from(final Account.Id fromId) { + public Address from(Account.Id fromId) { String senderName; if (fromId != null) { Account a = accountCache.get(fromId).getAccount(); @@ -172,7 +171,7 @@ } @Override - public Address from(final Account.Id fromId) { + public Address from(Account.Id fromId) { return srvAddr; } } @@ -203,7 +202,7 @@ } @Override - public Address from(final Account.Id fromId) { + public Address from(Account.Id fromId) { final String senderName; if (fromId != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java index 47115af..9c2e39d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -41,7 +41,7 @@ public MergedSender(EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id) throws OrmException { super(ea, "merged", newChangeData(ea, project, id)); - labelTypes = changeData.changeControl().getLabelTypes(); + labelTypes = changeData.getLabelTypes(); } @Override @@ -70,7 +70,7 @@ Table<Account.Id, String, PatchSetApproval> neg = HashBasedTable.create(); for (PatchSetApproval ca : args.approvalsUtil.byPatchSet( - args.db.get(), changeData.changeControl(), patchSet.getId())) { + args.db.get(), changeData.changeControl(), patchSet.getId(), null, null)) { LabelType lt = labelTypes.byLabel(ca.getLabelId()); if (lt == null) { continue;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NewChangeSender.java index f1a9ad8..9f94fa3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NewChangeSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/NewChangeSender.java
@@ -17,6 +17,7 @@ import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.query.change.ChangeData; import com.google.gwtorm.server.OrmException; import java.util.ArrayList; @@ -28,20 +29,30 @@ /** Sends an email alerting a user to a new change for them to review. */ public abstract class NewChangeSender extends ChangeEmail { private final Set<Account.Id> reviewers = new HashSet<>(); + private final Set<Address> reviewersByEmail = new HashSet<>(); private final Set<Account.Id> extraCC = new HashSet<>(); + private final Set<Address> extraCCByEmail = new HashSet<>(); protected NewChangeSender(EmailArguments ea, ChangeData cd) throws OrmException { super(ea, "newchange", cd); } - public void addReviewers(final Collection<Account.Id> cc) { + public void addReviewers(Collection<Account.Id> cc) { reviewers.addAll(cc); } - public void addExtraCC(final Collection<Account.Id> cc) { + public void addReviewersByEmail(Collection<Address> cc) { + reviewersByEmail.addAll(cc); + } + + public void addExtraCC(Collection<Account.Id> cc) { extraCC.addAll(cc); } + public void addExtraCCByEmail(Collection<Address> cc) { + extraCCByEmail.addAll(cc); + } + @Override protected void init() throws EmailException { super.init(); @@ -55,9 +66,11 @@ case ALL: default: add(RecipientType.CC, extraCC); - //$FALL-THROUGH$ + extraCCByEmail.stream().forEach(cc -> add(RecipientType.CC, cc)); + // $FALL-THROUGH$ case OWNER_REVIEWERS: - add(RecipientType.TO, reviewers); + add(RecipientType.TO, reviewers, true); + addByEmail(RecipientType.TO, reviewersByEmail, true); break; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java index 730b710..4e204ce 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -91,7 +91,7 @@ headers = new LinkedHashMap<>(); } - public void setFrom(final Account.Id id) { + public void setFrom(Account.Id id) { fromId = id; } @@ -309,26 +309,26 @@ } /** Set a header in the outgoing message using a template. */ - protected void setVHeader(final String name, final String value) throws EmailException { + protected void setVHeader(String name, String value) throws EmailException { setHeader(name, velocify(value)); } /** Set a header in the outgoing message. */ - protected void setHeader(final String name, final String value) { + protected void setHeader(String name, String value) { headers.put(name, new EmailHeader.String(value)); } /** Remove a header from the outgoing message. */ - protected void removeHeader(final String name) { + protected void removeHeader(String name) { headers.remove(name); } - protected void setHeader(final String name, final Date date) { + protected void setHeader(String name, Date date) { headers.put(name, new EmailHeader.Date(date)); } /** Append text to the outgoing email body. */ - protected void appendText(final String text) { + protected void appendText(String text) { if (text != null) { textBody.append(text); } @@ -342,7 +342,7 @@ } /** Lookup a human readable name for an account, usually the "full name". */ - protected String getNameFor(final Account.Id accountId) { + protected String getNameFor(Account.Id accountId) { if (accountId == null) { return args.gerritPersonIdent.getName(); } @@ -435,24 +435,49 @@ } /** Schedule this message for delivery to the listed accounts. */ - protected void add(final RecipientType rt, final Collection<Account.Id> list) { + protected void add(RecipientType rt, Collection<Account.Id> list) { + add(rt, list, false); + } + + /** Schedule this message for delivery to the listed accounts. */ + protected void add(RecipientType rt, Collection<Account.Id> list, boolean override) { for (final Account.Id id : list) { - add(rt, id); + add(rt, id, override); } } - protected void add(final RecipientType rt, final UserIdentity who) { + /** Schedule this message for delivery to the listed address. */ + protected void addByEmail(RecipientType rt, Collection<Address> list) { + addByEmail(rt, list, false); + } + + /** Schedule this message for delivery to the listed address. */ + protected void addByEmail(RecipientType rt, Collection<Address> list, boolean override) { + for (final Address id : list) { + add(rt, id, override); + } + } + + protected void add(RecipientType rt, UserIdentity who) { + add(rt, who, false); + } + + protected void add(RecipientType rt, UserIdentity who, boolean override) { if (who != null && who.getAccount() != null) { - add(rt, who.getAccount()); + add(rt, who.getAccount(), override); } } /** Schedule delivery of this message to the given account. */ - protected void add(final RecipientType rt, final Account.Id to) { + protected void add(RecipientType rt, Account.Id to) { + add(rt, to, false); + } + + protected void add(RecipientType rt, Account.Id to, boolean override) { try { if (!rcptTo.contains(to) && isVisibleTo(to)) { rcptTo.add(to); - add(rt, toAddress(to)); + add(rt, toAddress(to), override); } } catch (OrmException e) { log.error("Error reading database for account: " + to, e); @@ -464,18 +489,29 @@ * @throws OrmException * @return whether this email is visible to the given account. */ - protected boolean isVisibleTo(final Account.Id to) throws OrmException { + protected boolean isVisibleTo(Account.Id to) throws OrmException { return true; } /** Schedule delivery of this message to the given account. */ - protected void add(final RecipientType rt, final Address addr) { + protected void add(RecipientType rt, Address addr) { + add(rt, addr, false); + } + + protected void add(RecipientType rt, Address addr, boolean override) { if (addr != null && addr.getEmail() != null && addr.getEmail().length() > 0) { - if (!OutgoingEmailValidator.isValid(addr.getEmail())) { + if (!args.validator.isValid(addr.getEmail())) { log.warn("Not emailing " + addr.getEmail() + " (invalid email address)"); } else if (!args.emailSender.canEmail(addr.getEmail())) { log.warn("Not emailing " + addr.getEmail() + " (prohibited by allowrcpt)"); - } else if (smtpRcptTo.add(addr)) { + } else { + if (!smtpRcptTo.add(addr)) { + if (!override) { + return; + } + ((EmailHeader.AddressList) headers.get(HDR_TO)).remove(addr.getEmail()); + ((EmailHeader.AddressList) headers.get(HDR_CC)).remove(addr.getEmail()); + } switch (rt) { case TO: ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr); @@ -490,7 +526,7 @@ } } - private Address toAddress(final Account.Id id) { + private Address toAddress(Account.Id id) { final Account a = args.accountCache.get(id).getAccount(); final String e = a.getPreferredEmail(); if (!a.isActive() || e == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java index 2d9db1d..1a4d39b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/OutgoingEmailValidator.java
@@ -16,15 +16,34 @@ import static org.apache.commons.validator.routines.DomainValidator.ArrayType.GENERIC_PLUS; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.inject.Inject; +import com.google.inject.Singleton; import org.apache.commons.validator.routines.DomainValidator; import org.apache.commons.validator.routines.EmailValidator; +import org.eclipse.jgit.lib.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +@Singleton public class OutgoingEmailValidator { - static { - DomainValidator.updateTLDOverride(GENERIC_PLUS, new String[] {"local"}); + private static final Logger log = LoggerFactory.getLogger(OutgoingEmailValidator.class); + + @Inject + OutgoingEmailValidator(@GerritServerConfig Config config) { + String[] allowTLD = config.getStringList("sendemail", null, "allowTLD"); + if (allowTLD.length != 0) { + try { + DomainValidator.updateTLDOverride(GENERIC_PLUS, allowTLD); + } catch (IllegalStateException e) { + // Should only happen in tests, where the OutgoingEmailValidator + // is instantiated repeatedly. + log.error("Failed to update TLD override: " + e.getMessage()); + } + } } - public static boolean isValid(String addr) { + public boolean isValid(String addr) { return EmailValidator.getInstance(true, true).isValid(addr); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java index b459d25..91c4834 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ProjectWatch.java
@@ -141,7 +141,7 @@ private void add(Watchers matching, NotifyConfig nc) throws OrmException, QueryParseException { for (GroupReference ref : nc.getGroups()) { - CurrentUser user = new SingleGroupUser(args.capabilityControlFactory, ref.getUUID()); + CurrentUser user = new SingleGroupUser(ref.getUUID()); if (filterMatch(user, nc.getFilter())) { deliverToMembers(matching.list(nc.getHeader()), ref.getUUID()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java index c90000f..b5dbc84 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ReplacePatchSetSender.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.mail.send; import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.RecipientType; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; @@ -45,11 +46,11 @@ super(ea, "newpatchset", newChangeData(ea, project, id)); } - public void addReviewers(final Collection<Account.Id> cc) { + public void addReviewers(Collection<Account.Id> cc) { reviewers.addAll(cc); } - public void addExtraCC(final Collection<Account.Id> cc) { + public void addExtraCC(Collection<Account.Id> cc) { extraCC.addAll(cc); } @@ -62,11 +63,15 @@ // reviewers.remove(fromId); } - add(RecipientType.TO, reviewers); - add(RecipientType.CC, extraCC); + if (notify == NotifyHandling.ALL || notify == NotifyHandling.OWNER_REVIEWERS) { + add(RecipientType.TO, reviewers); + add(RecipientType.CC, extraCC); + } rcptToAuthors(RecipientType.CC); bccStarredBy(); - includeWatchers(NotifyType.NEW_PATCHSETS, !patchSet.isDraft()); + includeWatchers( + NotifyType.NEW_PATCHSETS, + !patchSet.isDraft() && !change.isWorkInProgress() && !change.isPrivate()); removeUsersThatIgnoredTheChange(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java index 80e6bb8..912b598 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -74,7 +74,7 @@ private int expiryDays; @Inject - SmtpEmailSender(@GerritServerConfig final Config cfg) { + SmtpEmailSender(@GerritServerConfig Config cfg) { enabled = cfg.getBoolean("sendemail", null, "enable", true); connectTimeout = Ints.checkedCast( @@ -320,8 +320,7 @@ + "--\r\n"; } - private void setMissingHeader( - final Map<String, EmailHeader> hdrs, final String name, final String value) { + private void setMissingHeader(final Map<String, EmailHeader> hdrs, String name, String value) { if (!hdrs.containsKey(name) || hdrs.get(name).isEmpty()) { hdrs.put(name, new EmailHeader.String(value)); } @@ -365,7 +364,7 @@ try { client.disconnect(); } catch (IOException e2) { - //Ignored + // Ignored } } if (e instanceof EmailException) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mime/FileTypeRegistry.java b/gerrit-server/src/main/java/com/google/gerrit/server/mime/FileTypeRegistry.java index e9e3c71..15ee1bd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mime/FileTypeRegistry.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mime/FileTypeRegistry.java
@@ -16,6 +16,7 @@ import eu.medsea.mimeutil.MimeType; import eu.medsea.mimeutil.MimeUtil2; +import java.io.InputStream; public interface FileTypeRegistry { /** @@ -33,6 +34,20 @@ MimeType getMimeType(String path, byte[] content); /** + * Get the most specific MIME type available for a file. + * + * @param path name of the file. The base name (component after the last '/') may be used to help + * determine the MIME type, such as by examining the extension (portion after the last '.' if + * present). + * @param is InputStream corresponding to the complete file content. The content may be used to + * guess the MIME type by examining the beginning for common file headers. + * @return the MIME type for this content. If the MIME type is not recognized or cannot be + * determined, {@link MimeUtil2#UNKNOWN_MIME_TYPE} which is an alias for {@code + * application/octet-stream}. + */ + MimeType getMimeType(String path, InputStream is); + + /** * Is this content type safe to transmit to a browser directly? * * @param type the MIME type of the file content.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java b/gerrit-server/src/main/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java index 859363c..7cb34e2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mime/MimeUtilFileTypeRegistry.java
@@ -20,6 +20,7 @@ import eu.medsea.mimeutil.MimeException; import eu.medsea.mimeutil.MimeType; import eu.medsea.mimeutil.MimeUtil2; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -78,7 +79,7 @@ @Override @SuppressWarnings("unchecked") - public MimeType getMimeType(final String path, final byte[] content) { + public MimeType getMimeType(String path, byte[] content) { Set<MimeType> mimeTypes = new HashSet<>(); if (content != null && content.length > 0) { try { @@ -87,6 +88,23 @@ log.warn("Unable to determine MIME type from content", e); } } + return getMimeType(mimeTypes, path); + } + + @Override + @SuppressWarnings("unchecked") + public MimeType getMimeType(String path, InputStream is) { + Set<MimeType> mimeTypes = new HashSet<>(); + try { + mimeTypes.addAll(mimeUtil.getMimeTypes(is)); + } catch (MimeException e) { + log.warn("Unable to determine MIME type from content", e); + } + return getMimeType(mimeTypes, path); + } + + @SuppressWarnings("unchecked") + private MimeType getMimeType(Set<MimeType> mimeTypes, String path) { try { mimeTypes.addAll(mimeUtil.getMimeTypes(path)); } catch (MimeException e) { @@ -110,7 +128,7 @@ } @Override - public boolean isSafeInline(final MimeType type) { + public boolean isSafeInline(MimeType type) { if (MimeUtil2.UNKNOWN_MIME_TYPE.equals(type)) { // Most browsers perform content type sniffing when they get told // a generic content type. This is bad, so assume we cannot send
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java index 4cb570a..91a93b0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -123,7 +123,10 @@ this.args = checkNotNull(args); this.changeId = checkNotNull(changeId); this.primaryStorage = primaryStorage; - this.autoRebuild = primaryStorage == PrimaryStorage.REVIEW_DB && autoRebuild; + this.autoRebuild = + primaryStorage == PrimaryStorage.REVIEW_DB + && !args.migration.disableChangeReviewDb() + && autoRebuild; } public Change.Id getChangeId() { @@ -143,7 +146,7 @@ if (!read && primaryStorage == PrimaryStorage.NOTE_DB) { throw new OrmException("NoteDb is required to read change " + changeId); } - boolean readOrWrite = read || args.migration.writeChanges(); + boolean readOrWrite = read || args.migration.rawWriteChangesSetting(); if (!readOrWrite && !autoRebuild) { loadDefaults(); return self();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java index d5b1b3d..d56baed 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeBundle.java
@@ -233,7 +233,7 @@ // last time this file was updated. checkColumns(Change.Id.class, 1); - checkColumns(Change.class, 1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18, 19, 101); + checkColumns(Change.class, 1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18, 19, 20, 21, 22, 101); checkColumns(ChangeMessage.Key.class, 1, 2); checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5, 6, 7); checkColumns(PatchSet.Id.class, 1, 2);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java index c848987..ce3b664 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -73,6 +73,7 @@ public static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set"); public static final FooterKey FOOTER_PATCH_SET_DESCRIPTION = new FooterKey("Patch-set-description"); + public static final FooterKey FOOTER_PRIVATE = new FooterKey("Private"); public static final FooterKey FOOTER_READ_ONLY_UNTIL = new FooterKey("Read-only-until"); public static final FooterKey FOOTER_REAL_USER = new FooterKey("Real-user"); public static final FooterKey FOOTER_STATUS = new FooterKey("Status"); @@ -81,6 +82,7 @@ public static final FooterKey FOOTER_SUBMITTED_WITH = new FooterKey("Submitted-with"); public static final FooterKey FOOTER_TOPIC = new FooterKey("Topic"); public static final FooterKey FOOTER_TAG = new FooterKey("Tag"); + public static final FooterKey FOOTER_WORK_IN_PROGRESS = new FooterKey("Work-in-progress"); private static final String AUTHOR = "Author"; private static final String BASE_PATCH_SET = "Base-for-patch-set";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java index 4993a5d..62ee879 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -21,6 +21,7 @@ import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES; import static java.util.Comparator.comparing; +import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; @@ -32,6 +33,8 @@ import com.google.common.collect.MultimapBuilder; import com.google.common.collect.Multimaps; import com.google.common.collect.Ordering; +import com.google.common.collect.Sets; +import com.google.common.collect.Sets.SetView; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.metrics.Timer1; @@ -48,6 +51,7 @@ import com.google.gerrit.reviewdb.client.RobotComment; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDbUtil; +import com.google.gerrit.server.ReviewerByEmailSet; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.ReviewerStatusUpdate; import com.google.gerrit.server.git.RefCache; @@ -66,7 +70,6 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -94,6 +97,7 @@ return new ConfigInvalidException("Change " + changeId + ": " + String.format(fmt, args)); } + @Nullable public static Change readOneReviewDbChange(ReviewDb db, Change.Id id) throws OrmException { return ReviewDbUtil.unwrapDb(db).changes().get(id); } @@ -164,7 +168,7 @@ // Prepopulate the change exists with proper noteDbState field. change = newNoteDbOnlyChange(project, changeId); } else { - checkNotNull(change, "change %s not found in ReviewDb", changeId); + checkArgument(change != null, "change %s not found in ReviewDb", changeId); checkArgument( change.getProject().equals(project), "passed project %s when creating ChangeNotes for %s, but actual project is %s", @@ -250,9 +254,15 @@ List<ChangeNotes> notes = new ArrayList<>(); if (args.migration.enabled()) { for (Change.Id cid : changeIds) { - ChangeNotes cn = create(db, project, cid); - if (cn.getChange() != null && predicate.test(cn)) { - notes.add(cn); + try { + ChangeNotes cn = create(db, project, cid); + if (cn.getChange() != null && predicate.test(cn)) { + notes.add(cn); + } + } catch (NoSuchChangeException e) { + // Match ReviewDb behavior, returning not found; maybe the caller learned about it from + // a dangling patch set ref or something. + continue; } } return notes; @@ -306,10 +316,14 @@ private List<ChangeNotes> scanDb(Repository repo, ReviewDb db) throws OrmException, IOException { - Set<Change.Id> ids = scan(repo); + // Scan IDs that might exist in ReviewDb, assuming that each change has at least one patch set + // ref. Not all changes might exist: some patch set refs might have been written where the + // corresponding ReviewDb write failed. These will be silently filtered out by the batch get + // call below, which is intended. + Set<Change.Id> ids = scanChangeIds(repo).fromPatchSetRefs(); List<ChangeNotes> notes = new ArrayList<>(ids.size()); - // A batch size of N may overload get(Iterable), so use something smaller, - // but still >1. + + // A batch size of N may overload get(Iterable), so use something smaller, but still >1. for (List<Change.Id> batch : Iterables.partition(ids, 30)) { for (Change change : ReviewDbUtil.unwrapDb(db).changes().get(batch)) { notes.add(createFromChangeOnlyWhenNoteDbDisabled(change)); @@ -320,14 +334,20 @@ private List<ChangeNotes> scanNoteDb(Repository repo, ReviewDb db, Project.NameKey project) throws OrmException, IOException { - Set<Change.Id> ids = scan(repo); - List<ChangeNotes> changeNotes = new ArrayList<>(ids.size()); + ScanResult sr = scanChangeIds(repo); + List<ChangeNotes> changeNotes = new ArrayList<>(sr.fromPatchSetRefs().size()); + PrimaryStorage defaultStorage = args.migration.changePrimaryStorage(); - for (Change.Id id : ids) { + for (Change.Id id : sr.all()) { Change change = readOneReviewDbChange(db, id); if (change == null) { if (defaultStorage == PrimaryStorage.REVIEW_DB) { - log.warn("skipping change {} found in project {} but not in ReviewDb", id, project); + // If changes should exist in ReviewDb, it's worth warning about a meta ref with no + // corresponding ReviewDb data. But stray patch set refs can happen due to normal error + // conditions, e.g. failed push processing, so aren't worth even a warning. + if (sr.fromMetaRefs().contains(id)) { + log.warn("skipping change {} found in project {} but not in ReviewDb", id, project); + } continue; } // TODO(dborowitz): See discussion in BatchUpdate#newChangeContext. @@ -346,16 +366,27 @@ return changeNotes; } - public static Set<Change.Id> scan(Repository repo) throws IOException { - Map<String, Ref> refs = repo.getRefDatabase().getRefs(RefNames.REFS_CHANGES); - Set<Change.Id> ids = new HashSet<>(refs.size()); - for (Ref r : refs.values()) { + @AutoValue + abstract static class ScanResult { + abstract ImmutableSet<Change.Id> fromPatchSetRefs(); + + abstract ImmutableSet<Change.Id> fromMetaRefs(); + + SetView<Change.Id> all() { + return Sets.union(fromPatchSetRefs(), fromMetaRefs()); + } + } + + private static ScanResult scanChangeIds(Repository repo) throws IOException { + ImmutableSet.Builder<Change.Id> fromPs = ImmutableSet.builder(); + ImmutableSet.Builder<Change.Id> fromMeta = ImmutableSet.builder(); + for (Ref r : repo.getRefDatabase().getRefs(RefNames.REFS_CHANGES).values()) { Change.Id id = Change.Id.fromRef(r.getName()); if (id != null) { - ids.add(id); + (r.getName().endsWith(RefNames.META_SUFFIX) ? fromMeta : fromPs).add(id); } } - return ids; + return new AutoValue_ChangeNotes_Factory_ScanResult(fromPs.build(), fromMeta.build()); } } @@ -428,6 +459,21 @@ return state.reviewers(); } + /** @return reviewers that do not currently have a Gerrit account and were added by email. */ + public ReviewerByEmailSet getReviewersByEmail() { + return state.reviewersByEmail(); + } + + /** @return reviewers that were modified during this change's current WIP phase. */ + public ReviewerSet getPendingReviewers() { + return state.pendingReviewers(); + } + + /** @return reviewers by email that were modified during this change's current WIP phase. */ + public ReviewerByEmailSet getPendingReviewersByEmail() { + return state.pendingReviewersByEmail(); + } + public ImmutableList<ReviewerStatusUpdate> getReviewerUpdates() { return state.reviewerUpdates(); } @@ -563,6 +609,24 @@ return state.readOnlyUntil(); } + public boolean isPrivate() { + if (state.isPrivate() == null) { + return false; + } + return state.isPrivate(); + } + + public boolean isWorkInProgress() { + if (state.isWorkInProgress() == null) { + return false; + } + return state.isWorkInProgress(); + } + + public boolean hasReviewStarted() { + return state.hasReviewStarted(); + } + @Override protected void onLoad(LoadHandle handle) throws NoSuchChangeException, IOException, ConfigInvalidException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java index 9626911..507b652 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -17,10 +17,13 @@ import com.google.auto.value.AutoValue; import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.Cache; +import com.google.common.collect.Table; import com.google.gerrit.common.Nullable; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.ReviewerByEmailSet; +import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.cache.CacheModule; import com.google.gerrit.server.notedb.AbstractChangeNotes.Args; import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk; @@ -111,6 +114,14 @@ + P + list(state.patchSets(), patchSet()) + P + + reviewerSet(state.reviewers(), 2) // REVIEWER or CC + + P + + reviewerSet(state.reviewersByEmail(), 2) // REVIEWER or CC + + P + + reviewerSet(state.pendingReviewers(), 3) // includes REMOVED + + P + + reviewerSet(state.pendingReviewersByEmail(), 3) // includes REMOVED + + P + list(state.allPastReviewers(), approval()) + P + list(state.reviewerUpdates(), 4 * O + K + K + P) @@ -122,7 +133,11 @@ + P + map(state.changeMessagesByPatchSet().asMap(), patchSetId()) + P - + map(state.publishedComments().asMap(), comment()); + + map(state.publishedComments().asMap(), comment()) + + T // readOnlyUntil + + 1 // isPrivate + + 1 // workInProgress + + 1; // hasReviewStarted } private static int ptr(Object o, int size) { @@ -176,6 +191,27 @@ return O + O + n * (P + elemSize); } + private static int hashBasedTable( + Table<?, ?, ?> table, int numRows, int rowKey, int columnKey, int elemSize) { + return O + + hashtable(numRows, rowKey + hashtable(0, 0)) + + hashtable(table.size(), columnKey + elemSize); + } + + private static int reviewerSet(ReviewerSet reviewers, int numRows) { + final int rowKey = 1; // ReviewerStateInternal + final int columnKey = K; // Account.Id + final int cellValue = T; // Timestamp + return hashBasedTable(reviewers.asTable(), numRows, rowKey, columnKey, cellValue); + } + + private static int reviewerSet(ReviewerByEmailSet reviewers, int numRows) { + final int rowKey = 1; // ReviewerStateInternal + final int columnKey = P + 2 * str(20); // name and email, just a guess + final int cellValue = T; // Timestamp + return hashBasedTable(reviewers.asTable(), numRows, rowKey, columnKey, cellValue); + } + private static int patchSet() { return O + P
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java index dac999c..eb366bb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -24,6 +24,7 @@ import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET_DESCRIPTION; +import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PRIVATE; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_READ_ONLY_UNTIL; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS; @@ -32,6 +33,7 @@ import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC; +import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_WORK_IN_PROGRESS; import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES; import static java.util.stream.Collectors.joining; @@ -40,6 +42,7 @@ import com.google.common.base.Splitter; import com.google.common.collect.HashBasedTable; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableTable; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Lists; @@ -62,8 +65,10 @@ import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.reviewdb.server.ReviewDbUtil; +import com.google.gerrit.server.ReviewerByEmailSet; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.ReviewerStatusUpdate; +import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk; import com.google.gerrit.server.util.LabelVote; import java.io.IOException; @@ -127,6 +132,7 @@ // Private final but mutable members initialized in the constructor and filled // in during the parsing process. private final Table<Account.Id, ReviewerStateInternal, Timestamp> reviewers; + private final Table<Address, ReviewerStateInternal, Timestamp> reviewersByEmail; private final List<Account.Id> allPastReviewers; private final List<ReviewerStatusUpdate> reviewerUpdates; private final List<SubmitRecord> submitRecords; @@ -157,6 +163,12 @@ private String tag; private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap; private Timestamp readOnlyUntil; + private Boolean isPrivate; + private Boolean workInProgress; + private Boolean previousWorkInProgressFooter; + private Boolean hasReviewStarted; + private ReviewerSet pendingReviewers; + private ReviewerByEmailSet pendingReviewersByEmail; ChangeNotesParser( Change.Id changeId, @@ -172,6 +184,9 @@ approvals = new LinkedHashMap<>(); bufferedApprovals = new ArrayList<>(); reviewers = HashBasedTable.create(); + reviewersByEmail = HashBasedTable.create(); + pendingReviewers = ReviewerSet.empty(); + pendingReviewersByEmail = ReviewerByEmailSet.empty(); allPastReviewers = new ArrayList<>(); reviewerUpdates = new ArrayList<>(); submitRecords = Lists.newArrayListWithExpectedSize(1); @@ -196,9 +211,17 @@ while ((commit = walk.next()) != null) { parse(commit); } + if (hasReviewStarted == null) { + if (previousWorkInProgressFooter == null) { + hasReviewStarted = true; + } else { + hasReviewStarted = !previousWorkInProgressFooter; + } + } parseNotes(); allPastReviewers.addAll(reviewers.rowKeySet()); pruneReviewers(); + pruneReviewersByEmail(); updatePatchSetStates(); checkMandatoryFooters(); @@ -232,13 +255,19 @@ patchSets, buildApprovals(), ReviewerSet.fromTable(Tables.transpose(reviewers)), + ReviewerByEmailSet.fromTable(Tables.transpose(reviewersByEmail)), + pendingReviewers, + pendingReviewersByEmail, allPastReviewers, buildReviewerUpdates(), submitRecords, buildAllMessages(), buildMessagesByPatchSet(), comments, - readOnlyUntil); + readOnlyUntil, + isPrivate, + workInProgress, + hasReviewStarted); } private PatchSet.Id buildCurrentPatchSetId() { @@ -371,6 +400,9 @@ for (String line : commit.getFooterLineValues(state.getFooterKey())) { parseReviewer(ts, state, line); } + for (String line : commit.getFooterLineValues(state.getByEmailFooterKey())) { + parseReviewerByEmail(ts, state, line); + } // Don't update timestamp when a reviewer was added, matching RevewDb // behavior. } @@ -379,6 +411,13 @@ parseReadOnlyUntil(commit); } + if (isPrivate == null) { + parseIsPrivate(commit); + } + + previousWorkInProgressFooter = null; + parseWorkInProgress(commit); + if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) { lastUpdatedOn = ts; } @@ -910,6 +949,19 @@ } } + private void parseReviewerByEmail(Timestamp ts, ReviewerStateInternal state, String line) + throws ConfigInvalidException { + Address adr; + try { + adr = Address.parse(line); + } catch (IllegalArgumentException e) { + throw invalidFooter(state.getByEmailFooterKey(), line); + } + if (!reviewersByEmail.containsRow(adr)) { + reviewersByEmail.put(adr, state, ts); + } + } + private void parseReadOnlyUntil(ChangeNotesCommit commit) throws ConfigInvalidException { String raw = parseOneFooter(commit, FOOTER_READ_ONLY_UNTIL); if (raw == null) { @@ -924,6 +976,52 @@ } } + private void parseIsPrivate(ChangeNotesCommit commit) throws ConfigInvalidException { + String raw = parseOneFooter(commit, FOOTER_PRIVATE); + if (raw == null) { + return; + } else if (Boolean.TRUE.toString().equalsIgnoreCase(raw)) { + isPrivate = true; + return; + } else if (Boolean.FALSE.toString().equalsIgnoreCase(raw)) { + isPrivate = false; + return; + } + throw invalidFooter(FOOTER_PRIVATE, raw); + } + + private void parseWorkInProgress(ChangeNotesCommit commit) throws ConfigInvalidException { + String raw = parseOneFooter(commit, FOOTER_WORK_IN_PROGRESS); + if (raw == null) { + // No change to WIP state in this revision. + previousWorkInProgressFooter = null; + return; + } else if (Boolean.TRUE.toString().equalsIgnoreCase(raw)) { + // This revision moves the change into WIP. + previousWorkInProgressFooter = true; + if (workInProgress == null) { + // Because this is the first time workInProgress is being set, we know + // that this change's current state is WIP. All the reviewer updates + // we've seen so far are pending, so take a snapshot of the reviewers + // and reviewersByEmail tables. + pendingReviewers = + ReviewerSet.fromTable(Tables.transpose(ImmutableTable.copyOf(reviewers))); + pendingReviewersByEmail = + ReviewerByEmailSet.fromTable(Tables.transpose(ImmutableTable.copyOf(reviewersByEmail))); + workInProgress = true; + } + return; + } else if (Boolean.FALSE.toString().equalsIgnoreCase(raw)) { + previousWorkInProgressFooter = false; + hasReviewStarted = true; + if (workInProgress == null) { + workInProgress = false; + } + return; + } + throw invalidFooter(FOOTER_WORK_IN_PROGRESS, raw); + } + private void pruneReviewers() { Iterator<Table.Cell<Account.Id, ReviewerStateInternal, Timestamp>> rit = reviewers.cellSet().iterator(); @@ -935,6 +1033,17 @@ } } + private void pruneReviewersByEmail() { + Iterator<Table.Cell<Address, ReviewerStateInternal, Timestamp>> rit = + reviewersByEmail.cellSet().iterator(); + while (rit.hasNext()) { + Table.Cell<Address, ReviewerStateInternal, Timestamp> e = rit.next(); + if (e.getColumnKey() == ReviewerStateInternal.REMOVED) { + rit.remove(); + } + } + } + private void updatePatchSetStates() { Set<PatchSet.Id> missing = new TreeSet<>(ReviewDbUtil.intKeyOrdering()); for (Iterator<PatchSet> it = patchSets.values().iterator(); it.hasNext(); ) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java index 7b25bbd..f9899e5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -34,6 +34,7 @@ import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RevId; +import com.google.gerrit.server.ReviewerByEmailSet; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.ReviewerStatusUpdate; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; @@ -65,13 +66,19 @@ ImmutableList.of(), ImmutableList.of(), ReviewerSet.empty(), + ReviewerByEmailSet.empty(), + ReviewerSet.empty(), + ReviewerByEmailSet.empty(), ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), ImmutableListMultimap.of(), ImmutableListMultimap.of(), - null); + null, + null, + null, + true); } static ChangeNotesState create( @@ -94,13 +101,19 @@ Map<PatchSet.Id, PatchSet> patchSets, ListMultimap<PatchSet.Id, PatchSetApproval> approvals, ReviewerSet reviewers, + ReviewerByEmailSet reviewersByEmail, + ReviewerSet pendingReviewers, + ReviewerByEmailSet pendingReviewersByEmail, List<Account.Id> allPastReviewers, List<ReviewerStatusUpdate> reviewerUpdates, List<SubmitRecord> submitRecords, List<ChangeMessage> allChangeMessages, ListMultimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet, ListMultimap<RevId, Comment> publishedComments, - @Nullable Timestamp readOnlyUntil) { + @Nullable Timestamp readOnlyUntil, + @Nullable Boolean isPrivate, + @Nullable Boolean workInProgress, + boolean hasReviewStarted) { if (hashtags == null) { hashtags = ImmutableSet.of(); } @@ -119,19 +132,28 @@ originalSubject, submissionId, assignee, - status), + status, + isPrivate, + workInProgress, + hasReviewStarted), ImmutableSet.copyOf(pastAssignees), ImmutableSet.copyOf(hashtags), ImmutableList.copyOf(patchSets.entrySet()), ImmutableList.copyOf(approvals.entries()), reviewers, + reviewersByEmail, + pendingReviewers, + pendingReviewersByEmail, ImmutableList.copyOf(allPastReviewers), ImmutableList.copyOf(reviewerUpdates), ImmutableList.copyOf(submitRecords), ImmutableList.copyOf(allChangeMessages), ImmutableListMultimap.copyOf(changeMessagesByPatchSet), ImmutableListMultimap.copyOf(publishedComments), - readOnlyUntil); + readOnlyUntil, + isPrivate, + workInProgress, + hasReviewStarted); } /** @@ -174,6 +196,15 @@ // TODO(dborowitz): Use a sensible default other than null @Nullable abstract Change.Status status(); + + @Nullable + abstract Boolean isPrivate(); + + @Nullable + abstract Boolean isWorkInProgress(); + + @Nullable + abstract Boolean hasReviewStarted(); } // Only null if NoteDb is disabled. @@ -197,6 +228,12 @@ abstract ReviewerSet reviewers(); + abstract ReviewerByEmailSet reviewersByEmail(); + + abstract ReviewerSet pendingReviewers(); + + abstract ReviewerByEmailSet pendingReviewersByEmail(); + abstract ImmutableList<Account.Id> allPastReviewers(); abstract ImmutableList<ReviewerStatusUpdate> reviewerUpdates(); @@ -212,6 +249,15 @@ @Nullable abstract Timestamp readOnlyUntil(); + @Nullable + abstract Boolean isPrivate(); + + @Nullable + abstract Boolean isWorkInProgress(); + + @Nullable + abstract Boolean hasReviewStarted(); + Change newChange(Project.NameKey project) { ChangeColumns c = checkNotNull(columns(), "columns are required"); Change change = @@ -269,6 +315,9 @@ change.setLastUpdatedOn(c.lastUpdatedOn()); change.setSubmissionId(c.submissionId()); change.setAssignee(c.assignee()); + change.setPrivate(c.isPrivate() == null ? false : c.isPrivate()); + change.setWorkInProgress(c.isWorkInProgress() == null ? false : c.isWorkInProgress()); + change.setReviewStarted(c.hasReviewStarted() == null ? false : c.hasReviewStarted()); if (!patchSets().isEmpty()) { change.setCurrentPatchSet(c.currentPatchSetId(), c.subject(), c.originalSubject());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java index 7af0cb4..fcde617 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -29,6 +29,7 @@ import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET_DESCRIPTION; +import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PRIVATE; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_READ_ONLY_UNTIL; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS; @@ -37,6 +38,7 @@ import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBMITTED_WITH; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TAG; import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC; +import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_WORK_IN_PROGRESS; import static com.google.gerrit.server.notedb.ChangeNoteUtil.sanitizeFooter; import static java.util.Comparator.comparing; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; @@ -60,6 +62,7 @@ import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.config.AnonymousCowardName; import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.util.LabelVote; @@ -121,12 +124,14 @@ } private final AccountCache accountCache; + private final NoteDbUpdateManager.Factory updateManagerFactory; private final ChangeDraftUpdate.Factory draftUpdateFactory; private final RobotCommentUpdate.Factory robotCommentUpdateFactory; - private final NoteDbUpdateManager.Factory updateManagerFactory; + private final DeleteCommentRewriter.Factory deleteCommentRewriterFactory; private final Table<String, Account.Id, Optional<Short>> approvals; private final Map<Account.Id, ReviewerStateInternal> reviewers = new LinkedHashMap<>(); + private final Map<Address, ReviewerStateInternal> reviewersByEmail = new LinkedHashMap<>(); private final List<Comment> comments = new ArrayList<>(); private String commitSubject; @@ -149,9 +154,12 @@ private String psDescription; private boolean currentPatchSet; private Timestamp readOnlyUntil; + private Boolean isPrivate; + private Boolean workInProgress; private ChangeDraftUpdate draftUpdate; private RobotCommentUpdate robotCommentUpdate; + private DeleteCommentRewriter deleteCommentRewriter; @AssistedInject private ChangeUpdate( @@ -163,6 +171,7 @@ NoteDbUpdateManager.Factory updateManagerFactory, ChangeDraftUpdate.Factory draftUpdateFactory, RobotCommentUpdate.Factory robotCommentUpdateFactory, + DeleteCommentRewriter.Factory deleteCommentRewriterFactory, ProjectCache projectCache, @Assisted ChangeControl ctl, ChangeNoteUtil noteUtil) { @@ -175,6 +184,7 @@ updateManagerFactory, draftUpdateFactory, robotCommentUpdateFactory, + deleteCommentRewriterFactory, projectCache, ctl, serverIdent.getWhen(), @@ -191,6 +201,7 @@ NoteDbUpdateManager.Factory updateManagerFactory, ChangeDraftUpdate.Factory draftUpdateFactory, RobotCommentUpdate.Factory robotCommentUpdateFactory, + DeleteCommentRewriter.Factory deleteCommentRewriterFactory, ProjectCache projectCache, @Assisted ChangeControl ctl, @Assisted Date when, @@ -204,6 +215,7 @@ updateManagerFactory, draftUpdateFactory, robotCommentUpdateFactory, + deleteCommentRewriterFactory, ctl, when, projectCache.get(getProjectName(ctl)).getLabelTypes().nameComparator(), @@ -229,15 +241,17 @@ NoteDbUpdateManager.Factory updateManagerFactory, ChangeDraftUpdate.Factory draftUpdateFactory, RobotCommentUpdate.Factory robotCommentUpdateFactory, + DeleteCommentRewriter.Factory deleteCommentRewriterFactory, @Assisted ChangeControl ctl, @Assisted Date when, @Assisted Comparator<String> labelNameComparator, ChangeNoteUtil noteUtil) { super(cfg, migration, ctl, serverIdent, anonymousCowardName, noteUtil, when); this.accountCache = accountCache; + this.updateManagerFactory = updateManagerFactory; this.draftUpdateFactory = draftUpdateFactory; this.robotCommentUpdateFactory = robotCommentUpdateFactory; - this.updateManagerFactory = updateManagerFactory; + this.deleteCommentRewriterFactory = deleteCommentRewriterFactory; this.approvals = approvals(labelNameComparator); } @@ -251,6 +265,7 @@ NoteDbUpdateManager.Factory updateManagerFactory, ChangeDraftUpdate.Factory draftUpdateFactory, RobotCommentUpdate.Factory robotCommentUpdateFactory, + DeleteCommentRewriter.Factory deleteCommentRewriterFactory, ChangeNoteUtil noteUtil, @Assisted Change change, @Assisted("effective") @Nullable Account.Id accountId, @@ -274,6 +289,7 @@ this.draftUpdateFactory = draftUpdateFactory; this.robotCommentUpdateFactory = robotCommentUpdateFactory; this.updateManagerFactory = updateManagerFactory; + this.deleteCommentRewriterFactory = deleteCommentRewriterFactory; this.approvals = approvals(labelNameComparator); } @@ -388,6 +404,11 @@ createDraftUpdateIfNull().deleteComment(c); } + public void deleteCommentByRewritingHistory(String uuid, String newMessage) { + deleteCommentRewriter = + deleteCommentRewriterFactory.create(getChange().getId(), uuid, newMessage); + } + @VisibleForTesting ChangeDraftUpdate createDraftUpdateIfNull() { if (draftUpdate == null) { @@ -469,6 +490,15 @@ reviewers.put(reviewer, ReviewerStateInternal.REMOVED); } + public void putReviewerByEmail(Address reviewer, ReviewerStateInternal type) { + checkArgument(type != ReviewerStateInternal.REMOVED, "invalid ReviewerType"); + reviewersByEmail.put(reviewer, type); + } + + public void removeReviewerByEmail(Address reviewer) { + reviewersByEmail.put(reviewer, ReviewerStateInternal.REMOVED); + } + public void setPatchSetState(PatchSetState psState) { this.psState = psState; } @@ -581,6 +611,8 @@ @Override protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr) throws OrmException, IOException { + checkState(deleteCommentRewriter == null, "cannot update and rewrite ref in one BatchUpdate"); + CommitBuilder cb = new CommitBuilder(); int ps = psId != null ? psId.get() : getChange().currentPatchSetId().get(); @@ -658,6 +690,10 @@ addIdent(msg, e.getKey()).append('\n'); } + for (Map.Entry<Address, ReviewerStateInternal> e : reviewersByEmail.entrySet()) { + addFooter(msg, e.getValue().getByEmailFooterKey(), e.getKey().toString()); + } + for (Table.Cell<String, Account.Id, Optional<Short>> c : approvals.cellSet()) { addFooter(msg, FOOTER_LABEL); // Label names/values are safe to append without sanitizing. @@ -711,6 +747,14 @@ addFooter(msg, FOOTER_READ_ONLY_UNTIL, ChangeNoteUtil.formatTime(serverIdent, readOnlyUntil)); } + if (isPrivate != null) { + addFooter(msg, FOOTER_PRIVATE, isPrivate); + } + + if (workInProgress != null) { + addFooter(msg, FOOTER_WORK_IN_PROGRESS, workInProgress); + } + cb.setMessage(msg.toString()); try { ObjectId treeId = storeRevisionNotes(rw, ins, curr); @@ -743,6 +787,7 @@ && changeMessage == null && comments.isEmpty() && reviewers.isEmpty() + && reviewersByEmail.isEmpty() && changeId == null && branch == null && status == null @@ -757,7 +802,9 @@ && tag == null && psDescription == null && !currentPatchSet - && readOnlyUntil == null; + && readOnlyUntil == null + && isPrivate == null + && workInProgress == null; } ChangeDraftUpdate getDraftUpdate() { @@ -768,6 +815,10 @@ return robotCommentUpdate; } + public DeleteCommentRewriter getDeleteCommentRewriter() { + return deleteCommentRewriter; + } + public void setAllowWriteToNewRef(boolean allow) { isAllowWriteToNewtRef = allow; } @@ -777,6 +828,14 @@ return isAllowWriteToNewtRef; } + public void setPrivate(boolean isPrivate) { + this.isPrivate = isPrivate; + } + + public void setWorkInProgress(boolean workInProgress) { + this.workInProgress = workInProgress; + } + void setReadOnlyUntil(Timestamp readOnlyUntil) { this.readOnlyUntil = readOnlyUntil; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java index c0b0525..06826ae 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
@@ -17,13 +17,12 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES; -import com.google.common.collect.ImmutableSet; +import com.google.auto.value.AutoValue; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Singleton; -import java.util.Set; import org.eclipse.jgit.lib.Config; /** @@ -32,7 +31,7 @@ * <p>This class controls the state of the migration according to options in {@code gerrit.config}. * In general, any changes to these options should only be made by adventurous administrators, who * know what they're doing, on non-production data, for the purposes of testing the NoteDb - * implementation. Changing options quite likely requires re-running {@code RebuildNoteDb}. For + * implementation. Changing options quite likely requires re-running {@code MigrateToNoteDb}. For * these reasons, the options remain undocumented. */ @Singleton @@ -44,89 +43,142 @@ } } - private static final String NOTE_DB = "noteDb"; + public static final String SECTION_NOTE_DB = "noteDb"; - // All of these names must be reflected in the allowed set in checkConfig. private static final String DISABLE_REVIEW_DB = "disableReviewDb"; + private static final String FUSE_UPDATES = "fuseUpdates"; private static final String PRIMARY_STORAGE = "primaryStorage"; private static final String READ = "read"; private static final String SEQUENCE = "sequence"; private static final String WRITE = "write"; - private static void checkConfig(Config cfg) { - Set<String> keys = ImmutableSet.of(CHANGES.key()); - Set<String> allowed = - ImmutableSet.of( - DISABLE_REVIEW_DB.toLowerCase(), - PRIMARY_STORAGE.toLowerCase(), - READ.toLowerCase(), - WRITE.toLowerCase(), - SEQUENCE.toLowerCase()); - for (String t : cfg.getSubsections(NOTE_DB)) { - checkArgument(keys.contains(t.toLowerCase()), "invalid NoteDb table: %s", t); - for (String key : cfg.getNames(NOTE_DB, t)) { - checkArgument(allowed.contains(key.toLowerCase()), "invalid NoteDb key: %s.%s", t, key); - } - } - } - public static Config allEnabledConfig() { Config cfg = new Config(); - cfg.setBoolean(NOTE_DB, CHANGES.key(), WRITE, true); - cfg.setBoolean(NOTE_DB, CHANGES.key(), READ, true); + cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), WRITE, true); + cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), READ, true); + cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), SEQUENCE, true); + cfg.setString(SECTION_NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, PrimaryStorage.NOTE_DB.name()); + cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), DISABLE_REVIEW_DB, true); + // TODO(dborowitz): Set to true when FileRepository supports it. + cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), FUSE_UPDATES, false); return cfg; } - private final boolean writeChanges; - private final boolean readChanges; - private final boolean readChangeSequence; - private final PrimaryStorage changePrimaryStorage; - private final boolean disableChangeReviewDb; + public static void setConfigValues(Config cfg, NotesMigration migration) { + cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), WRITE, migration.rawWriteChangesSetting()); + cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), READ, migration.readChanges()); + cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), SEQUENCE, migration.readChangeSequence()); + cfg.setEnum(SECTION_NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, migration.changePrimaryStorage()); + cfg.setBoolean( + SECTION_NOTE_DB, CHANGES.key(), DISABLE_REVIEW_DB, migration.disableChangeReviewDb()); + cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), FUSE_UPDATES, migration.fuseUpdates()); + } + + public static String toText(NotesMigration migration) { + Config cfg = new Config(); + setConfigValues(cfg, migration); + return cfg.toText(); + } + + @AutoValue + abstract static class Snapshot { + static Snapshot create(Config cfg) { + boolean writeChanges = cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), WRITE, false); + boolean readChanges = cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), READ, false); + + // Reading change sequence numbers from NoteDb is not the default even if + // reading changes themselves is. Once this is enabled, it's not easy to + // undo: ReviewDb might hand out numbers that have already been assigned by + // NoteDb. This decision for the default may be reevaluated later. + boolean readChangeSequence = cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), SEQUENCE, false); + + PrimaryStorage changePrimaryStorage = + cfg.getEnum(SECTION_NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, PrimaryStorage.REVIEW_DB); + boolean disableChangeReviewDb = + cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), DISABLE_REVIEW_DB, false); + boolean fuseUpdates = cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), FUSE_UPDATES, false); + + checkArgument( + !(disableChangeReviewDb && changePrimaryStorage != PrimaryStorage.NOTE_DB), + "cannot disable ReviewDb for changes if default change primary storage is ReviewDb"); + + return new AutoValue_ConfigNotesMigration_Snapshot( + writeChanges, + readChanges, + readChangeSequence, + changePrimaryStorage, + disableChangeReviewDb, + fuseUpdates); + } + + abstract boolean writeChanges(); + + abstract boolean readChanges(); + + abstract boolean readChangeSequence(); + + abstract PrimaryStorage changePrimaryStorage(); + + abstract boolean disableChangeReviewDb(); + + abstract boolean fuseUpdates(); + } + + private volatile Snapshot snapshot; @Inject - ConfigNotesMigration(@GerritServerConfig Config cfg) { - checkConfig(cfg); - - writeChanges = cfg.getBoolean(NOTE_DB, CHANGES.key(), WRITE, false); - readChanges = cfg.getBoolean(NOTE_DB, CHANGES.key(), READ, false); - - // Reading change sequence numbers from NoteDb is not the default even if - // reading changes themselves is. Once this is enabled, it's not easy to - // undo: ReviewDb might hand out numbers that have already been assigned by - // NoteDb. This decision for the default may be reevaluated later. - readChangeSequence = cfg.getBoolean(NOTE_DB, CHANGES.key(), SEQUENCE, false); - - changePrimaryStorage = - cfg.getEnum(NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, PrimaryStorage.REVIEW_DB); - disableChangeReviewDb = cfg.getBoolean(NOTE_DB, CHANGES.key(), DISABLE_REVIEW_DB, false); - - checkArgument( - !(disableChangeReviewDb && changePrimaryStorage != PrimaryStorage.NOTE_DB), - "cannot disable ReviewDb for changes if default change primary storage is ReviewDb"); + public ConfigNotesMigration(@GerritServerConfig Config cfg) { + this.snapshot = Snapshot.create(cfg); } @Override - protected boolean writeChanges() { - return writeChanges; + public boolean rawWriteChangesSetting() { + return snapshot.writeChanges(); } @Override public boolean readChanges() { - return readChanges; + return snapshot.readChanges(); } @Override public boolean readChangeSequence() { - return readChangeSequence; + return snapshot.readChangeSequence(); } @Override public PrimaryStorage changePrimaryStorage() { - return changePrimaryStorage; + return snapshot.changePrimaryStorage(); } @Override public boolean disableChangeReviewDb() { - return disableChangeReviewDb; + return snapshot.disableChangeReviewDb(); + } + + @Override + public boolean fuseUpdates() { + return snapshot.fuseUpdates(); + } + + /** + * Set the in-memory values returned by this instance to match another instance. + * + * <p>This method is only intended for use by {@link + * com.google.gerrit.server.notedb.rebuild.NoteDbMigrator}. + * + * <p>This <em>only</em> modifies the in-memory state; if this instance was initialized from a + * file-based config, the underlying storage is not updated. Callers are responsible for managing + * the underlying storage on their own. This method is synchronized to aid in such + * implementations. + * + * @see NotesMigration#setFrom(NotesMigration) + */ + @Override + public synchronized ConfigNotesMigration setFrom(NotesMigration other) { + Config cfg = new Config(); + setConfigValues(cfg, other); + snapshot = Snapshot.create(cfg); + return this; } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java new file mode 100644 index 0000000..3264be2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
@@ -0,0 +1,239 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.Preconditions.checkArgument; +import static com.google.gerrit.reviewdb.client.PatchLineComment.Status.PUBLISHED; +import static java.util.stream.Collectors.toList; +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; + +import com.google.common.annotations.VisibleForTesting; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Comment; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.client.RevId; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.notes.NoteMap; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevSort; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Deletes a published comment from NoteDb by rewriting the commit history. Instead of deleting the + * whole comment, it just replaces the comment's message with a new message. + */ +public class DeleteCommentRewriter implements NoteDbRewriter { + + public interface Factory { + /** + * Creates a DeleteCommentRewriter instance. + * + * @param id the id of the change which contains the target comment. + * @param uuid the uuid of the target comment. + * @param newMessage the message used to replace the old message of the target comment. + * @return the DeleteCommentRewriter instance + */ + DeleteCommentRewriter create( + Change.Id id, @Assisted("uuid") String uuid, @Assisted("newMessage") String newMessage); + } + + private final ChangeNoteUtil noteUtil; + private final Change.Id changeId; + private final String uuid; + private final String newMessage; + + @Inject + DeleteCommentRewriter( + ChangeNoteUtil noteUtil, + @Assisted Change.Id changeId, + @Assisted("uuid") String uuid, + @Assisted("newMessage") String newMessage) { + this.noteUtil = noteUtil; + this.changeId = changeId; + this.uuid = uuid; + this.newMessage = newMessage; + } + + @Override + public String getRefName() { + return RefNames.changeMetaRef(changeId); + } + + @Override + public ObjectId rewriteCommitHistory(RevWalk revWalk, ObjectInserter inserter, ObjectId currTip) + throws IOException, ConfigInvalidException, OrmException { + checkArgument(!currTip.equals(ObjectId.zeroId())); + + // Walk from the first commit of the branch. + revWalk.reset(); + revWalk.markStart(revWalk.parseCommit(currTip)); + revWalk.sort(RevSort.REVERSE); + + ObjectReader reader = revWalk.getObjectReader(); + RevCommit newTipCommit = revWalk.next(); // The first commit will not be rewritten. + Map<String, Comment> parentComments = + getPublishedComments(noteUtil, changeId, reader, NoteMap.read(reader, newTipCommit)); + + boolean rewrite = false; + RevCommit originalCommit; + while ((originalCommit = revWalk.next()) != null) { + NoteMap noteMap = NoteMap.read(reader, originalCommit); + Map<String, Comment> currComments = getPublishedComments(noteUtil, changeId, reader, noteMap); + + if (!rewrite && currComments.containsKey(uuid)) { + rewrite = true; + } + + if (!rewrite) { + parentComments = currComments; + newTipCommit = originalCommit; + continue; + } + + List<Comment> putInComments = getPutInComments(parentComments, currComments); + List<Comment> deletedComments = getDeletedComments(parentComments, currComments); + newTipCommit = + revWalk.parseCommit( + rewriteCommit( + originalCommit, newTipCommit, inserter, reader, putInComments, deletedComments)); + parentComments = currComments; + } + + return newTipCommit; + } + + /** + * Gets all the comments which are presented at a commit. Note they include the comments put in by + * the previous commits. + */ + @VisibleForTesting + public static Map<String, Comment> getPublishedComments( + ChangeNoteUtil noteUtil, Change.Id changeId, ObjectReader reader, NoteMap noteMap) + throws IOException, ConfigInvalidException { + return RevisionNoteMap.parse(noteUtil, changeId, reader, noteMap, PUBLISHED) + .revisionNotes + .values() + .stream() + .flatMap(n -> n.getComments().stream()) + .collect(Collectors.toMap(c -> c.key.uuid, c -> c)); + } + + /** + * Gets the comments put in by the current commit. The message of the target comment will be + * replaced by the new message. + * + * @param parMap the comment map of the parent commit. + * @param curMap the comment map of the current commit. + * @return The comments put in by the current commit. + */ + private List<Comment> getPutInComments(Map<String, Comment> parMap, Map<String, Comment> curMap) { + List<Comment> comments = new ArrayList<>(); + for (String key : curMap.keySet()) { + if (!parMap.containsKey(key)) { + Comment comment = curMap.get(key); + if (key.equals(uuid)) { + comment.message = newMessage; + } + comments.add(comment); + } + } + return comments; + } + + /** + * Gets the comments deleted by the current commit. + * + * @param parMap the comment map of the parent commit. + * @param curMap the comment map of the current commit. + * @return The comments deleted by the current commit. + */ + private List<Comment> getDeletedComments( + Map<String, Comment> parMap, Map<String, Comment> curMap) { + return parMap + .entrySet() + .stream() + .filter(c -> !curMap.containsKey(c.getKey())) + .map(c -> c.getValue()) + .collect(toList()); + } + + /** + * Rewrites one commit. + * + * @param originalCommit the original commit to be rewritten. + * @param parentCommit the parent of the new commit. + * @param inserter the {@code ObjectInserter} for the rewrite process. + * @param reader the {@code ObjectReader} for the rewrite process. + * @param putInComments the comments put in by this commit. + * @param deletedComments the comments deleted by this commit. + * @return the {@code objectId} of the new commit. + * @throws IOException + * @throws ConfigInvalidException + */ + private ObjectId rewriteCommit( + RevCommit originalCommit, + RevCommit parentCommit, + ObjectInserter inserter, + ObjectReader reader, + List<Comment> putInComments, + List<Comment> deletedComments) + throws IOException, ConfigInvalidException { + RevisionNoteMap<ChangeRevisionNote> revNotesMap = + RevisionNoteMap.parse( + noteUtil, changeId, reader, NoteMap.read(reader, parentCommit), PUBLISHED); + RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(revNotesMap); + + for (Comment c : putInComments) { + cache.get(new RevId(c.revId)).putComment(c); + } + + for (Comment c : deletedComments) { + cache.get(new RevId(c.revId)).deleteComment(c.key); + } + + Map<RevId, RevisionNoteBuilder> builders = cache.getBuilders(); + for (Map.Entry<RevId, RevisionNoteBuilder> entry : builders.entrySet()) { + ObjectId objectId = ObjectId.fromString(entry.getKey().get()); + byte[] data = entry.getValue().build(noteUtil, noteUtil.getWriteJson()); + if (data.length == 0) { + revNotesMap.noteMap.remove(objectId); + } else { + revNotesMap.noteMap.set(objectId, inserter.insert(OBJ_BLOB, data)); + } + } + + CommitBuilder cb = new CommitBuilder(); + cb.setParentId(parentCommit); + cb.setTreeId(revNotesMap.noteMap.writeTree(inserter)); + cb.setMessage(originalCommit.getFullMessage()); + cb.setCommitter(originalCommit.getCommitterIdent()); + cb.setAuthor(originalCommit.getAuthorIdent()); + cb.setEncoding(originalCommit.getEncoding()); + + return inserter.insert(cb); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/GwtormChangeBundleReader.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/GwtormChangeBundleReader.java index ee28d29..34ed64c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/GwtormChangeBundleReader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/GwtormChangeBundleReader.java
@@ -31,19 +31,15 @@ @Override public ChangeBundle fromReviewDb(ReviewDb db, Change.Id id) throws OrmException { - db.changes().beginTransaction(id); - try { - List<PatchSetApproval> approvals = db.patchSetApprovals().byChange(id).toList(); - return new ChangeBundle( - db.changes().get(id), - db.changeMessages().byChange(id), - db.patchSets().byChange(id), - approvals, - db.patchComments().byChange(id), - ReviewerSet.fromApprovals(approvals), - Source.REVIEW_DB); - } finally { - db.rollback(); - } + // TODO(dborowitz): Figure out how to do this more consistently, e.g. hand-written inner joins. + List<PatchSetApproval> approvals = db.patchSetApprovals().byChange(id).toList(); + return new ChangeBundle( + db.changes().get(id), + db.changeMessages().byChange(id), + db.patchSets().byChange(id), + approvals, + db.patchComments().byChange(id), + ReviewerSet.fromApprovals(approvals), + Source.REVIEW_DB); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java index fef7fdf..12967b8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
@@ -78,11 +78,11 @@ this.code = code; } - public static PrimaryStorage of(Change c) { + public static PrimaryStorage of(@Nullable Change c) { return of(NoteDbChangeState.parse(c)); } - public static PrimaryStorage of(NoteDbChangeState s) { + public static PrimaryStorage of(@Nullable NoteDbChangeState s) { return s != null ? s.getPrimaryStorage() : REVIEW_DB; } } @@ -150,12 +150,12 @@ } } - public static NoteDbChangeState parse(Change c) { + public static NoteDbChangeState parse(@Nullable Change c) { return c != null ? parse(c.getId(), c.getNoteDbState()) : null; } @VisibleForTesting - public static NoteDbChangeState parse(Change.Id id, String str) { + public static NoteDbChangeState parse(Change.Id id, @Nullable String str) { if (Strings.isNullOrEmpty(str)) { // Return null rather than Optional as this is what goes in the field in // ReviewDb.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java index d249689..c17fafd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -47,12 +47,14 @@ @Override public void configure() { - factory(ChangeUpdate.Factory.class); factory(ChangeDraftUpdate.Factory.class); + factory(ChangeUpdate.Factory.class); + factory(DeleteCommentRewriter.Factory.class); factory(DraftCommentNotes.Factory.class); - factory(RobotCommentUpdate.Factory.class); - factory(RobotCommentNotes.Factory.class); factory(NoteDbUpdateManager.Factory.class); + factory(RobotCommentNotes.Factory.class); + factory(RobotCommentUpdate.Factory.class); + if (!useTestBindings) { install(ChangeNotesCache.module()); if (cfg.getBoolean("noteDb", null, "testRebuilderWrapper", false)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbRewriter.java new file mode 100644 index 0000000..3c7b0a3 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbRewriter.java
@@ -0,0 +1,39 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.gwtorm.server.OrmException; +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.revwalk.RevWalk; + +public interface NoteDbRewriter { + + /** Gets the name of the target ref which will be rewritten. */ + String getRefName(); + + /** + * Rewrites the commit history. + * + * @param revWalk a {@code RevWalk} instance. + * @param inserter a {@code ObjectInserter} instance. + * @param currTip the {@code ObjectId} of the ref's tip commit. + * @return the {@code ObjectId} of the ref's new tip commit. + */ + ObjectId rewriteCommitHistory(RevWalk revWalk, ObjectInserter inserter, ObjectId currTip) + throws IOException, ConfigInvalidException, OrmException; +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbTable.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbTable.java index 255998c..be24e28 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbTable.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbTable.java
@@ -14,11 +14,11 @@ package com.google.gerrit.server.notedb; -enum NoteDbTable { +public enum NoteDbTable { ACCOUNTS, CHANGES; - String key() { + public String key() { return name().toLowerCase(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java index 59d7cbb..45cf244 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -38,14 +38,14 @@ import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.InMemoryInserter; import com.google.gerrit.server.git.InsertedObject; -import com.google.gerrit.server.git.LockFailureException; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.update.ChainedReceiveCommands; +import com.google.gerrit.server.update.RefUpdateUtil; import com.google.gwtorm.server.OrmConcurrencyException; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.io.IOException; import java.util.Collection; import java.util.HashMap; @@ -53,8 +53,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.BatchRefUpdate; -import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.ObjectReader; @@ -62,6 +62,7 @@ import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.PushCertificate; import org.eclipse.jgit.transport.ReceiveCommand; /** @@ -168,11 +169,15 @@ } void flush() throws IOException { + flushToFinalInserter(); + finalIns.flush(); + } + + void flushToFinalInserter() throws IOException { checkState(finalIns != null); for (InsertedObject obj : tempIns.getInsertedObjects()) { finalIns.insert(obj.type(), obj.data().toByteArray()); } - finalIns.flush(); tempIns.clear(); } @@ -198,6 +203,7 @@ private final ListMultimap<String, ChangeUpdate> changeUpdates; private final ListMultimap<String, ChangeDraftUpdate> draftUpdates; private final ListMultimap<String, RobotCommentUpdate> robotCommentUpdates; + private final ListMultimap<String, NoteDbRewriter> rewriters; private final Set<Change.Id> toDelete; private OpenRepo changeRepo; @@ -206,8 +212,9 @@ private boolean checkExpectedState = true; private String refLogMessage; private PersonIdent refLogIdent; + private PushCertificate pushCert; - @AssistedInject + @Inject NoteDbUpdateManager( @GerritPersonIdent Provider<PersonIdent> serverIdent, GitRepositoryManager repoManager, @@ -224,6 +231,7 @@ changeUpdates = MultimapBuilder.hashKeys().arrayListValues().build(); draftUpdates = MultimapBuilder.hashKeys().arrayListValues().build(); robotCommentUpdates = MultimapBuilder.hashKeys().arrayListValues().build(); + rewriters = MultimapBuilder.hashKeys().arrayListValues().build(); toDelete = new HashSet<>(); } @@ -273,6 +281,25 @@ return this; } + /** + * Set a push certificate for the push that originally triggered this NoteDb update. + * + * <p>The pusher will not necessarily have specified any of the NoteDb refs explicitly, such as + * when processing a push to {@code refs/for/master}. That's fine; this is just passed to the + * underlying {@link BatchRefUpdate}, and the implementation decides what to do with it. + * + * <p>The cert should be associated with the main repo. There is currently no way of associating a + * push cert with the {@code All-Users} repo, since it is not currently possible to update draft + * changes via push. + * + * @param pushCert push certificate; may be null. + * @return this + */ + public NoteDbUpdateManager setPushCertificate(PushCertificate pushCert) { + this.pushCert = pushCert; + return this; + } + public OpenRepo getChangeRepo() throws IOException { initChangeRepo(); return changeRepo; @@ -317,7 +344,14 @@ return changeUpdates.isEmpty() && draftUpdates.isEmpty() && robotCommentUpdates.isEmpty() - && toDelete.isEmpty(); + && rewriters.isEmpty() + && toDelete.isEmpty() + && !hasCommands(changeRepo) + && !hasCommands(allUsersRepo); + } + + private static boolean hasCommands(@Nullable OpenRepo or) { + return or != null && !or.cmds.isEmpty(); } /** @@ -344,6 +378,10 @@ if (rcu != null) { robotCommentUpdates.put(rcu.getRefName(), rcu); } + DeleteCommentRewriter deleteCommentRewriter = update.getDeleteCommentRewriter(); + if (deleteCommentRewriter != null) { + rewriters.put(deleteCommentRewriter.getRefName(), deleteCommentRewriter); + } } public void add(ChangeDraftUpdate draftUpdate) { @@ -385,6 +423,10 @@ Set<Change.Id> changeIds = new HashSet<>(); for (ReceiveCommand cmd : changeRepo.getCommandsSnapshot()) { Change.Id changeId = Change.Id.fromRef(cmd.getRefName()); + if (changeId == null || !cmd.getRefName().equals(RefNames.changeMetaRef(changeId))) { + // Not a meta ref update, likely due to a repo update along with the change meta update. + continue; + } changeIds.add(changeId); Optional<ObjectId> metaId = Optional.of(cmd.getNewId()); staged.put( @@ -450,13 +492,19 @@ } } - public void execute() throws OrmException, IOException { + @Nullable + public BatchRefUpdate execute() throws OrmException, IOException { + return execute(false); + } + + @Nullable + public BatchRefUpdate execute(boolean dryrun) throws OrmException, IOException { // Check before even inspecting the list, as this is a programmer error. if (migration.failChangeWrites()) { throw new OrmException(CHANGES_READ_ONLY); } if (isEmpty()) { - return; + return null; } try (Timer1.Context timer = metrics.updateLatency.start(CHANGES)) { stage(); @@ -468,36 +516,38 @@ // we may have stale draft comments. Doing it in this order allows stale // comments to be filtered out by ChangeNotes, reflecting the fact that // comments can only go from DRAFT to PUBLISHED, not vice versa. - execute(changeRepo); - execute(allUsersRepo); + BatchRefUpdate result = execute(changeRepo, dryrun, pushCert); + execute(allUsersRepo, dryrun, null); + return result; } finally { close(); } } - private void execute(OpenRepo or) throws IOException { + private BatchRefUpdate execute(OpenRepo or, boolean dryrun, @Nullable PushCertificate pushCert) + throws IOException { if (or == null || or.cmds.isEmpty()) { - return; + return null; } - or.flush(); + 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); bru.setRefLogMessage(firstNonNull(refLogMessage, "Update NoteDb refs"), false); bru.setRefLogIdent(refLogIdent != null ? refLogIdent : serverIdent.get()); or.cmds.addTo(bru); bru.setAllowNonFastForwards(true); - bru.execute(or.rw, NullProgressMonitor.INSTANCE); - boolean lockFailure = false; - for (ReceiveCommand cmd : bru.getCommands()) { - if (cmd.getResult() == ReceiveCommand.Result.LOCK_FAILURE) { - lockFailure = true; - } else if (cmd.getResult() != ReceiveCommand.Result.OK) { - throw new IOException("Update failed: " + bru); - } + if (!dryrun) { + RefUpdateUtil.executeChecked(bru, or.rw); } - if (lockFailure) { - throw new LockFailureException("Update failed with one or more lock failures: " + bru); - } + return bru; } private void addCommands() throws OrmException, IOException { @@ -515,6 +565,21 @@ if (!robotCommentUpdates.isEmpty()) { addUpdates(robotCommentUpdates, changeRepo); } + if (!rewriters.isEmpty()) { + Optional<String> conflictKey = + rewriters + .keySet() + .stream() + .filter(k -> (draftUpdates.containsKey(k) || robotCommentUpdates.containsKey(k))) + .findAny(); + if (conflictKey.isPresent()) { + throw new IllegalArgumentException( + String.format( + "cannot update and rewrite ref %s in one BatchUpdate", conflictKey.get())); + } + addRewrites(rewriters, changeRepo); + } + for (Change.Id id : toDelete) { doDelete(id); } @@ -635,6 +700,35 @@ } } + private static void addRewrites(ListMultimap<String, NoteDbRewriter> rewriters, OpenRepo openRepo) + throws OrmException, IOException { + for (Map.Entry<String, Collection<NoteDbRewriter>> entry : rewriters.asMap().entrySet()) { + String refName = entry.getKey(); + ObjectId oldTip = openRepo.cmds.get(refName).orElse(ObjectId.zeroId()); + + if (oldTip.equals(ObjectId.zeroId())) { + throw new OrmException(String.format("Ref %s is empty", refName)); + } + + ObjectId currTip = oldTip; + try { + for (NoteDbRewriter noteDbRewriter : entry.getValue()) { + ObjectId nextTip = + noteDbRewriter.rewriteCommitHistory(openRepo.rw, openRepo.tempIns, currTip); + if (nextTip != null) { + currTip = nextTip; + } + } + } catch (ConfigInvalidException e) { + throw new OrmException("Cannot rewrite commit history", e); + } + + if (!oldTip.equals(currTip)) { + openRepo.cmds.add(new ReceiveCommand(oldTip, currTip, refName)); + } + } + } + private static <U extends AbstractChangeUpdate> boolean allowWrite( Collection<U> updates, ObjectId old) { if (!old.equals(ObjectId.zeroId())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java index c708bfe..61bcf17 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -15,23 +15,36 @@ package com.google.gerrit.server.notedb; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; +import java.util.Objects; /** - * Holds the current state of the NoteDb migration. + * Current low-level settings of the NoteDb migration for changes. * - * <p>The migration will proceed one root entity type at a time. A <em>root entity</em> is an entity - * stored in ReviewDb whose key's {@code getParentKey()} method returns null. For an example of the - * entity hierarchy rooted at Change, see the diagram in {@code - * com.google.gerrit.reviewdb.client.Change}. + * <p>This class only describes the migration state of the {@link + * com.google.gerrit.reviewdb.client.Change Change} entity group, since it is possible for a given + * site to be in different states of the Change NoteDb migration process while staying at the same + * ReviewDb schema version. It does <em>not</em> describe the migration state of non-Change tables; + * those are automatically migrated using the ReviewDb schema migration process, so the NoteDb + * migration state at a given ReviewDb schema cannot vary. * - * <p>During a transitional period, each root entity group from ReviewDb may be either <em>written - * to</em> or <em>both written to and read from</em> NoteDb. + * <p>In many places, core Gerrit code should not directly care about the NoteDb migration state, + * and should prefer high-level APIs like {@link com.google.gerrit.server.ApprovalsUtil + * ApprovalsUtil} that don't require callers to inspect the migration state. The + * <em>implementation</em> of those utilities does care about the state, and should query the {@code + * NotesMigration} for the properties of the migration, for example, {@link #changePrimaryStorage() + * where new changes should be stored}. + * + * <p>Core Gerrit code is mostly interested in one facet of the migration at a time (reading or + * writing, say), but not all combinations of return values are supported or even make sense. * * <p>This class controls the state of the migration according to options in {@code gerrit.config}. * In general, any changes to these options should only be made by adventurous administrators, who * know what they're doing, on non-production data, for the purposes of testing the NoteDb - * implementation. Changing options quite likely requires re-running {@code RebuildNoteDb}. For + * implementation. Changing options quite likely requires re-running {@code MigrateToNoteDb}. For * these reasons, the options remain undocumented. + * + * <p><strong>Note:</strong> Callers should not assume the values returned by {@code + * NotesMigration}'s methods will not change in a running server. */ public abstract class NotesMigration { /** @@ -49,6 +62,9 @@ /** * Write changes to NoteDb. * + * <p>This method is awkwardly named because you should be using either {@link + * #commitChangeWrites()} or {@link #failChangeWrites()} instead. + * * <p>Updates to change data are written to NoteDb refs, but ReviewDb is still the source of * truth. Change data will not be written unless the NoteDb refs are already up to date, and the * write path will attempt to rebuild the change if not. @@ -57,7 +73,7 @@ * readChanges() = false}, writes to NoteDb are simply ignored; if {@code true}, any attempts to * write will generate an error. */ - protected abstract boolean writeChanges(); + public abstract boolean rawWriteChangesSetting(); /** * Read sequential change ID numbers from NoteDb. @@ -80,6 +96,31 @@ public abstract boolean disableChangeReviewDb(); /** + * Fuse meta ref updates in the same batch as code updates. + * + * <p>When set, each {@link com.google.gerrit.server.update.BatchUpdate} results in a single + * {@link org.eclipse.jgit.lib.BatchRefUpdate} to update both code and meta refs atomically. + * Setting this option with a repository backend that does not support atomic multi-ref + * transactions ({@link org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions()}) is a + * configuration error, and all updates will fail at runtime. + * + * <p>Has no effect if {@link #disableChangeReviewDb()} is false. + */ + public abstract boolean fuseUpdates(); + + /** + * Set the values returned by this instance to match another instance. + * + * <p>Optional operation: not all implementations support setting values after initialization. + * + * @param other other instance to copy values from. + * @return this. + */ + public NotesMigration setFrom(NotesMigration other) { + throw new UnsupportedOperationException(getClass().getSimpleName() + " is read-only"); + } + + /** * Whether to fail when reading any data from NoteDb. * * <p>Used in conjunction with {@link #readChanges()} for tests. @@ -88,7 +129,7 @@ return false; } - public boolean commitChangeWrites() { + public final boolean commitChangeWrites() { // It may seem odd that readChanges() without writeChanges() means we should // attempt to commit writes. However, this method is used by callers to know // whether or not they should short-circuit and skip attempting to read or @@ -99,14 +140,41 @@ // same codepath. This specific condition is used by the auto-rebuilding // path to rebuild a change and stage the results, but not commit them due // to failChangeWrites(). - return writeChanges() || readChanges(); + return rawWriteChangesSetting() || readChanges(); } - public boolean failChangeWrites() { - return !writeChanges() && readChanges(); + public final boolean failChangeWrites() { + return !rawWriteChangesSetting() && readChanges(); } - public boolean enabled() { - return writeChanges() || readChanges(); + public final boolean enabled() { + return rawWriteChangesSetting() || readChanges(); + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof NotesMigration)) { + return false; + } + NotesMigration m = (NotesMigration) o; + return readChanges() == m.readChanges() + && rawWriteChangesSetting() == m.rawWriteChangesSetting() + && readChangeSequence() == m.readChangeSequence() + && changePrimaryStorage() == m.changePrimaryStorage() + && disableChangeReviewDb() == m.disableChangeReviewDb() + && fuseUpdates() == m.fuseUpdates() + && failOnLoad() == m.failOnLoad(); + } + + @Override + public final int hashCode() { + return Objects.hash( + readChanges(), + rawWriteChangesSetting(), + readChangeSequence(), + changePrimaryStorage(), + disableChangeReviewDb(), + fuseUpdates(), + failOnLoad()); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigrationState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigrationState.java new file mode 100644 index 0000000..8c589d8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigrationState.java
@@ -0,0 +1,105 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.server.notedb.NoteDbChangeState.PrimaryStorage; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Possible high-level states of the NoteDb migration for changes. + * + * <p>This class describes the series of states required to migrate a site from ReviewDb-only to + * NoteDb-only. This process has several steps, and covers only a small subset of the theoretically + * possible combinations of {@link NotesMigration} return values. + * + * <p>These states are ordered: a one-way migration from ReviewDb to NoteDb will pass through states + * in the order in which they are defined. + */ +public enum NotesMigrationState { + REVIEW_DB(false, false, false, PrimaryStorage.REVIEW_DB, false, false), + + WRITE(false, true, false, PrimaryStorage.REVIEW_DB, false, false), + + READ_WRITE_NO_SEQUENCE(true, true, false, PrimaryStorage.REVIEW_DB, false, false), + + READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY( + true, true, true, PrimaryStorage.REVIEW_DB, false, false), + + READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY(true, true, true, PrimaryStorage.NOTE_DB, false, false), + + // TODO(dborowitz): This only exists as a separate state to support testing in different + // NoteDbModes. Once FileRepository fuses BatchRefUpdates, we won't have separate fused/unfused + // states. + NOTE_DB_UNFUSED(true, true, true, PrimaryStorage.NOTE_DB, true, false), + + NOTE_DB(true, true, true, PrimaryStorage.NOTE_DB, true, true); + + public static Optional<NotesMigrationState> forNotesMigration(NotesMigration migration) { + return Stream.of(values()).filter(s -> s.migration().equals(migration)).findFirst(); + } + + private final NotesMigration migration; + + NotesMigrationState( + // Arguments match abstract methods in NotesMigration. + boolean readChanges, + boolean rawWriteChangesSetting, + boolean readChangeSequence, + PrimaryStorage changePrimaryStorage, + boolean disableChangeReviewDb, + boolean fuseUpdates) { + this.migration = + new NotesMigration() { + @Override + public boolean readChanges() { + return readChanges; + } + + @Override + public boolean rawWriteChangesSetting() { + return rawWriteChangesSetting; + } + + @Override + public boolean readChangeSequence() { + return readChangeSequence; + } + + @Override + public PrimaryStorage changePrimaryStorage() { + return changePrimaryStorage; + } + + @Override + public boolean disableChangeReviewDb() { + return disableChangeReviewDb; + } + + @Override + public boolean fuseUpdates() { + return fuseUpdates; + } + }; + } + + public NotesMigration migration() { + return migration; + } + + public String toText() { + return ConfigNotesMigration.toText(migration); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java index 3f0db77..8503f0a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
@@ -52,6 +52,7 @@ import com.google.gerrit.server.update.BatchUpdate; import com.google.gerrit.server.update.BatchUpdateOp; import com.google.gerrit.server.update.ChangeContext; +import com.google.gerrit.server.update.RetryHelper; import com.google.gerrit.server.update.UpdateException; import com.google.gwtorm.server.AtomicUpdate; import com.google.gwtorm.server.OrmException; @@ -82,7 +83,6 @@ private static final Logger log = LoggerFactory.getLogger(PrimaryStorageMigrator.class); private final AllUsersName allUsers; - private final BatchUpdate.Factory batchUpdateFactory; private final ChangeControl.GenericFactory changeControlFactory; private final ChangeRebuilder rebuilder; private final ChangeUpdate.Factory updateFactory; @@ -90,6 +90,7 @@ private final InternalUser.Factory internalUserFactory; private final Provider<InternalChangeQuery> queryProvider; private final Provider<ReviewDb> db; + private final RetryHelper retryHelper; private final long skewMs; private final long timeoutMs; @@ -106,7 +107,7 @@ Provider<InternalChangeQuery> queryProvider, ChangeUpdate.Factory updateFactory, InternalUser.Factory internalUserFactory, - BatchUpdate.Factory batchUpdateFactory) { + RetryHelper retryHelper) { this( cfg, db, @@ -118,7 +119,7 @@ queryProvider, updateFactory, internalUserFactory, - batchUpdateFactory); + retryHelper); } @VisibleForTesting @@ -133,7 +134,7 @@ Provider<InternalChangeQuery> queryProvider, ChangeUpdate.Factory updateFactory, InternalUser.Factory internalUserFactory, - BatchUpdate.Factory batchUpdateFactory) { + RetryHelper retryHelper) { this.db = db; this.repoManager = repoManager; this.allUsers = allUsers; @@ -143,7 +144,7 @@ this.queryProvider = queryProvider; this.updateFactory = updateFactory; this.internalUserFactory = internalUserFactory; - this.batchUpdateFactory = batchUpdateFactory; + this.retryHelper = retryHelper; skewMs = NoteDbChangeState.getReadOnlySkew(cfg); String s = "notedb"; @@ -451,19 +452,27 @@ private void releaseReadOnlyLeaseInNoteDb(Project.NameKey project, Change.Id id) throws OrmException { // Use a BatchUpdate since ReviewDb is primary at this point, so it needs to reflect the update. - try (BatchUpdate bu = - batchUpdateFactory.create( - db.get(), project, internalUserFactory.create(), TimeUtil.nowTs())) { - bu.addOp( - id, - new BatchUpdateOp() { - @Override - public boolean updateChange(ChangeContext ctx) { - ctx.getUpdate(ctx.getChange().currentPatchSetId()).setReadOnlyUntil(new Timestamp(0)); - return true; + // (In practice retrying won't happen, since we aren't using fused updates at this point.) + try { + retryHelper.execute( + updateFactory -> { + try (BatchUpdate bu = + updateFactory.create( + db.get(), project, internalUserFactory.create(), TimeUtil.nowTs())) { + bu.addOp( + id, + new BatchUpdateOp() { + @Override + public boolean updateChange(ChangeContext ctx) { + ctx.getUpdate(ctx.getChange().currentPatchSetId()) + .setReadOnlyUntil(new Timestamp(0)); + return true; + } + }); + bu.execute(); + return null; } }); - bu.execute(); } catch (RestApiException | UpdateException e) { throw new OrmException(e); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java index 0b097d3..43813f8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RepoSequence.java
@@ -67,6 +67,7 @@ * numbers. */ public class RepoSequence { + @FunctionalInterface public interface Seed { int get() throws OrmException; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java index f250646..fad9832 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ReviewerStateInternal.java
@@ -29,13 +29,17 @@ /** The user was previously a reviewer on the change, but was removed. */ REMOVED(new FooterKey("Removed"), ReviewerState.REMOVED); + public static ReviewerStateInternal fromReviewerState(ReviewerState state) { + return ReviewerStateInternal.values()[state.ordinal()]; + } + static { boolean ok = true; if (ReviewerStateInternal.values().length != ReviewerState.values().length) { ok = false; } - for (ReviewerStateInternal s : ReviewerStateInternal.values()) { - ok &= s.name().equals(s.state.name()); + for (int i = 0; i < ReviewerStateInternal.values().length; i++) { + ok &= ReviewerState.values()[i].equals(ReviewerStateInternal.values()[i].state); } if (!ok) { throw new IllegalStateException( @@ -58,6 +62,10 @@ return footerKey; } + FooterKey getByEmailFooterKey() { + return new FooterKey(footerKey.getName() + "-email"); + } + public ReviewerState asReviewerState() { return state; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java index aec8442..deec7e9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RevisionNote.java
@@ -61,7 +61,7 @@ MutableInteger p = new MutableInteger(); trimLeadingEmptyLines(raw, p); if (p.value >= raw.length) { - comments = null; + comments = ImmutableList.of(); return; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java index e6549f0..99d9615 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentNotes.java
@@ -24,8 +24,8 @@ import com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.reviewdb.client.RobotComment; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.io.IOException; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.ObjectId; @@ -44,7 +44,7 @@ private RevisionNoteMap<RobotCommentsRevisionNote> revisionNoteMap; private ObjectId metaId; - @AssistedInject + @Inject RobotCommentNotes(Args args, @Assisted Change change) { super(args, change.getId(), PrimaryStorage.of(change), false); this.change = change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java index ad22330..29528f9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
@@ -35,11 +35,17 @@ "^Change has been successfully (merged|cherry-picked|rebased|pushed).*$"), Change.Status.NEW, Pattern.compile("^Restored(\n.*)*$")); + private static final Pattern PRIVATE_SET_REGEXP = Pattern.compile("^Set private$"); + private static final Pattern PRIVATE_UNSET_REGEXP = Pattern.compile("^Unset private$"); + private static final Pattern TOPIC_SET_REGEXP = Pattern.compile("^Topic set to (.+)$"); private static final Pattern TOPIC_CHANGED_REGEXP = Pattern.compile("^Topic changed from (.+) to (.+)$"); private static final Pattern TOPIC_REMOVED_REGEXP = Pattern.compile("^Topic (.+) removed$"); + private static final Pattern WIP_SET_REGEXP = Pattern.compile("^Set Work In Progress$"); + private static final Pattern WIP_UNSET_REGEXP = Pattern.compile("^Set Ready For Review$"); + private final Change change; private final Change noteDbChange; private final Optional<Change.Status> status; @@ -80,7 +86,9 @@ void apply(ChangeUpdate update) throws OrmException { checkUpdate(update); update.setChangeMessage(message.getMessage()); + setPrivate(update); setTopic(update); + setWorkInProgress(update); if (status.isPresent()) { Change.Status s = status.get(); @@ -106,6 +114,25 @@ return Optional.empty(); } + private void setPrivate(ChangeUpdate update) { + String msg = message.getMessage(); + if (msg == null) { + return; + } + Matcher m = PRIVATE_SET_REGEXP.matcher(msg); + if (m.matches()) { + update.setPrivate(true); + noteDbChange.setPrivate(true); + return; + } + + m = PRIVATE_UNSET_REGEXP.matcher(msg); + if (m.matches()) { + update.setPrivate(false); + noteDbChange.setPrivate(false); + } + } + private void setTopic(ChangeUpdate update) { String msg = message.getMessage(); if (msg == null) { @@ -133,6 +160,25 @@ } } + private void setWorkInProgress(ChangeUpdate update) { + String msg = message.getMessage(); + if (msg == null) { + return; + } + Matcher m = WIP_SET_REGEXP.matcher(msg); + if (m.matches()) { + update.setWorkInProgress(true); + noteDbChange.setWorkInProgress(true); + return; + } + + m = WIP_UNSET_REGEXP.matcher(msg); + if (m.matches()) { + update.setWorkInProgress(false); + noteDbChange.setWorkInProgress(false); + } + } + @Override protected void addToString(ToStringHelper helper) { helper.add("message", message);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java index 6f9090f..8ce9987 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
@@ -25,7 +25,6 @@ import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; import java.io.IOException; -import java.util.concurrent.Callable; public abstract class ChangeRebuilder { public static class NoPatchSetsException extends OrmException { @@ -43,14 +42,11 @@ } public final ListenableFuture<Result> rebuildAsync( - final Change.Id id, ListeningExecutorService executor) { + Change.Id id, ListeningExecutorService executor) { return executor.submit( - new Callable<Result>() { - @Override - public Result call() throws Exception { - try (ReviewDb db = schemaFactory.open()) { - return rebuild(db, id); - } + () -> { + try (ReviewDb db = schemaFactory.open()) { + return rebuild(db, id); } }); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java index c970cb0..9c7f3bd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -179,8 +179,8 @@ private Result rebuild(ReviewDb db, Change.Id changeId, boolean checkReadOnly) throws IOException, OrmException { db = ReviewDbUtil.unwrapDb(db); - // Read change just to get project; this instance is then discarded so we - // can read a consistent ChangeBundle inside a transaction. + // Read change just to get project; this instance is then discarded so we can read a consistent + // ChangeBundle inside a transaction. Change change = db.changes().get(changeId); if (change == null) { throw new NoSuchChangeException(changeId); @@ -247,36 +247,34 @@ throw new AbortUpdateException(); } else if (!Objects.equals(oldNoteDbState, currNoteDbState)) { // Another thread updated the state to something else. - throw new ConflictingUpdateException(change, oldNoteDbState); + throw new ConflictingUpdateRuntimeException(change, oldNoteDbState); } change.setNoteDbState(newNoteDbState); return change; } }); - } catch (ConflictingUpdateException e) { - // Rethrow as an OrmException so the caller knows to use staged results. - // Strictly speaking they are not completely up to date, but result we - // send to the caller is the same as if this rebuild had executed before - // the other thread. - throw new OrmException(e.getMessage()); + } catch (ConflictingUpdateRuntimeException e) { + // Rethrow as an OrmException so the caller knows to use staged results. Strictly speaking + // they are not completely up to date, but result we send to the caller is the same as if this + // rebuild had executed before the other thread. + throw new ConflictingUpdateException(e); } catch (AbortUpdateException e) { if (NoteDbChangeState.parse(changeId, newNoteDbState) .isUpToDate( manager.getChangeRepo().cmds.getRepoRefCache(), manager.getAllUsersRepo().cmds.getRepoRefCache())) { - // If the state in ReviewDb matches NoteDb at this point, it means - // another thread successfully completed this rebuild. It's ok to not - // execute the update in this case, since the object referenced in the - // Result was flushed to the repo by whatever thread won the race. + // If the state in ReviewDb matches NoteDb at this point, it means another thread + // successfully completed this rebuild. It's ok to not execute the update in this case, + // since the object referenced in the Result was flushed to the repo by whatever thread won + // the race. return r; } - // If the state doesn't match, that means another thread attempted this - // rebuild, but failed. Fall through and try to update the ref again. + // If the state doesn't match, that means another thread attempted this rebuild, but + // failed. Fall through and try to update the ref again. } if (migration.failChangeWrites()) { - // Don't even attempt to execute if read-only, it would fail anyway. But - // do throw an exception to the caller so they know to use the staged - // results instead of reading from the repo. + // Don't even attempt to execute if read-only, it would fail anyway. But do throw an exception + // to the caller so they know to use the staged results instead of reading from the repo. throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY); } manager.execute(); @@ -302,15 +300,15 @@ throw new NoPatchSetsException(change.getId()); } - // We will rebuild all events, except for draft comments, in buckets based - // on author and timestamp. + // We will rebuild all events, except for draft comments, in buckets based on author and + // timestamp. List<Event> events = new ArrayList<>(); ListMultimap<Account.Id, DraftCommentEvent> draftCommentEvents = MultimapBuilder.hashKeys().arrayListValues().build(); events.addAll(getHashtagsEvents(change, manager)); - // Delete ref only after hashtags have been read + // Delete ref only after hashtags have been read. deleteChangeMetaRef(change, manager.getChangeRepo().cmds); deleteDraftRefs(change, manager.getAllUsersRepo()); @@ -429,8 +427,8 @@ setPostSubmitDeps(events); new EventSorter(events).sort(); - // Ensure the first event in the list creates the change, setting the author - // and any required footers. + // Ensure the first event in the list creates the change, setting the author and any required + // footers. Event first = events.get(0); if (first instanceof PatchSetEvent && change.getOwner().equals(first.user)) { ((PatchSetEvent) first).createChange = true; @@ -440,22 +438,19 @@ // Final pass to correct some inconsistencies. // - // First, fill in any missing patch set IDs using the latest patch set of - // the change at the time of the event, because NoteDb can't represent - // actions with no associated patch set ID. This workaround is as if a user - // added a ChangeMessage on the change by replying from the latest patch - // set. + // First, fill in any missing patch set IDs using the latest patch set of the change at the time + // of the event, because NoteDb can't represent actions with no associated patch set ID. This + // workaround is as if a user added a ChangeMessage on the change by replying from the latest + // patch set. // - // Start with the first patch set that actually exists. If there are no - // patch sets at all, minPsNum will be null, so just bail and use 1 as the - // patch set ID. The corresponding patch set won't exist, but this change is - // probably corrupt anyway, as deleting the last draft patch set should have - // deleted the whole change. + // Start with the first patch set that actually exists. If there are no patch sets at all, + // minPsNum will be null, so just bail and use 1 as the patch set ID. The corresponding patch + // set won't exist, but this change is probably corrupt anyway, as deleting the last draft patch + // set should have deleted the whole change. // - // Second, ensure timestamps are nondecreasing, by copying the previous - // timestamp if this happens. This assumes that the only way this can happen - // is due to dependency constraints, and it is ok to give an event the same - // timestamp as one of its dependencies. + // Second, ensure timestamps are nondecreasing, by copying the previous timestamp if this + // happens. This assumes that the only way this can happen is due to dependency constraints, and + // it is ok to give an event the same timestamp as one of its dependencies. int ps = firstNonNull(minPsNum, 1); for (int i = 0; i < events.size(); i++) { Event e = events.get(i); @@ -492,8 +487,8 @@ if (projectCache != null) { labelNameComparator = projectCache.get(change.getProject()).getLabelTypes().nameComparator(); } else { - // No project cache available, bail and use natural ordering; there's no - // semantic difference anyway difference. + // No project cache available, bail and use natural ordering; there's no semantic difference + // anyway difference. labelNameComparator = Ordering.natural(); } ChangeUpdate update =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateException.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateException.java index c6ffffc..d8e7480 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateException.java
@@ -1,4 +1,4 @@ -// Copyright (C) 2016 The Android Open Source Project +// Copyright (C) 2017 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,16 +14,19 @@ package com.google.gerrit.server.notedb.rebuild; -import com.google.gerrit.reviewdb.client.Change; -import com.google.gwtorm.server.OrmRuntimeException; +import com.google.gwtorm.server.OrmException; -class ConflictingUpdateException extends OrmRuntimeException { +/** + * {@link com.google.gwtorm.server.OrmException} thrown by {@link ChangeRebuilder} when rebuilding a + * change failed because another operation modified its {@link + * com.google.gerrit.server.notedb.NoteDbChangeState}. + */ +public class ConflictingUpdateException extends OrmException { private static final long serialVersionUID = 1L; - ConflictingUpdateException(Change change, String expectedNoteDbState) { - super( - String.format( - "Expected change %s to have noteDbState %s but was %s", - change.getId(), expectedNoteDbState, change.getNoteDbState())); + // Always created from a ConflictingUpdateRuntimeException because it originates from an + // AtomicUpdate, which cannot throw checked exceptions. + ConflictingUpdateException(ConflictingUpdateRuntimeException cause) { + super(cause.getMessage(), cause); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateRuntimeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateRuntimeException.java new file mode 100644 index 0000000..abfafa2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ConflictingUpdateRuntimeException.java
@@ -0,0 +1,29 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.rebuild; + +import com.google.gerrit.reviewdb.client.Change; +import com.google.gwtorm.server.OrmRuntimeException; + +class ConflictingUpdateRuntimeException extends OrmRuntimeException { + private static final long serialVersionUID = 1L; + + ConflictingUpdateRuntimeException(Change change, String expectedNoteDbState) { + super( + String.format( + "Expected change %s to have noteDbState %s but was %s", + change.getId(), expectedNoteDbState, change.getNoteDbState())); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java index b1bd6ec..55d5a31 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/FinalUpdatesEvent.java
@@ -57,6 +57,12 @@ // TODO(dborowitz): Stamp approximate approvals at this time. update.fixStatus(change.getStatus()); } + if (change.isPrivate() != noteDbChange.isPrivate()) { + update.setPrivate(change.isPrivate()); + } + if (change.isWorkInProgress() != noteDbChange.isWorkInProgress()) { + update.setWorkInProgress(change.isWorkInProgress()); + } if (change.getSubmissionId() != null && noteDbChange.getSubmissionId() == null) { update.setSubmissionId(change.getSubmissionId()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java new file mode 100644 index 0000000..0cf78be --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/MigrationException.java
@@ -0,0 +1,30 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.rebuild; + +import java.io.IOException; + +/** Exception thrown by {@link NoteDbMigrator} when migration fails. */ +public class MigrationException extends IOException { + private static final long serialVersionUID = 1L; + + MigrationException(String message) { + super(message); + } + + MigrationException(String message, Throwable why) { + super(message, why); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java new file mode 100644 index 0000000..15e10a2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
@@ -0,0 +1,781 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.rebuild; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb; +import static com.google.gerrit.server.notedb.ConfigNotesMigration.SECTION_NOTE_DB; +import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB_UNFUSED; +import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_NO_SEQUENCE; +import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY; +import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY; +import static com.google.gerrit.server.notedb.NotesMigrationState.WRITE; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Stopwatch; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Ordering; +import com.google.common.collect.SetMultimap; +import com.google.common.collect.Streams; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.gerrit.common.FormatUtil; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.reviewdb.server.ReviewDbWrapper; +import com.google.gerrit.server.InternalUser; +import com.google.gerrit.server.Sequences; +import com.google.gerrit.server.config.AllProjectsName; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.LockFailureException; +import com.google.gerrit.server.git.WorkQueue; +import com.google.gerrit.server.notedb.ConfigNotesMigration; +import com.google.gerrit.server.notedb.NoteDbTable; +import com.google.gerrit.server.notedb.NotesMigration; +import com.google.gerrit.server.notedb.NotesMigrationState; +import com.google.gerrit.server.notedb.PrimaryStorageMigrator; +import com.google.gerrit.server.notedb.RepoSequence; +import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException; +import com.google.gerrit.server.util.ManualRequestContext; +import com.google.gerrit.server.util.ThreadLocalRequestContext; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.Inject; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Predicate; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.errors.RepositoryNotFoundException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.TextProgressMonitor; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.io.NullOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** One stop shop for migrating a site's change storage from ReviewDb to NoteDb. */ +public class NoteDbMigrator implements AutoCloseable { + private static final Logger log = LoggerFactory.getLogger(NoteDbMigrator.class); + + private static final String AUTO_MIGRATE = "autoMigrate"; + + public static boolean getAutoMigrate(Config cfg) { + return cfg.getBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), AUTO_MIGRATE, false); + } + + private static void setAutoMigrate(Config cfg, boolean autoMigrate) { + cfg.setBoolean(SECTION_NOTE_DB, NoteDbTable.CHANGES.key(), AUTO_MIGRATE, autoMigrate); + } + + public static class Builder { + private final Config cfg; + private final SitePaths sitePaths; + private final SchemaFactory<ReviewDb> schemaFactory; + private final GitRepositoryManager repoManager; + private final AllProjectsName allProjects; + private final InternalUser.Factory userFactory; + private final ThreadLocalRequestContext requestContext; + private final ChangeRebuilder rebuilder; + private final WorkQueue workQueue; + private final NotesMigration globalNotesMigration; + private final PrimaryStorageMigrator primaryStorageMigrator; + + private int threads; + private ImmutableList<Project.NameKey> projects = ImmutableList.of(); + private ImmutableList<Change.Id> changes = ImmutableList.of(); + private OutputStream progressOut = NullOutputStream.INSTANCE; + private NotesMigrationState stopAtState; + private boolean trial; + private boolean forceRebuild; + private int sequenceGap = -1; + private boolean autoMigrate; + + @Inject + Builder( + @GerritServerConfig Config cfg, + SitePaths sitePaths, + SchemaFactory<ReviewDb> schemaFactory, + GitRepositoryManager repoManager, + AllProjectsName allProjects, + ThreadLocalRequestContext requestContext, + InternalUser.Factory userFactory, + ChangeRebuilder rebuilder, + WorkQueue workQueue, + NotesMigration globalNotesMigration, + PrimaryStorageMigrator primaryStorageMigrator) { + this.cfg = cfg; + this.sitePaths = sitePaths; + this.schemaFactory = schemaFactory; + this.repoManager = repoManager; + this.allProjects = allProjects; + this.requestContext = requestContext; + this.userFactory = userFactory; + this.rebuilder = rebuilder; + this.workQueue = workQueue; + this.globalNotesMigration = globalNotesMigration; + this.primaryStorageMigrator = primaryStorageMigrator; + } + + /** + * Set the number of threads used by parallelizable phases of the migration, such as rebuilding + * all changes. + * + * <p>Not all phases are parallelizable, and calling {@link #rebuild()} directly will do + * substantial work in the calling thread regardless of the number of threads configured. + * + * <p>By default, all work is done in the calling thread. + * + * @param threads thread count; if less than 2, all work happens in the calling thread. + * @return this. + */ + public Builder setThreads(int threads) { + this.threads = threads; + return this; + } + + /** + * Limit the set of projects that are processed. + * + * <p>Incompatible with {@link #setChanges(Collection)}. + * + * <p>By default, all projects will be processed. + * + * @param projects set of projects; if null or empty, all projects will be processed. + * @return this. + */ + public Builder setProjects(@Nullable Collection<Project.NameKey> projects) { + this.projects = projects != null ? ImmutableList.copyOf(projects) : ImmutableList.of(); + return this; + } + + /** + * Limit the set of changes that are processed. + * + * <p>Incompatible with {@link #setProjects(Collection)}. + * + * <p>By default, all changes will be processed. + * + * @param changes set of changes; if null or empty, all changes will be processed. + * @return this. + */ + public Builder setChanges(@Nullable Collection<Change.Id> changes) { + this.changes = changes != null ? ImmutableList.copyOf(changes) : ImmutableList.of(); + return this; + } + + /** + * Set output stream for progress monitors. + * + * <p>By default, there is no progress monitor output (although there may be other logs). + * + * @param progressOut output stream. + * @return this. + */ + public Builder setProgressOut(OutputStream progressOut) { + this.progressOut = checkNotNull(progressOut); + return this; + } + + /** + * Stop at a specific migration state, for testing only. + * + * @param stopAtState state to stop at. + * @return this. + */ + @VisibleForTesting + public Builder setStopAtStateForTesting(NotesMigrationState stopAtState) { + this.stopAtState = stopAtState; + return this; + } + + /** + * Rebuild in "trial mode": configure Gerrit to write to and read from NoteDb, but leave + * ReviewDb as the source of truth for all changes. + * + * <p>By default, trial mode is off, and NoteDb is the source of truth for all changes following + * the migration. + * + * @param trial whether to rebuild in trial mode. + * @return this. + */ + public Builder setTrialMode(boolean trial) { + this.trial = trial; + return this; + } + + /** + * Rebuild all changes in NoteDb from ReviewDb, even if Gerrit is currently configured to read + * from NoteDb. + * + * <p>Only supported if ReviewDb is still the source of truth for all changes. + * + * <p>By default, force rebuilding is off. + * + * @param forceRebuild whether to force rebuilding. + * @return this. + */ + public Builder setForceRebuild(boolean forceRebuild) { + this.forceRebuild = forceRebuild; + return this; + } + + /** + * Gap between ReviewDb change sequence numbers and NoteDb. + * + * <p>If NoteDb sequences are enabled in a running server, there is a race between the migration + * step that calls {@code nextChangeId()} to seed the ref, and other threads that call {@code + * nextChangeId()} to create new changes. In order to prevent these operations stepping on one + * another, we use this value to skip some predefined sequence numbers. This is strongly + * recommended in a running server. + * + * <p>If the migration takes place offline, there is no race with other threads, and this option + * may be set to 0. However, admins may still choose to use a gap, for example to make it easier + * to distinguish changes that were created before and after the NoteDb migration. + * + * <p>By default, uses the value from {@code noteDb.changes.initialSequenceGap} in {@code + * gerrit.config}, which defaults to 1000. + * + * @param sequenceGap sequence gap size; if negative, use the default. + * @return this. + */ + public Builder setSequenceGap(int sequenceGap) { + this.sequenceGap = sequenceGap; + return this; + } + + /** + * Enable auto-migration on subsequent daemon launches. + * + * <p>If true, prior to running any migration steps, sets the necessary configuration in {@code + * gerrit.config} to make {@code gerrit.war daemon} retry the migration on next startup, if it + * fails. + * + * @param autoMigrate whether to set auto-migration config. + * @return this. + */ + public Builder setAutoMigrate(boolean autoMigrate) { + this.autoMigrate = autoMigrate; + return this; + } + + public NoteDbMigrator build() throws MigrationException { + return new NoteDbMigrator( + sitePaths, + schemaFactory, + repoManager, + allProjects, + requestContext, + userFactory, + rebuilder, + globalNotesMigration, + primaryStorageMigrator, + threads > 1 + ? MoreExecutors.listeningDecorator(workQueue.createQueue(threads, "RebuildChange")) + : MoreExecutors.newDirectExecutorService(), + projects, + changes, + progressOut, + stopAtState, + trial, + forceRebuild, + sequenceGap >= 0 ? sequenceGap : Sequences.getChangeSequenceGap(cfg), + autoMigrate); + } + } + + private final FileBasedConfig gerritConfig; + private final SchemaFactory<ReviewDb> schemaFactory; + private final GitRepositoryManager repoManager; + private final AllProjectsName allProjects; + private final ThreadLocalRequestContext requestContext; + private final InternalUser.Factory userFactory; + private final ChangeRebuilder rebuilder; + private final NotesMigration globalNotesMigration; + private final PrimaryStorageMigrator primaryStorageMigrator; + + private final ListeningExecutorService executor; + private final ImmutableList<Project.NameKey> projects; + private final ImmutableList<Change.Id> changes; + private final OutputStream progressOut; + private final NotesMigrationState stopAtState; + private final boolean trial; + private final boolean forceRebuild; + private final int sequenceGap; + private final boolean autoMigrate; + + private NoteDbMigrator( + SitePaths sitePaths, + SchemaFactory<ReviewDb> schemaFactory, + GitRepositoryManager repoManager, + AllProjectsName allProjects, + ThreadLocalRequestContext requestContext, + InternalUser.Factory userFactory, + ChangeRebuilder rebuilder, + NotesMigration globalNotesMigration, + PrimaryStorageMigrator primaryStorageMigrator, + ListeningExecutorService executor, + ImmutableList<Project.NameKey> projects, + ImmutableList<Change.Id> changes, + OutputStream progressOut, + NotesMigrationState stopAtState, + boolean trial, + boolean forceRebuild, + int sequenceGap, + boolean autoMigrate) + throws MigrationException { + if (!changes.isEmpty() && !projects.isEmpty()) { + throw new MigrationException("Cannot set both changes and projects"); + } + if (sequenceGap < 0) { + throw new MigrationException("Sequence gap must be non-negative: " + sequenceGap); + } + + this.schemaFactory = schemaFactory; + this.rebuilder = rebuilder; + this.repoManager = repoManager; + this.allProjects = allProjects; + this.requestContext = requestContext; + this.userFactory = userFactory; + this.globalNotesMigration = globalNotesMigration; + this.primaryStorageMigrator = primaryStorageMigrator; + this.gerritConfig = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.detect()); + this.executor = executor; + this.projects = projects; + this.changes = changes; + this.progressOut = progressOut; + this.stopAtState = stopAtState; + this.trial = trial; + this.forceRebuild = forceRebuild; + this.sequenceGap = sequenceGap; + this.autoMigrate = autoMigrate; + } + + @Override + public void close() { + executor.shutdownNow(); + } + + public void migrate() throws OrmException, IOException { + if (!changes.isEmpty() || !projects.isEmpty()) { + throw new MigrationException( + "Cannot set changes or projects during full migration; call rebuild() instead"); + } + Optional<NotesMigrationState> maybeState = loadState(); + if (!maybeState.isPresent()) { + throw new MigrationException("Could not determine initial migration state"); + } + + NotesMigrationState state = maybeState.get(); + if (trial && state.compareTo(READ_WRITE_NO_SEQUENCE) > 0) { + throw new MigrationException( + "Migration has already progressed past the endpoint of the \"trial mode\" state;" + + " NoteDb is already the primary storage for some changes"); + } + if (forceRebuild && state.compareTo(READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY) > 0) { + throw new MigrationException( + "Cannot force rebuild changes; NoteDb is already the primary storage for some changes"); + } + if (autoMigrate) { + if (trial) { + throw new MigrationException("Auto-migration cannot be used with trial mode"); + } + enableAutoMigrate(); + } + + boolean rebuilt = false; + while (state.compareTo(NOTE_DB_UNFUSED) < 0) { + if (state.equals(stopAtState)) { + return; + } + boolean stillNeedsRebuild = forceRebuild && !rebuilt; + if (trial && state.compareTo(READ_WRITE_NO_SEQUENCE) >= 0) { + if (stillNeedsRebuild && state == READ_WRITE_NO_SEQUENCE) { + // We're at the end state of trial mode, but still need a rebuild due to forceRebuild. Let + // the loop go one more time. + } else { + return; + } + } + switch (state) { + case REVIEW_DB: + state = turnOnWrites(state); + break; + case WRITE: + state = rebuildAndEnableReads(state); + rebuilt = true; + break; + case READ_WRITE_NO_SEQUENCE: + if (stillNeedsRebuild) { + state = rebuildAndEnableReads(state); + rebuilt = true; + } else { + state = enableSequences(state); + } + break; + case READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY: + if (stillNeedsRebuild) { + state = rebuildAndEnableReads(state); + rebuilt = true; + } else { + state = setNoteDbPrimary(state); + } + break; + case READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY: + // The only way we can get here is if there was a failure on a previous run of + // setNoteDbPrimary, since that method moves to NOTE_DB_UNFUSED if it completes + // successfully. Assume that not all changes were converted and re-run the step. + // migrateToNoteDbPrimary is a relatively fast no-op for already-migrated changes, so this + // isn't actually repeating work. + state = setNoteDbPrimary(state); + break; + case NOTE_DB_UNFUSED: + // Done! + break; + case NOTE_DB: + // TODO(dborowitz): Allow this state once FileRepository supports fused updates. + // Until then, fallthrough and throw. + default: + throw new MigrationException( + "Migration out of the following state is not supported:\n" + state.toText()); + } + } + } + + private NotesMigrationState turnOnWrites(NotesMigrationState prev) throws IOException { + return saveState(prev, WRITE); + } + + private NotesMigrationState rebuildAndEnableReads(NotesMigrationState prev) + throws OrmException, IOException { + rebuild(); + return saveState(prev, READ_WRITE_NO_SEQUENCE); + } + + private NotesMigrationState enableSequences(NotesMigrationState prev) + throws OrmException, IOException { + try (ReviewDb db = schemaFactory.open()) { + @SuppressWarnings("deprecation") + RepoSequence seq = + new RepoSequence( + repoManager, + allProjects, + Sequences.NAME_CHANGES, + // If sequenceGap is 0, this writes into the sequence ref the same ID that is returned + // by the call to seq.next() below. If we actually used this as a change ID, that + // would be a problem, but we just discard it, so this is safe. + () -> db.nextChangeId() + sequenceGap - 1, + 1); + seq.next(); + } + return saveState(prev, READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY); + } + + private NotesMigrationState setNoteDbPrimary(NotesMigrationState prev) + throws MigrationException, OrmException, IOException { + checkState( + projects.isEmpty() && changes.isEmpty(), + "Should not have attempted setNoteDbPrimary with a subset of changes"); + checkState( + prev == READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY + || prev == READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY, + "Unexpected start state for setNoteDbPrimary: %s", + prev); + + // Before changing the primary storage of old changes, ensure new changes are created with + // NoteDb primary. + prev = saveState(prev, READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY); + + Stopwatch sw = Stopwatch.createStarted(); + log.info("Setting primary storage to NoteDb"); + List<Change.Id> allChanges; + try (ReviewDb db = unwrapDb(schemaFactory.open())) { + allChanges = Streams.stream(db.changes().all()).map(Change::getId).collect(toList()); + } + + try (ContextHelper contextHelper = new ContextHelper()) { + List<ListenableFuture<Boolean>> futures = + allChanges + .stream() + .map( + id -> + executor.submit( + () -> { + try (ManualRequestContext ctx = contextHelper.open()) { + primaryStorageMigrator.migrateToNoteDbPrimary(id); + return true; + } catch (Exception e) { + log.error("Error migrating primary storage for " + id, e); + return false; + } + })) + .collect(toList()); + + boolean ok = futuresToBoolean(futures, "Error migrating primary storage"); + double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d; + log.info( + String.format( + "Migrated primary storage of %d changes in %.01fs (%.01f/s)\n", + allChanges.size(), t, allChanges.size() / t)); + if (!ok) { + throw new MigrationException("Migrating primary storage for some changes failed, see log"); + } + } + + return disableReviewDb(prev); + } + + private NotesMigrationState disableReviewDb(NotesMigrationState prev) throws IOException { + return saveState(prev, NOTE_DB_UNFUSED, c -> setAutoMigrate(c, false)); + } + + private Optional<NotesMigrationState> loadState() throws IOException { + try { + gerritConfig.load(); + return NotesMigrationState.forNotesMigration(new ConfigNotesMigration(gerritConfig)); + } catch (ConfigInvalidException | IllegalArgumentException e) { + log.warn("error reading NoteDb migration options from " + gerritConfig.getFile(), e); + return Optional.empty(); + } + } + + private NotesMigrationState saveState( + NotesMigrationState expectedOldState, NotesMigrationState newState) throws IOException { + return saveState(expectedOldState, newState, c -> {}); + } + + private NotesMigrationState saveState( + NotesMigrationState expectedOldState, + NotesMigrationState newState, + Consumer<Config> additionalUpdates) + throws IOException { + synchronized (globalNotesMigration) { + // This read-modify-write is racy. We're counting on the fact that no other Gerrit operation + // modifies gerrit.config, and hoping that admins don't either. + Optional<NotesMigrationState> actualOldState = loadState(); + if (!actualOldState.equals(Optional.of(expectedOldState))) { + throw new MigrationException( + "Cannot move to new state:\n" + + newState.toText() + + "\n\n" + + "Expected this state in gerrit.config:\n" + + expectedOldState.toText() + + "\n\n" + + (actualOldState.isPresent() + ? "But found this state:\n" + actualOldState.get().toText() + : "But could not parse the current state")); + } + ConfigNotesMigration.setConfigValues(gerritConfig, newState.migration()); + additionalUpdates.accept(gerritConfig); + gerritConfig.save(); + + // Only set in-memory state once it's been persisted to storage. + globalNotesMigration.setFrom(newState.migration()); + + return newState; + } + } + + private void enableAutoMigrate() throws MigrationException { + try { + gerritConfig.load(); + setAutoMigrate(gerritConfig, true); + gerritConfig.save(); + } catch (ConfigInvalidException | IOException e) { + throw new MigrationException("Error saving auto-migration config", e); + } + } + + public void rebuild() throws MigrationException, OrmException { + if (!globalNotesMigration.commitChangeWrites()) { + throw new MigrationException("Cannot rebuild without noteDb.changes.write=true"); + } + Stopwatch sw = Stopwatch.createStarted(); + log.info("Rebuilding changes in NoteDb"); + + List<ListenableFuture<Boolean>> futures = new ArrayList<>(); + try (ContextHelper contextHelper = new ContextHelper()) { + ImmutableListMultimap<Project.NameKey, Change.Id> changesByProject = + getChangesByProject(contextHelper.getReviewDb()); + List<Project.NameKey> projectNames = + Ordering.usingToString().sortedCopy(changesByProject.keySet()); + for (Project.NameKey project : projectNames) { + ListenableFuture<Boolean> future = + executor.submit( + () -> { + try { + return rebuildProject(contextHelper.getReviewDb(), changesByProject, project); + } catch (Exception e) { + log.error("Error rebuilding project " + project, e); + return false; + } + }); + futures.add(future); + } + + boolean ok = futuresToBoolean(futures, "Error rebuilding projects"); + double t = sw.elapsed(TimeUnit.MILLISECONDS) / 1000d; + log.info( + String.format( + "Rebuilt %d changes in %.01fs (%.01f/s)\n", + changesByProject.size(), t, changesByProject.size() / t)); + if (!ok) { + throw new MigrationException("Rebuilding some changes failed, see log"); + } + } + } + + private ImmutableListMultimap<Project.NameKey, Change.Id> getChangesByProject(ReviewDb db) + throws OrmException { + // Memoize all changes so we can close the db connection and allow other threads to use the full + // connection pool. + SetMultimap<Project.NameKey, Change.Id> out = + MultimapBuilder.treeKeys(comparing(Project.NameKey::get)) + .treeSetValues(comparing(Change.Id::get)) + .build(); + if (!projects.isEmpty()) { + return byProject(db.changes().all(), c -> projects.contains(c.getProject()), out); + } + if (!changes.isEmpty()) { + return byProject(db.changes().get(changes), c -> true, out); + } + return byProject(db.changes().all(), c -> true, out); + } + + private static ImmutableListMultimap<Project.NameKey, Change.Id> byProject( + Iterable<Change> changes, + Predicate<Change> pred, + SetMultimap<Project.NameKey, Change.Id> out) { + Streams.stream(changes).filter(pred).forEach(c -> out.put(c.getProject(), c.getId())); + return ImmutableListMultimap.copyOf(out); + } + + private boolean rebuildProject( + ReviewDb db, + ImmutableListMultimap<Project.NameKey, Change.Id> allChanges, + Project.NameKey project) { + checkArgument(allChanges.containsKey(project)); + boolean ok = true; + ProgressMonitor pm = + new TextProgressMonitor( + new PrintWriter(new BufferedWriter(new OutputStreamWriter(progressOut, UTF_8)))); + pm.beginTask(FormatUtil.elide(project.get(), 50), allChanges.get(project).size()); + try { + Collection<Change.Id> changes = allChanges.get(project); + for (Change.Id changeId : changes) { + // Update one change at a time, which ends up creating one NoteDbUpdateManager per change as + // well. This turns out to be no more expensive than batching, since each NoteDb operation + // is only writing single loose ref updates and loose objects. Plus we have to do one + // ReviewDb transaction per change due to the AtomicUpdate, so if we somehow batched NoteDb + // operations, ReviewDb would become the bottleneck. + try { + rebuilder.rebuild(db, changeId); + } catch (NoPatchSetsException e) { + log.warn(e.getMessage()); + } catch (RepositoryNotFoundException e) { + log.warn("Repository {} not found while rebuilding change {}", project, changeId); + } catch (ConflictingUpdateException e) { + log.warn( + "Rebuilding detected a conflicting ReviewDb update for change {};" + + " will be auto-rebuilt at runtime", + changeId); + } catch (LockFailureException e) { + log.warn( + "Rebuilding detected a conflicting NoteDb update for change {};" + + " will be auto-rebuilt at runtime", + changeId); + } catch (Throwable t) { + log.error("Failed to rebuild change " + changeId, t); + ok = false; + } + pm.update(1); + } + } finally { + pm.endTask(); + } + return ok; + } + + private static boolean futuresToBoolean(List<ListenableFuture<Boolean>> futures, String errMsg) { + try { + return Futures.allAsList(futures).get().stream().allMatch(b -> b); + } catch (InterruptedException | ExecutionException e) { + log.error(errMsg, e); + return false; + } + } + + private class ContextHelper implements AutoCloseable { + private final Thread callingThread; + private ReviewDb db; + + ContextHelper() { + callingThread = Thread.currentThread(); + } + + ManualRequestContext open() throws OrmException { + return new ManualRequestContext( + userFactory.create(), + // Reuse the same lazily-opened ReviewDb on the original calling thread, otherwise open + // SchemaFactory in the normal way. + Thread.currentThread().equals(callingThread) ? this::getReviewDb : schemaFactory, + requestContext); + } + + synchronized ReviewDb getReviewDb() throws OrmException { + if (db == null) { + db = + new ReviewDbWrapper(unwrapDb(schemaFactory.open())) { + @Override + public void close() { + // Closed by ContextHelper#close. + } + }; + } + return db; + } + + @Override + public synchronized void close() { + if (db != null) { + db.close(); + } + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java new file mode 100644 index 0000000..4d5951b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/OnlineNoteDbMigrator.java
@@ -0,0 +1,89 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.rebuild; + +import com.google.common.base.Stopwatch; +import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.index.OnlineUpgrader; +import com.google.gerrit.server.index.VersionManager; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; +import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.lib.Config; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class OnlineNoteDbMigrator implements LifecycleListener { + private static final Logger log = LoggerFactory.getLogger(OnlineNoteDbMigrator.class); + + public static class Module extends LifecycleModule { + @Override + public void configure() { + listener().to(OnlineNoteDbMigrator.class); + } + } + + private Provider<NoteDbMigrator.Builder> migratorBuilderProvider; + private final OnlineUpgrader indexUpgrader; + private final boolean upgradeIndex; + + @Inject + OnlineNoteDbMigrator( + @GerritServerConfig Config cfg, + Provider<NoteDbMigrator.Builder> migratorBuilderProvider, + OnlineUpgrader indexUpgrader) { + this.migratorBuilderProvider = migratorBuilderProvider; + this.indexUpgrader = indexUpgrader; + this.upgradeIndex = VersionManager.getOnlineUpgrade(cfg); + } + + @Override + public void start() { + Thread t = new Thread(this::migrate); + t.setDaemon(true); + t.setName(getClass().getSimpleName()); + t.start(); + } + + private void migrate() { + log.info("Starting online NoteDb migration"); + if (upgradeIndex) { + log.info("Online index schema upgrades will be deferred until NoteDb migration is complete"); + } + Stopwatch sw = Stopwatch.createStarted(); + // TODO(dborowitz): Tune threads, maybe expose a progress monitor somewhere. + try (NoteDbMigrator migrator = migratorBuilderProvider.get().setAutoMigrate(true).build()) { + migrator.migrate(); + } catch (Exception e) { + log.error("Error in online NoteDb migration", e); + } + log.info("Online NoteDb migration completed in {}s", sw.elapsed(TimeUnit.SECONDS)); + + if (upgradeIndex) { + log.info("Starting deferred index schema upgrades"); + indexUpgrader.start(); + } + } + + @Override + public void stop() { + // Do nothing; upgrade process uses daemon threads and knows how to recover from failures on + // next attempt. + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java index 74a3132..19568cf 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AutoMerger.java
@@ -77,7 +77,7 @@ public RevCommit merge( Repository repo, RevWalk rw, - final ObjectInserter ins, + ObjectInserter ins, RevCommit merge, ThreeWayMergeStrategy mergeStrategy) throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryKey.java index bfa7ec3..0a02e36 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryKey.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryKey.java
@@ -19,6 +19,7 @@ import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull; import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull; +import com.google.common.base.Preconditions; import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; import java.io.IOException; import java.io.ObjectInputStream; @@ -40,6 +41,7 @@ private transient Whitespace whitespace; public static DiffSummaryKey fromPatchListKey(PatchListKey plk) { + Preconditions.checkArgument(plk.getAlgorithm() == PatchListKey.Algorithm.OPTIMIZED_DIFF); return new DiffSummaryKey( plk.getOldId(), plk.getParentNum(), plk.getNewId(), plk.getWhitespace()); } @@ -52,7 +54,8 @@ } PatchListKey toPatchListKey() { - return new PatchListKey(oldId, parentNum, newId, whitespace); + return new PatchListKey( + oldId, parentNum, newId, whitespace, PatchListKey.Algorithm.OPTIMIZED_DIFF); } @Override @@ -61,7 +64,7 @@ } @Override - public boolean equals(final Object o) { + public boolean equals(Object o) { if (o instanceof DiffSummaryKey) { DiffSummaryKey k = (DiffSummaryKey) o; return Objects.equals(oldId, k.oldId) @@ -89,7 +92,7 @@ return n.toString(); } - private void writeObject(final ObjectOutputStream out) throws IOException { + private void writeObject(ObjectOutputStream out) throws IOException { writeCanBeNull(out, oldId); out.writeInt(parentNum == null ? 0 : parentNum); writeNotNull(out, newId); @@ -100,7 +103,7 @@ out.writeChar(c); } - private void readObject(final ObjectInputStream in) throws IOException { + private void readObject(ObjectInputStream in) throws IOException { oldId = readCanBeNull(in); int n = in.readInt(); parentNum = n == 0 ? null : Integer.valueOf(n);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryLoader.java index fa02691..188513f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryLoader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/DiffSummaryLoader.java
@@ -16,8 +16,8 @@ import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.Project; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -36,7 +36,7 @@ private final DiffSummaryKey key; private final Project.NameKey project; - @AssistedInject + @Inject DiffSummaryLoader(PatchListCache plc, @Assisted DiffSummaryKey k, @Assisted Project.NameKey p) { patchListCache = plc; key = k;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/EditTransformer.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/EditTransformer.java new file mode 100644 index 0000000..376dc51 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/EditTransformer.java
@@ -0,0 +1,295 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.Multimaps.toMultimap; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; + +import com.google.auto.value.AutoValue; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multimap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; +import org.eclipse.jgit.diff.Edit; + +/** + * Transformer of edits regarding their base trees. An edit describes a difference between {@code + * treeA} and {@code treeB}. This class allows to describe the edit as a difference between {@code + * treeA'} and {@code treeB'} given the transformation of {@code treeA} to {@code treeA'} and {@code + * treeB} to {@code treeB'}. Edits which can't be transformed due to conflicts with the + * transformation are omitted. + */ +class EditTransformer { + + private List<ContextAwareEdit> edits; + + /** + * Creates a new {@code EditTransformer} for the edits contained in the specified {@code + * PatchListEntry}s. + * + * @param patchListEntries a list of {@code PatchListEntry}s containing the edits + */ + public EditTransformer(List<PatchListEntry> patchListEntries) { + edits = patchListEntries.stream().flatMap(EditTransformer::toEdits).collect(toImmutableList()); + } + + /** + * Transforms the references of side A of the edits. If the edits describe differences between + * {@code treeA} and {@code treeB} and the specified {@code PatchListEntry}s define a + * transformation from {@code treeA} to {@code treeA'}, the resulting edits will be defined as + * differences between {@code treeA'} and {@code treeB}. Edits which can't be transformed due to + * conflicts with the transformation are omitted. + * + * @param transformationEntries a list of {@code PatchListEntry}s defining the transformation of + * {@code treeA} to {@code treeA'} + */ + public void transformReferencesOfSideA(List<PatchListEntry> transformationEntries) { + transformEdits(transformationEntries, SideAStrategy.INSTANCE); + } + + /** + * Transforms the references of side B of the edits. If the edits describe differences between + * {@code treeA} and {@code treeB} and the specified {@code PatchListEntry}s define a + * transformation from {@code treeB} to {@code treeB'}, the resulting edits will be defined as + * differences between {@code treeA} and {@code treeB'}. Edits which can't be transformed due to + * conflicts with the transformation are omitted. + * + * @param transformationEntries a list of {@code PatchListEntry}s defining the transformation of + * {@code treeB} to {@code treeB'} + */ + public void transformReferencesOfSideB(List<PatchListEntry> transformationEntries) { + transformEdits(transformationEntries, SideBStrategy.INSTANCE); + } + + /** + * Returns the transformed edits per file path they modify in {@code treeB'}. + * + * @return the transformed edits per file path + */ + public Multimap<String, ContextAwareEdit> getEditsPerFilePath() { + return edits + .stream() + .collect( + toMultimap( + ContextAwareEdit::getNewFilePath, Function.identity(), ArrayListMultimap::create)); + } + + public static Stream<ContextAwareEdit> toEdits(PatchListEntry patchListEntry) { + ImmutableList<Edit> edits = patchListEntry.getEdits(); + if (edits.isEmpty()) { + return Stream.of(ContextAwareEdit.createForNoContentEdit(patchListEntry)); + } + + return edits.stream().map(edit -> ContextAwareEdit.create(patchListEntry, edit)); + } + + private void transformEdits(List<PatchListEntry> transformingEntries, SideStrategy sideStrategy) { + Map<String, List<ContextAwareEdit>> editsPerFilePath = + edits.stream().collect(groupingBy(sideStrategy::getFilePath)); + Map<String, PatchListEntry> transformingEntryPerPath = + transformingEntries + .stream() + .collect(toMap(EditTransformer::getOldFilePath, Function.identity())); + + edits = + editsPerFilePath + .entrySet() + .stream() + .flatMap( + pathAndEdits -> { + PatchListEntry transformingEntry = + transformingEntryPerPath.get(pathAndEdits.getKey()); + return transformEdits(sideStrategy, pathAndEdits.getValue(), transformingEntry) + .stream(); + }) + .collect(toList()); + } + + private static String getOldFilePath(PatchListEntry patchListEntry) { + return MoreObjects.firstNonNull(patchListEntry.getOldName(), patchListEntry.getNewName()); + } + + private static List<ContextAwareEdit> transformEdits( + SideStrategy sideStrategy, + List<ContextAwareEdit> originalEdits, + PatchListEntry transformingEntry) { + if (transformingEntry == null) { + return originalEdits; + } + return transformEdits( + sideStrategy, originalEdits, transformingEntry.getEdits(), transformingEntry.getNewName()); + } + + private static List<ContextAwareEdit> transformEdits( + SideStrategy sideStrategy, + List<ContextAwareEdit> unorderedOriginalEdits, + List<Edit> unorderedTransformingEdits, + String adjustedFilePath) { + List<ContextAwareEdit> originalEdits = new ArrayList<>(unorderedOriginalEdits); + originalEdits.sort(comparing(sideStrategy::getBegin).thenComparing(sideStrategy::getEnd)); + List<Edit> transformingEdits = new ArrayList<>(unorderedTransformingEdits); + transformingEdits.sort(comparing(Edit::getBeginA).thenComparing(Edit::getEndA)); + + int shiftedAmount = 0; + int transIndex = 0; + int origIndex = 0; + List<ContextAwareEdit> resultingEdits = new ArrayList<>(originalEdits.size()); + while (origIndex < originalEdits.size() && transIndex < transformingEdits.size()) { + ContextAwareEdit originalEdit = originalEdits.get(origIndex); + Edit transformingEdit = transformingEdits.get(transIndex); + if (transformingEdit.getEndA() < sideStrategy.getBegin(originalEdit)) { + shiftedAmount = transformingEdit.getEndB() - transformingEdit.getEndA(); + transIndex++; + } else if (sideStrategy.getEnd(originalEdit) < transformingEdit.getBeginA()) { + resultingEdits.add(sideStrategy.create(originalEdit, shiftedAmount, adjustedFilePath)); + origIndex++; + } else { + // Overlapping -> ignore. + origIndex++; + } + } + for (int i = origIndex; i < originalEdits.size(); i++) { + resultingEdits.add( + sideStrategy.create(originalEdits.get(i), shiftedAmount, adjustedFilePath)); + } + return resultingEdits; + } + + @AutoValue + abstract static class ContextAwareEdit { + static ContextAwareEdit create(PatchListEntry patchListEntry, Edit edit) { + return create( + patchListEntry.getOldName(), + patchListEntry.getNewName(), + edit.getBeginA(), + edit.getEndA(), + edit.getBeginB(), + edit.getEndB()); + } + + static ContextAwareEdit createForNoContentEdit(PatchListEntry patchListEntry) { + return create(patchListEntry.getOldName(), patchListEntry.getNewName(), -1, -1, -1, -1); + } + + static ContextAwareEdit create( + String oldFilePath, String newFilePath, int beginA, int endA, int beginB, int endB) { + String adjustedOldFilePath = MoreObjects.firstNonNull(oldFilePath, newFilePath); + return new AutoValue_EditTransformer_ContextAwareEdit( + adjustedOldFilePath, newFilePath, beginA, endA, beginB, endB); + } + + public abstract String getOldFilePath(); + + public abstract String getNewFilePath(); + + public abstract int getBeginA(); + + public abstract int getEndA(); + + public abstract int getBeginB(); + + public abstract int getEndB(); + + public Optional<Edit> toEdit() { + if (getBeginA() < 0) { + return Optional.empty(); + } + + return Optional.of(new Edit(getBeginA(), getEndA(), getBeginB(), getEndB())); + } + } + + private interface SideStrategy { + String getFilePath(ContextAwareEdit edit); + + int getBegin(ContextAwareEdit edit); + + int getEnd(ContextAwareEdit edit); + + ContextAwareEdit create(ContextAwareEdit edit, int shiftedAmount, String adjustedFilePath); + } + + private enum SideAStrategy implements SideStrategy { + INSTANCE; + + @Override + public String getFilePath(ContextAwareEdit edit) { + return edit.getOldFilePath(); + } + + @Override + public int getBegin(ContextAwareEdit edit) { + return edit.getBeginA(); + } + + @Override + public int getEnd(ContextAwareEdit edit) { + return edit.getEndA(); + } + + @Override + public ContextAwareEdit create( + ContextAwareEdit edit, int shiftedAmount, String adjustedFilePath) { + return ContextAwareEdit.create( + adjustedFilePath, + edit.getNewFilePath(), + edit.getBeginA() + shiftedAmount, + edit.getEndA() + shiftedAmount, + edit.getBeginB(), + edit.getEndB()); + } + } + + private enum SideBStrategy implements SideStrategy { + INSTANCE; + + @Override + public String getFilePath(ContextAwareEdit edit) { + return edit.getNewFilePath(); + } + + @Override + public int getBegin(ContextAwareEdit edit) { + return edit.getBeginB(); + } + + @Override + public int getEnd(ContextAwareEdit edit) { + return edit.getEndB(); + } + + @Override + public ContextAwareEdit create( + ContextAwareEdit edit, int shiftedAmount, String adjustedFilePath) { + return ContextAwareEdit.create( + edit.getOldFilePath(), + adjustedFilePath, + edit.getBeginA(), + edit.getEndA(), + edit.getBeginB() + shiftedAmount, + edit.getEndB() + shiftedAmount); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java index e51b4ab..ee8b88b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.server.ioutil.BasicSerialization.writeEnum; import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32; +import com.google.common.collect.ImmutableList; import com.google.gerrit.reviewdb.client.CodedEnum; import java.io.IOException; import java.io.InputStream; @@ -29,6 +30,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.diff.ReplaceEdit; @@ -54,27 +56,29 @@ } private transient Status status; - private transient List<Edit> edits; + private transient ImmutableList<Edit> edits; IntraLineDiff(Status status) { this.status = status; - this.edits = Collections.emptyList(); + this.edits = ImmutableList.of(); } IntraLineDiff(List<Edit> edits) { this.status = Status.EDIT_LIST; - this.edits = Collections.unmodifiableList(edits); + this.edits = ImmutableList.copyOf(edits); } public Status getStatus() { return status; } - public List<Edit> getEdits() { - return edits; + public ImmutableList<Edit> getEdits() { + // Edits are mutable objects. As we serialize IntraLineDiff asynchronously in H2CacheImpl, we + // must ensure that its state isn't modified until it was properly stored in the cache. + return deepCopyEdits(edits); } - private void writeObject(final ObjectOutputStream out) throws IOException { + private void writeObject(ObjectOutputStream out) throws IOException { writeEnum(out, status); writeVarInt32(out, edits.size()); for (Edit e : edits) { @@ -92,7 +96,7 @@ } } - private void readObject(final ObjectInputStream in) throws IOException { + private void readObject(ObjectInputStream in) throws IOException { status = readEnum(in, Status.values()); int editCount = readVarInt32(in); Edit[] editArray = new Edit[editCount]; @@ -108,7 +112,25 @@ editArray[i] = new ReplaceEdit(editArray[i], toList(inner)); } } - edits = toList(editArray); + edits = ImmutableList.copyOf(editArray); + } + + private static ImmutableList<Edit> deepCopyEdits(List<Edit> edits) { + return edits.stream().map(IntraLineDiff::copy).collect(ImmutableList.toImmutableList()); + } + + private static Edit copy(Edit edit) { + if (edit instanceof ReplaceEdit) { + return copy((ReplaceEdit) edit); + } + return new Edit(edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB()); + } + + private static ReplaceEdit copy(ReplaceEdit edit) { + List<Edit> internalEdits = + edit.getInternalEdits().stream().map(IntraLineDiff::copy).collect(Collectors.toList()); + return new ReplaceEdit( + edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB(), internalEdits); } private static void writeEdit(OutputStream out, Edit e) throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java index 46ee56a..882360c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffArgs.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.patch; import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; import com.google.gerrit.reviewdb.client.Project; import java.util.List; import org.eclipse.jgit.diff.Edit; @@ -29,14 +30,23 @@ Project.NameKey project, ObjectId commit, String path) { - return new AutoValue_IntraLineDiffArgs(aText, bText, edits, project, commit, path); + return new AutoValue_IntraLineDiffArgs( + aText, bText, deepCopyEdits(edits), project, commit, path); + } + + private static ImmutableList<Edit> deepCopyEdits(List<Edit> edits) { + return edits.stream().map(IntraLineDiffArgs::copy).collect(ImmutableList.toImmutableList()); + } + + private static Edit copy(Edit edit) { + return new Edit(edit.getBeginA(), edit.getEndA(), edit.getBeginB(), edit.getEndB()); } public abstract Text aText(); public abstract Text bText(); - public abstract List<Edit> edits(); + public abstract ImmutableList<Edit> edits(); public abstract Project.NameKey project();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java index ed58408..b78d519 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
@@ -21,7 +21,7 @@ @AutoValue public abstract class IntraLineDiffKey implements Serializable { - public static final long serialVersionUID = 5L; + public static final long serialVersionUID = 7L; public static IntraLineDiffKey create(ObjectId aId, ObjectId bId, Whitespace whitespace) { return new AutoValue_IntraLineDiffKey(aId, bId, whitespace);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java index a571c46..e5e1bad 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -16,10 +16,12 @@ package com.google.gerrit.server.patch; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.GerritServerConfig; +import com.google.inject.Inject; import com.google.inject.assistedinject.Assisted; -import com.google.inject.assistedinject.AssistedInject; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -52,7 +54,7 @@ private final IntraLineDiffKey key; private final IntraLineDiffArgs args; - @AssistedInject + @Inject IntraLineLoader( @DiffExecutor ExecutorService diffExecutor, @GerritServerConfig Config cfg, @@ -75,12 +77,7 @@ public IntraLineDiff call() throws Exception { Future<IntraLineDiff> result = diffExecutor.submit( - new Callable<IntraLineDiff>() { - @Override - public IntraLineDiff call() throws Exception { - return IntraLineLoader.compute(args.aText(), args.bText(), args.edits()); - } - }); + () -> IntraLineLoader.compute(args.aText(), args.bText(), args.edits())); try { return result.get(timeoutMillis, TimeUnit.MILLISECONDS); } catch (InterruptedException | TimeoutException e) { @@ -107,7 +104,9 @@ } } - static IntraLineDiff compute(Text aText, Text bText, List<Edit> edits) throws Exception { + static IntraLineDiff compute(Text aText, Text bText, ImmutableList<Edit> immutableEdits) + throws Exception { + List<Edit> edits = new ArrayList<>(immutableEdits); combineLineEdits(edits, aText, bText); for (int i = 0; i < edits.size(); i++) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java index b4c2fbe..aff519a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
@@ -96,7 +96,7 @@ * @throws IOException the patch or complete file content cannot be read. * @throws NoSuchEntityException */ - public String getLine(final int file, final int line) throws IOException, NoSuchEntityException { + public String getLine(int file, int line) throws IOException, NoSuchEntityException { switch (file) { case 0: if (a == null) { @@ -123,7 +123,7 @@ * @throws IOException the patch or complete file content cannot be read. * @throws NoSuchEntityException the file is not exist. */ - public int getLineCount(final int file) throws IOException, NoSuchEntityException { + public int getLineCount(int file) throws IOException, NoSuchEntityException { switch (file) { case 0: if (a == null) { @@ -142,7 +142,7 @@ } } - private Text load(final ObjectId tree, final String path) + private Text load(ObjectId tree, String path) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { if (path == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java index 020c354..16ede58 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
@@ -49,7 +49,7 @@ private static final Comparator<PatchListEntry> PATCH_CMP = new Comparator<PatchListEntry>() { @Override - public int compare(final PatchListEntry a, final PatchListEntry b) { + public int compare(PatchListEntry a, PatchListEntry b) { return comparePaths(a.getNewName(), b.getNewName()); } }; @@ -151,26 +151,26 @@ * specified, but is a current legacy artifact of how the cache is keyed versus how the * database is keyed. */ - public List<Patch> toPatchList(final PatchSet.Id setId) { + public List<Patch> toPatchList(PatchSet.Id setId) { final ArrayList<Patch> r = new ArrayList<>(patches.length); - for (final PatchListEntry e : patches) { + for (PatchListEntry e : patches) { r.add(e.toPatch(setId)); } return r; } /** Find an entry by name, returning an empty entry if not present. */ - public PatchListEntry get(final String fileName) { + public PatchListEntry get(String fileName) { final int index = search(fileName); return 0 <= index ? patches[index] : PatchListEntry.empty(fileName); } - private int search(final String fileName) { + private int search(String fileName) { PatchListEntry want = PatchListEntry.empty(fileName); return Arrays.binarySearch(patches, 0, patches.length, want, PATCH_CMP); } - private void writeObject(final ObjectOutputStream output) throws IOException { + private void writeObject(ObjectOutputStream output) throws IOException { final ByteArrayOutputStream buf = new ByteArrayOutputStream(); try (DeflaterOutputStream out = new DeflaterOutputStream(buf)) { writeCanBeNull(out, oldId); @@ -187,7 +187,7 @@ writeBytes(output, buf.toByteArray()); } - private void readObject(final ObjectInputStream input) throws IOException { + private void readObject(ObjectInputStream input) throws IOException { final ByteArrayInputStream buf = new ByteArrayInputStream(readBytes(input)); try (InflaterInputStream in = new InflaterInputStream(buf)) { oldId = readCanBeNull(in);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java index edff293..4a6e01f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -101,7 +101,9 @@ throws PatchListNotAvailableException { try { PatchList pl = fileCache.get(key, fileLoaderFactory.create(key, project)); - diffSummaryCache.put(DiffSummaryKey.fromPatchListKey(key), toDiffSummary(pl)); + if (key.getAlgorithm() == PatchListKey.Algorithm.OPTIMIZED_DIFF) { + diffSummaryCache.put(DiffSummaryKey.fromPatchListKey(key), toDiffSummary(pl)); + } return pl; } catch (ExecutionException e) { PatchListLoader.log.warn("Error computing " + key, e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java index a8a8b79..96f66f6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -25,6 +25,8 @@ import static com.google.gerrit.server.ioutil.BasicSerialization.writeString; import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.Patch.ChangeType; import com.google.gerrit.reviewdb.client.Patch.PatchType; @@ -33,9 +35,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; +import java.util.Collection; import java.util.List; +import java.util.Set; import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.patch.CombinedFileHeader; @@ -46,14 +48,15 @@ public class PatchListEntry { private static final byte[] EMPTY_HEADER = {}; - static PatchListEntry empty(final String fileName) { + static PatchListEntry empty(String fileName) { return new PatchListEntry( ChangeType.MODIFIED, PatchType.UNIFIED, null, fileName, EMPTY_HEADER, - Collections.<Edit>emptyList(), + ImmutableList.of(), + ImmutableSet.of(), 0, 0, 0, @@ -65,7 +68,8 @@ private final String oldName; private final String newName; private final byte[] header; - private final List<Edit> edits; + private final ImmutableList<Edit> edits; + private final ImmutableSet<Edit> editsDueToRebase; private final int insertions; private final int deletions; private final long size; @@ -73,7 +77,8 @@ // Note: When adding new fields, the serialVersionUID in PatchListKey must be // incremented so that entries from the cache are automatically invalidated. - PatchListEntry(FileHeader hdr, List<Edit> editList, long size, long sizeDelta) { + PatchListEntry( + FileHeader hdr, List<Edit> editList, Set<Edit> editsDueToRebase, long size, long sizeDelta) { changeType = toChangeType(hdr); patchType = toPatchType(hdr); @@ -103,16 +108,19 @@ header = compact(hdr); if (hdr instanceof CombinedFileHeader || hdr.getHunks().isEmpty()) { - edits = Collections.emptyList(); + edits = ImmutableList.of(); } else { - edits = Collections.unmodifiableList(editList); + edits = ImmutableList.copyOf(editList); } + this.editsDueToRebase = ImmutableSet.copyOf(editsDueToRebase); int ins = 0; int del = 0; for (Edit e : editList) { - del += e.getEndA() - e.getBeginA(); - ins += e.getEndB() - e.getBeginB(); + if (!editsDueToRebase.contains(e)) { + del += e.getEndA() - e.getBeginA(); + ins += e.getEndB() - e.getBeginB(); + } } insertions = ins; deletions = del; @@ -126,7 +134,8 @@ String oldName, String newName, byte[] header, - List<Edit> edits, + ImmutableList<Edit> edits, + ImmutableSet<Edit> editsDueToRebase, int insertions, int deletions, long size, @@ -137,6 +146,7 @@ this.newName = newName; this.header = header; this.edits = edits; + this.editsDueToRebase = editsDueToRebase; this.insertions = insertions; this.deletions = deletions; this.size = size; @@ -149,6 +159,7 @@ size += stringSize(newName); size += header.length; size += (8 + 16 + 4 * 4) * edits.size(); + size += (8 + 16 + 4 * 4) * editsDueToRebase.size(); return size; } @@ -175,10 +186,14 @@ return newName; } - public List<Edit> getEdits() { + public ImmutableList<Edit> getEdits() { return edits; } + public ImmutableSet<Edit> getEditsDueToRebase() { + return editsDueToRebase; + } + public int getInsertions() { return insertions; } @@ -209,7 +224,7 @@ return headerLines; } - Patch toPatch(final PatchSet.Id setId) { + Patch toPatch(PatchSet.Id setId) { final Patch p = new Patch(new Patch.Key(setId, getNewName())); p.setChangeType(getChangeType()); p.setPatchType(getPatchType()); @@ -230,12 +245,17 @@ writeFixInt64(out, size); writeFixInt64(out, sizeDelta); + writeEditArray(out, edits); + writeEditArray(out, editsDueToRebase); + } + + private static void writeEditArray(OutputStream out, Collection<Edit> edits) throws IOException { writeVarInt32(out, edits.size()); - for (final Edit e : edits) { - writeVarInt32(out, e.getBeginA()); - writeVarInt32(out, e.getEndA()); - writeVarInt32(out, e.getBeginB()); - writeVarInt32(out, e.getEndB()); + for (Edit edit : edits) { + writeVarInt32(out, edit.getBeginA()); + writeVarInt32(out, edit.getEndA()); + writeVarInt32(out, edit.getBeginB()); + writeVarInt32(out, edit.getEndB()); } } @@ -250,25 +270,37 @@ long size = readFixInt64(in); long sizeDelta = readFixInt64(in); - int editCount = readVarInt32(in); - Edit[] editArray = new Edit[editCount]; - for (int i = 0; i < editCount; i++) { + Edit[] editArray = readEditArray(in); + Edit[] editsDueToRebase = readEditArray(in); + + return new PatchListEntry( + changeType, + patchType, + oldName, + newName, + hdr, + ImmutableList.copyOf(editArray), + ImmutableSet.copyOf(editsDueToRebase), + ins, + del, + size, + sizeDelta); + } + + private static Edit[] readEditArray(InputStream in) throws IOException { + int numEdits = readVarInt32(in); + Edit[] edits = new Edit[numEdits]; + for (int i = 0; i < numEdits; i++) { int beginA = readVarInt32(in); int endA = readVarInt32(in); int beginB = readVarInt32(in); int endB = readVarInt32(in); - editArray[i] = new Edit(beginA, endA, beginB, endB); + edits[i] = new Edit(beginA, endA, beginB, endB); } - - return new PatchListEntry( - changeType, patchType, oldName, newName, hdr, toList(editArray), ins, del, size, sizeDelta); + return edits; } - private static List<Edit> toList(Edit[] l) { - return Collections.unmodifiableList(Arrays.asList(l)); - } - - private static byte[] compact(final FileHeader h) { + private static byte[] compact(FileHeader h) { final int end = end(h); if (h.getStartOffset() == 0 && end == h.getBuffer().length) { return h.getBuffer(); @@ -279,7 +311,7 @@ return buf; } - private static int end(final FileHeader h) { + private static int end(FileHeader h) { if (h instanceof CombinedFileHeader) { return h.getEndOffset(); } @@ -289,7 +321,7 @@ return h.getEndOffset(); } - private static ChangeType toChangeType(final FileHeader hdr) { + private static ChangeType toChangeType(FileHeader hdr) { switch (hdr.getChangeType()) { case ADD: return Patch.ChangeType.ADDED; @@ -306,7 +338,7 @@ } } - private static PatchType toPatchType(final FileHeader hdr) { + private static PatchType toPatchType(FileHeader hdr) { PatchType pt; switch (hdr.getPatchType()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java index ae771d3..e7b78b7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -32,7 +32,17 @@ import org.eclipse.jgit.lib.ObjectId; public class PatchListKey implements Serializable { - public static final long serialVersionUID = 24L; + public static final long serialVersionUID = 27L; + + public enum Algorithm { + PURE_TREE_DIFF, + OPTIMIZED_DIFF + } + + private static final ImmutableBiMap<Algorithm, Character> ALGORITHM_TYPES = + ImmutableBiMap.of( + Algorithm.PURE_TREE_DIFF, 'T', + Algorithm.OPTIMIZED_DIFF, 'O'); public static final ImmutableBiMap<Whitespace, Character> WHITESPACE_TYPES = ImmutableBiMap.of( @@ -43,14 +53,25 @@ static { checkState(WHITESPACE_TYPES.size() == Whitespace.values().length); + checkState(ALGORITHM_TYPES.size() == Algorithm.values().length); } public static PatchListKey againstDefaultBase(AnyObjectId newId, Whitespace ws) { - return new PatchListKey(null, newId, ws); + return new PatchListKey(null, newId, ws, Algorithm.OPTIMIZED_DIFF); } public static PatchListKey againstParentNum(int parentNum, AnyObjectId newId, Whitespace ws) { - return new PatchListKey(parentNum, newId, ws); + return new PatchListKey(parentNum, newId, ws, Algorithm.OPTIMIZED_DIFF); + } + + public static PatchListKey againstCommit( + AnyObjectId otherCommitId, AnyObjectId newId, Whitespace whitespace) { + return new PatchListKey(otherCommitId, newId, whitespace, Algorithm.OPTIMIZED_DIFF); + } + + public static PatchListKey againstCommitWithPureTreeDiff( + AnyObjectId otherCommitId, AnyObjectId newId, Whitespace whitespace) { + return new PatchListKey(otherCommitId, newId, whitespace, Algorithm.PURE_TREE_DIFF); } /** @@ -75,25 +96,34 @@ private transient ObjectId newId; private transient Whitespace whitespace; + private transient Algorithm algorithm; - public PatchListKey(AnyObjectId a, AnyObjectId b, Whitespace ws) { + private PatchListKey(AnyObjectId a, AnyObjectId b, Whitespace ws, Algorithm algorithm) { oldId = a != null ? a.copy() : null; newId = b.copy(); whitespace = ws; + this.algorithm = algorithm; } - private PatchListKey(int parentNum, AnyObjectId b, Whitespace ws) { + private PatchListKey(int parentNum, AnyObjectId b, Whitespace ws, Algorithm algorithm) { this.parentNum = Integer.valueOf(parentNum); newId = b.copy(); whitespace = ws; + this.algorithm = algorithm; } /** For use only by DiffSummaryKey. */ - PatchListKey(ObjectId oldId, Integer parentNum, ObjectId newId, Whitespace whitespace) { + PatchListKey( + ObjectId oldId, + Integer parentNum, + ObjectId newId, + Whitespace whitespace, + Algorithm algorithm) { this.oldId = oldId; this.parentNum = parentNum; this.newId = newId; this.whitespace = whitespace; + this.algorithm = algorithm; } /** Old side commit, or null to assume ancestor or combined merge. */ @@ -117,19 +147,24 @@ return whitespace; } - @Override - public int hashCode() { - return Objects.hash(oldId, parentNum, newId, whitespace); + public Algorithm getAlgorithm() { + return algorithm; } @Override - public boolean equals(final Object o) { + public int hashCode() { + return Objects.hash(oldId, parentNum, newId, whitespace, algorithm); + } + + @Override + public boolean equals(Object o) { if (o instanceof PatchListKey) { PatchListKey k = (PatchListKey) o; return Objects.equals(oldId, k.oldId) && Objects.equals(parentNum, k.parentNum) && Objects.equals(newId, k.newId) - && whitespace == k.whitespace; + && whitespace == k.whitespace + && algorithm == k.algorithm; } return false; } @@ -147,11 +182,13 @@ n.append(" "); } n.append(whitespace.name()); + n.append(" "); + n.append(algorithm.name()); n.append("]"); return n.toString(); } - private void writeObject(final ObjectOutputStream out) throws IOException { + private void writeObject(ObjectOutputStream out) throws IOException { writeCanBeNull(out, oldId); out.writeInt(parentNum == null ? 0 : parentNum); writeNotNull(out, newId); @@ -160,9 +197,10 @@ throw new IOException("Invalid whitespace type: " + whitespace); } out.writeChar(c); + out.writeChar(ALGORITHM_TYPES.get(algorithm)); } - private void readObject(final ObjectInputStream in) throws IOException { + private void readObject(ObjectInputStream in) throws IOException { oldId = readCanBeNull(in); int n = in.readInt(); parentNum = n == 0 ? null : Integer.valueOf(n); @@ -172,5 +210,7 @@ if (whitespace == null) { throw new IOException("Invalid whitespace type code: " + t); } + char algorithmCharacter = in.readChar(); + algorithm = ALGORITHM_TYPES.inverse().get(algorithmCharacter); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java index 124fe8e..e6ababd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -21,6 +21,10 @@ import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.Project; @@ -29,12 +33,13 @@ 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.EditTransformer.ContextAwareEdit; +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.Collections; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -42,8 +47,8 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.stream.Stream; import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.diff.EditList; @@ -86,7 +91,7 @@ private final long timeoutMillis; private final boolean save; - @AssistedInject + @Inject PatchListLoader( GitRepositoryManager mgr, PatchListCache plc, @@ -144,7 +149,7 @@ return save ? repo.newObjectInserter() : new InMemoryInserter(repo); } - public PatchList readPatchList(Repository repo, RevWalk rw, ObjectInserter ins) + private PatchList readPatchList(Repository repo, RevWalk rw, ObjectInserter ins) throws IOException, PatchListNotAvailableException { ObjectReader reader = rw.getObjectReader(); checkArgument(reader.getCreatedFromInserter() == ins); @@ -178,19 +183,6 @@ df.setDetectRenames(true); List<DiffEntry> diffEntries = df.scan(aTree, bTree); - Set<String> paths = null; - if (key.getOldId() != null && b.getParentCount() == 1) { - PatchListKey newKey = PatchListKey.againstDefaultBase(key.getNewId(), key.getWhitespace()); - PatchListKey oldKey = PatchListKey.againstDefaultBase(key.getOldId(), key.getWhitespace()); - paths = - Stream.concat( - patchListCache.get(newKey, project).getPatches().stream(), - patchListCache.get(oldKey, project).getPatches().stream()) - .map(PatchListEntry::getNewName) - .collect(toSet()); - } - - int cnt = diffEntries.size(); List<PatchListEntry> entries = new ArrayList<>(); entries.add( newCommitMessage( @@ -205,21 +197,135 @@ b, comparisonType)); } - for (int i = 0; i < cnt; i++) { - DiffEntry e = diffEntries.get(i); - if (paths == null || paths.contains(e.getNewPath()) || paths.contains(e.getOldPath())) { - - FileHeader fh = toFileHeader(key, df, e); - long oldSize = getFileSize(reader, e.getOldMode(), e.getOldPath(), aTree); - long newSize = getFileSize(reader, e.getNewMode(), e.getNewPath(), bTree); - entries.add(newEntry(aTree, fh, newSize, newSize - oldSize)); - } + Multimap<String, ContextAwareEdit> editsDueToRebasePerFilePath = + key.getAlgorithm() == PatchListKey.Algorithm.OPTIMIZED_DIFF + ? getEditsDueToRebasePerFilePath(aCommit, b) + : ImmutableMultimap.of(); + for (DiffEntry diffEntry : diffEntries) { + Set<ContextAwareEdit> editsDueToRebase = + getEditsDueToRebase(editsDueToRebasePerFilePath, diffEntry); + Optional<PatchListEntry> patchListEntry = + getPatchListEntry(reader, df, diffEntry, aTree, bTree, editsDueToRebase); + patchListEntry.ifPresent(entries::add); } return new PatchList( a, b, isMerge, comparisonType, entries.toArray(new PatchListEntry[entries.size()])); } } + /** + * Identifies the edits which are present between {@code commitA} and {@code commitB} due to other + * commits in between those two. Edits which cannot be clearly attributed to those other commits + * (because they overlap with modifications introduced by {@code commitA} or {@code commitB}) are + * omitted from the result. The edits are expressed as differences between {@code treeA} of {@code + * commitA} and {@code treeB} of {@code commitB}. + * + * <p><b>Note:</b> If one of the commits is a merge commit, an empty {@code Multimap} will be + * returned. + * + * <p><b>Warning:</b> This method assumes that commitA and commitB are either a parent and child + * commit or represent two patch sets which belong to the same change. No checks are made to + * confirm this assumption! Passing arbitrary commits to this method may lead to strange results + * or take very long. + * + * <p>This logic could be expanded to arbitrary commits if the following adjustments were applied: + * + * <ul> + * <li>If {@code commitA} is an ancestor of {@code commitB} (or the other way around), {@code + * commitA} (or {@code commitB}) is used instead of its parent in this method. + * <li>Special handling for merge commits is added. If only one of them is a merge commit, the + * whole computation has to be done between the single parent and all parents of the merge + * commit. If both of them are merge commits, all combinations of parents have to be + * considered. Alternatively, we could decide to not support this feature for merge commits + * (or just for specific types of merge commits). + * </ul> + * + * @param commitA the commit defining {@code treeA} + * @param commitB the commit defining {@code treeB} + * @return the edits per file path they modify in {@code treeB} + * @throws PatchListNotAvailableException if the edits can't be identified + */ + private Multimap<String, ContextAwareEdit> getEditsDueToRebasePerFilePath( + RevCommit commitA, RevCommit commitB) throws PatchListNotAvailableException { + if (commitA == null + || isRootOrMergeCommit(commitA) + || isRootOrMergeCommit(commitB) + || areParentChild(commitA, commitB) + || haveCommonParent(commitA, commitB)) { + return ImmutableMultimap.of(); + } + + PatchListKey parentDiffKey = + PatchListKey.againstCommitWithPureTreeDiff( + commitA.getParent(0), commitB.getParent(0), key.getWhitespace()); + PatchList parentPatchList = patchListCache.get(parentDiffKey, project); + PatchListKey oldKey = PatchListKey.againstDefaultBase(key.getOldId(), key.getWhitespace()); + PatchList oldPatchList = patchListCache.get(oldKey, project); + PatchListKey newKey = PatchListKey.againstDefaultBase(key.getNewId(), key.getWhitespace()); + PatchList newPatchList = patchListCache.get(newKey, project); + + EditTransformer editTransformer = new EditTransformer(parentPatchList.getPatches()); + editTransformer.transformReferencesOfSideA(oldPatchList.getPatches()); + editTransformer.transformReferencesOfSideB(newPatchList.getPatches()); + return editTransformer.getEditsPerFilePath(); + } + + private static boolean isRootOrMergeCommit(RevCommit commit) { + return commit.getParentCount() != 1; + } + + private static boolean areParentChild(RevCommit commitA, RevCommit commitB) { + return ObjectId.equals(commitA.getParent(0), commitB) + || ObjectId.equals(commitB.getParent(0), commitA); + } + + private static boolean haveCommonParent(RevCommit commitA, RevCommit commitB) { + return ObjectId.equals(commitA.getParent(0), commitB.getParent(0)); + } + + private static Set<ContextAwareEdit> getEditsDueToRebase( + Multimap<String, ContextAwareEdit> editsDueToRebasePerFilePath, DiffEntry diffEntry) { + if (editsDueToRebasePerFilePath.isEmpty()) { + return ImmutableSet.of(); + } + + String filePath = diffEntry.getNewPath(); + if (diffEntry.getChangeType() == ChangeType.DELETE) { + filePath = diffEntry.getOldPath(); + } + return ImmutableSet.copyOf(editsDueToRebasePerFilePath.get(filePath)); + } + + private Optional<PatchListEntry> getPatchListEntry( + ObjectReader objectReader, + DiffFormatter diffFormatter, + DiffEntry diffEntry, + RevTree treeA, + RevTree treeB, + Set<ContextAwareEdit> editsDueToRebase) + throws IOException { + FileHeader fileHeader = toFileHeader(key, diffFormatter, diffEntry); + long oldSize = getFileSize(objectReader, diffEntry.getOldMode(), diffEntry.getOldPath(), treeA); + long newSize = getFileSize(objectReader, diffEntry.getNewMode(), diffEntry.getNewPath(), treeB); + Set<Edit> contentEditsDueToRebase = getContentEdits(editsDueToRebase); + PatchListEntry patchListEntry = + newEntry(treeA, fileHeader, contentEditsDueToRebase, newSize, newSize - oldSize); + // All edits in a file are due to rebase -> exclude the file from the diff. + if (EditTransformer.toEdits(patchListEntry).allMatch(editsDueToRebase::contains)) { + return Optional.empty(); + } + return Optional.of(patchListEntry); + } + + private static Set<Edit> getContentEdits(Set<ContextAwareEdit> editsDueToRebase) { + return editsDueToRebase + .stream() + .map(ContextAwareEdit::toEdit) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(toSet()); + } + private ComparisonType getComparisonType(RevObject a, RevCommit b) { for (int i = 0; i < b.getParentCount(); i++) { if (b.getParent(i).equals(a)) { @@ -250,17 +356,13 @@ } private FileHeader toFileHeader( - PatchListKey key, final DiffFormatter diffFormatter, final DiffEntry diffEntry) - throws IOException { + PatchListKey key, DiffFormatter diffFormatter, DiffEntry diffEntry) throws IOException { Future<FileHeader> result = diffExecutor.submit( - new Callable<FileHeader>() { - @Override - public FileHeader call() throws IOException { - synchronized (diffEntry) { - return diffFormatter.toFileHeader(diffEntry); - } + () -> { + synchronized (diffEntry) { + return diffFormatter.toFileHeader(diffEntry); } }); @@ -331,7 +433,7 @@ RawText bRawText = new RawText(bContent); EditList edits = new HistogramDiff().diff(cmp, aRawText, bRawText); FileHeader fh = new FileHeader(rawHdr, edits, PatchType.UNIFIED); - return new PatchListEntry(fh, edits, size, sizeDelta); + return new PatchListEntry(fh, edits, ImmutableSet.of(), size, sizeDelta); } private static byte[] getRawHeader(boolean hasA, String fileName) { @@ -354,18 +456,19 @@ return hdr.toString().getBytes(UTF_8); } - private PatchListEntry newEntry(RevTree aTree, FileHeader fileHeader, long size, long sizeDelta) { + private static PatchListEntry newEntry( + RevTree aTree, FileHeader fileHeader, Set<Edit> editsDueToRebase, long size, long sizeDelta) { if (aTree == null // want combined diff || fileHeader.getPatchType() != PatchType.UNIFIED || fileHeader.getHunks().isEmpty()) { - return new PatchListEntry(fileHeader, Collections.<Edit>emptyList(), size, sizeDelta); + return new PatchListEntry(fileHeader, ImmutableList.of(), ImmutableSet.of(), size, sizeDelta); } List<Edit> edits = fileHeader.toEditList(); if (edits.isEmpty()) { - return new PatchListEntry(fileHeader, Collections.<Edit>emptyList(), size, sizeDelta); + return new PatchListEntry(fileHeader, ImmutableList.of(), ImmutableSet.of(), size, sizeDelta); } - return new PatchListEntry(fileHeader, edits, size, sizeDelta); + return new PatchListEntry(fileHeader, edits, editsDueToRebase, size, sizeDelta); } private RevObject aFor(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java index f40eac6..942d0e0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
@@ -23,7 +23,8 @@ int size = 16 + 4 * 8 - + 2 * 36 // Size of PatchListKey, 64 bit JVM + + 2 * 36 + + 8 // Size of PatchListKey, 64 bit JVM + 16 + 3 * 8 + 3 * 4
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java index 2dd5af7..8b86be2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -56,7 +56,7 @@ private static final Comparator<Edit> EDIT_SORT = new Comparator<Edit>() { @Override - public int compare(final Edit o1, final Edit o2) { + public int compare(Edit o1, Edit o2) { return o1.getBeginA() - o2.getBeginA(); } }; @@ -91,11 +91,11 @@ this.projectKey = projectKey; } - void setChange(final Change c) { + void setChange(Change c) { this.change = c; } - void setDiffPrefs(final DiffPreferencesInfo dp) { + void setDiffPrefs(DiffPreferencesInfo dp) { diffPrefs = dp; context = diffPrefs.context; @@ -106,14 +106,13 @@ } } - void setTrees(final ComparisonType ct, final ObjectId a, final ObjectId b) { + void setTrees(ComparisonType ct, ObjectId a, ObjectId b) { comparisonType = ct; aId = a; bId = b; } - PatchScript toPatchScript( - final PatchListEntry content, final CommentDetail comments, final List<Patch> history) + PatchScript toPatchScript(PatchListEntry content, CommentDetail comments, List<Patch> history) throws IOException { reader = db.newObjectReader(); try { @@ -123,8 +122,7 @@ } } - private PatchScript build( - final PatchListEntry content, final CommentDetail comments, final List<Patch> history) + private PatchScript build(PatchListEntry content, CommentDetail comments, List<Patch> history) throws IOException { boolean intralineDifferenceIsPossible = true; boolean intralineFailure = false; @@ -216,6 +214,7 @@ a.dst, b.dst, edits, + content.getEditsDueToRebase(), a.displayMethod, b.displayMethod, a.mimeType.toString(), @@ -246,7 +245,7 @@ } } - private static String oldName(final PatchListEntry entry) { + private static String oldName(PatchListEntry entry) { switch (entry.getChangeType()) { case ADDED: return null; @@ -261,7 +260,7 @@ } } - private static String newName(final PatchListEntry entry) { + private static String newName(PatchListEntry entry) { switch (entry.getChangeType()) { case DELETED: return null; @@ -275,7 +274,7 @@ } } - private void ensureCommentsVisible(final CommentDetail comments) { + private void ensureCommentsVisible(CommentDetail comments) { if (comments.getCommentsA().isEmpty() && comments.getCommentsB().isEmpty()) { // No comments, no additional dummy edits are required. // @@ -323,10 +322,10 @@ Collections.sort(edits, EDIT_SORT); } - private void safeAdd(final List<Edit> empty, final Edit toAdd) { + private void safeAdd(List<Edit> empty, Edit toAdd) { final int a = toAdd.getBeginA(); final int b = toAdd.getBeginB(); - for (final Edit e : edits) { + for (Edit e : edits) { if (e.getBeginA() <= a && a <= e.getEndA()) { return; } @@ -337,7 +336,7 @@ empty.add(toAdd); } - private int mapA2B(final int a) { + private int mapA2B(int a) { if (edits.isEmpty()) { // Magic special case of an unmodified file. // @@ -363,7 +362,7 @@ return last.getEndB() + (a - last.getEndA()); } - private int mapB2A(final int b) { + private int mapB2A(int b) { if (edits.isEmpty()) { // Magic special case of an unmodified file. // @@ -391,7 +390,7 @@ private void packContent(boolean ignoredWhitespace) { EditList list = new EditList(edits, context, a.size(), b.size()); - for (final EditList.Hunk hunk : list.getHunks()) { + for (EditList.Hunk hunk : list.getHunks()) { while (hunk.next()) { if (hunk.isContextLine()) { final String lineA = a.src.getString(hunk.getCurA()); @@ -442,7 +441,7 @@ dst.addLine(line, src.getString(line)); } - void resolve(final Side other, final ObjectId within) throws IOException { + void resolve(Side other, ObjectId within) throws IOException { try { final boolean reuse; if (Patch.COMMIT_MSG.equals(path)) { @@ -534,11 +533,7 @@ } } - if (srcContent.length > 0 && srcContent[srcContent.length - 1] != '\n') { - dst.setMissingNewlineAtEnd(true); - } dst.setSize(size()); - dst.setPath(path); if (mode == FileMode.SYMLINK) { fileMode = PatchScript.FileMode.SYMLINK; @@ -550,7 +545,7 @@ } } - private TreeWalk find(final ObjectId within) + private TreeWalk find(ObjectId within) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { if (path == null || within == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java index 82c6150..081ba7a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -114,9 +114,9 @@ CommentsUtil commentsUtil, ChangeEditUtil editReader, @Assisted ChangeControl control, - @Assisted final String fileName, - @Assisted("patchSetA") @Nullable final PatchSet.Id patchSetA, - @Assisted("patchSetB") final PatchSet.Id patchSetB, + @Assisted String fileName, + @Assisted("patchSetA") @Nullable PatchSet.Id patchSetA, + @Assisted("patchSetB") PatchSet.Id patchSetB, @Assisted DiffPreferencesInfo diffPrefs) { this.repoManager = grm; this.psUtil = psUtil; @@ -233,18 +233,18 @@ } } - private PatchListKey keyFor(final Whitespace whitespace) { + private PatchListKey keyFor(Whitespace whitespace) { if (parentNum < 0) { - return new PatchListKey(aId, bId, whitespace); + return PatchListKey.againstCommit(aId, bId, whitespace); } return PatchListKey.againstParentNum(parentNum + 1, bId, whitespace); } - private PatchList listFor(final PatchListKey key) throws PatchListNotAvailableException { + private PatchList listFor(PatchListKey key) throws PatchListNotAvailableException { return patchListCache.get(key, project); } - private PatchScriptBuilder newBuilder(final PatchList list, Repository git) { + private PatchScriptBuilder newBuilder(PatchList list, Repository git) { final PatchScriptBuilder b = builderFactory.get(); b.setRepository(git, project); b.setChange(change); @@ -253,9 +253,7 @@ return b; } - private ObjectId toObjectId(PatchSet ps) - throws NoSuchChangeException, AuthException, NoSuchChangeException, IOException, - OrmException { + private ObjectId toObjectId(PatchSet ps) throws AuthException, IOException, OrmException { if (ps.getId().get() == 0) { return getEditRev(); } @@ -271,16 +269,15 @@ } } - private ObjectId getEditRev() - throws AuthException, NoSuchChangeException, IOException, OrmException { + private ObjectId getEditRev() throws AuthException, IOException, OrmException { edit = editReader.byChange(change); if (edit.isPresent()) { - return edit.get().getRef().getObjectId(); + return edit.get().getEditCommit(); } throw new NoSuchChangeException(change.getId()); } - private void validatePatchSetId(final PatchSet.Id psId) throws NoSuchChangeException { + private void validatePatchSetId(PatchSet.Id psId) throws NoSuchChangeException { if (psId == null) { // OK, means use base; } else if (changeId.equals(psId.getParentKey())) { // OK, same change; } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java index 3fc6ba6..cd9c4c3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -90,7 +90,7 @@ } // TODO: The same method exists in EventFactory, find a common place for it - private UserIdentity toUserIdentity(final PersonIdent who) { + private UserIdentity toUserIdentity(PersonIdent who) { final UserIdentity u = new UserIdentity(); u.setName(who.getName()); u.setEmail(who.getEmailAddress()); @@ -108,7 +108,7 @@ return u; } - private List<PatchSetInfo.ParentInfo> toParentInfos(final RevCommit[] parents, final RevWalk walk) + private List<PatchSetInfo.ParentInfo> toParentInfos(RevCommit[] parents, RevWalk walk) throws IOException, MissingObjectException { List<PatchSetInfo.ParentInfo> pInfos = new ArrayList<>(parents.length); for (RevCommit parent : parents) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java index f001591..90141715 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/Text.java
@@ -168,7 +168,7 @@ private Charset charset; - public Text(final byte[] r) { + public Text(byte[] r) { super(r); } @@ -181,7 +181,7 @@ } @Override - protected String decode(final int s, int e) { + protected String decode(int s, int e) { if (charset == null) { charset = charset(content, null); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermission.java new file mode 100644 index 0000000..4b06861 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermission.java
@@ -0,0 +1,56 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.permissions; + +import com.google.gerrit.common.data.Permission; +import java.util.Locale; +import java.util.Optional; + +public enum ChangePermission implements ChangePermissionOrLabel { + READ(Permission.READ), + RESTORE, + DELETE, + ABANDON(Permission.ABANDON), + EDIT_ASSIGNEE(Permission.EDIT_ASSIGNEE), + EDIT_DESCRIPTION, + EDIT_HASHTAGS(Permission.EDIT_HASHTAGS), + EDIT_TOPIC_NAME(Permission.EDIT_TOPIC_NAME), + REMOVE_REVIEWER(Permission.REMOVE_REVIEWER), + ADD_PATCH_SET(Permission.ADD_PATCH_SET), + REBASE(Permission.REBASE), + SUBMIT(Permission.SUBMIT), + SUBMIT_AS(Permission.SUBMIT_AS); + + private final String name; + + ChangePermission() { + name = null; + } + + ChangePermission(String name) { + this.name = name; + } + + /** @return name used in {@code project.config} permissions. */ + @Override + public Optional<String> permissionName() { + return Optional.ofNullable(name); + } + + @Override + public String describeForException() { + return toString().toLowerCase(Locale.US).replace('_', ' '); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java new file mode 100644 index 0000000..06c0d73 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ChangePermissionOrLabel.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.permissions; + +import java.util.Optional; + +/** A {@link ChangePermission} or a {@link LabelPermission}. */ +public interface ChangePermissionOrLabel { + /** @return name used in {@code project.config} permissions. */ + public Optional<String> permissionName(); + + /** @return readable identifier of this permission for exception message. */ + public String describeForException(); +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java new file mode 100644 index 0000000..24f5164 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -0,0 +1,173 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.permissions; + +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.permissions.PermissionBackend.ForChange; +import com.google.gerrit.server.permissions.PermissionBackend.ForProject; +import com.google.gerrit.server.permissions.PermissionBackend.ForRef; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.inject.Provider; +import java.util.Collection; +import java.util.Set; + +/** + * Helpers for {@link PermissionBackend} that must fail. + * + * <p>These helpers are useful to curry failure state identified inside a non-throwing factory + * method to the throwing {@code check} or {@code test} methods. + */ +public class FailedPermissionBackend { + public static ForProject project(String message) { + return project(message, null); + } + + public static ForProject project(String message, Throwable cause) { + return new FailedProject(message, cause); + } + + public static ForRef ref(String message) { + return ref(message, null); + } + + public static ForRef ref(String message, Throwable cause) { + return new FailedRef(message, cause); + } + + public static ForChange change(String message) { + return change(message, null); + } + + public static ForChange change(String message, Throwable cause) { + return new FailedChange(message, cause); + } + + private FailedPermissionBackend() {} + + private static class FailedProject extends ForProject { + private final String message; + private final Throwable cause; + + FailedProject(String message, Throwable cause) { + this.message = message; + this.cause = cause; + } + + @Override + public ForProject database(Provider<ReviewDb> db) { + return this; + } + + @Override + public ForProject user(CurrentUser user) { + return this; + } + + @Override + public ForRef ref(String ref) { + return new FailedRef(message, cause); + } + + @Override + public void check(ProjectPermission perm) throws PermissionBackendException { + throw new PermissionBackendException(message, cause); + } + + @Override + public Set<ProjectPermission> test(Collection<ProjectPermission> permSet) + throws PermissionBackendException { + throw new PermissionBackendException(message, cause); + } + } + + private static class FailedRef extends ForRef { + private final String message; + private final Throwable cause; + + FailedRef(String message, Throwable cause) { + this.message = message; + this.cause = cause; + } + + @Override + public ForRef database(Provider<ReviewDb> db) { + return this; + } + + @Override + public ForRef user(CurrentUser user) { + return this; + } + + @Override + public ForChange change(ChangeData cd) { + return new FailedChange(message, cause); + } + + @Override + public ForChange change(ChangeNotes cd) { + return new FailedChange(message, cause); + } + + @Override + public void check(RefPermission perm) throws PermissionBackendException { + throw new PermissionBackendException(message, cause); + } + + @Override + public Set<RefPermission> test(Collection<RefPermission> permSet) + throws PermissionBackendException { + throw new PermissionBackendException(message, cause); + } + } + + private static class FailedChange extends ForChange { + private final String message; + private final Throwable cause; + + FailedChange(String message, Throwable cause) { + this.message = message; + this.cause = cause; + } + + @Override + public ForChange database(Provider<ReviewDb> db) { + return this; + } + + @Override + public ForChange user(CurrentUser user) { + return this; + } + + @Override + public void check(ChangePermissionOrLabel perm) throws PermissionBackendException { + throw new PermissionBackendException(message, cause); + } + + @Override + public <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet) + throws PermissionBackendException { + throw new PermissionBackendException(message, cause); + } + + @Override + public CurrentUser user() { + throw new UnsupportedOperationException("FailedPermissionBackend is not scoped to user"); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java new file mode 100644 index 0000000..4d293c8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java
@@ -0,0 +1,182 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.permissions; + +import com.google.common.collect.ImmutableMap; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.data.GlobalCapability; +import com.google.gerrit.extensions.annotations.CapabilityScope; +import com.google.gerrit.extensions.annotations.RequiresAnyCapability; +import com.google.gerrit.extensions.annotations.RequiresCapability; +import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission; +import com.google.gerrit.extensions.api.access.PluginPermission; +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Global server permissions built into Gerrit. */ +public enum GlobalPermission implements GlobalOrPluginPermission { + ACCESS_DATABASE(GlobalCapability.ACCESS_DATABASE), + ADMINISTRATE_SERVER(GlobalCapability.ADMINISTRATE_SERVER), + CREATE_ACCOUNT(GlobalCapability.CREATE_ACCOUNT), + CREATE_GROUP(GlobalCapability.CREATE_GROUP), + CREATE_PROJECT(GlobalCapability.CREATE_PROJECT), + EMAIL_REVIEWERS(GlobalCapability.EMAIL_REVIEWERS), + FLUSH_CACHES(GlobalCapability.FLUSH_CACHES), + KILL_TASK(GlobalCapability.KILL_TASK), + MAINTAIN_SERVER(GlobalCapability.MAINTAIN_SERVER), + MODIFY_ACCOUNT(GlobalCapability.MODIFY_ACCOUNT), + RUN_AS(GlobalCapability.RUN_AS), + RUN_GC(GlobalCapability.RUN_GC), + STREAM_EVENTS(GlobalCapability.STREAM_EVENTS), + VIEW_ALL_ACCOUNTS(GlobalCapability.VIEW_ALL_ACCOUNTS), + VIEW_CACHES(GlobalCapability.VIEW_CACHES), + VIEW_CONNECTIONS(GlobalCapability.VIEW_CONNECTIONS), + VIEW_PLUGINS(GlobalCapability.VIEW_PLUGINS), + VIEW_QUEUE(GlobalCapability.VIEW_QUEUE); + + private static final Logger log = LoggerFactory.getLogger(GlobalPermission.class); + private static final ImmutableMap<String, GlobalPermission> BY_NAME; + + static { + ImmutableMap.Builder<String, GlobalPermission> m = ImmutableMap.builder(); + for (GlobalPermission p : values()) { + m.put(p.permissionName(), p); + } + BY_NAME = m.build(); + } + + @Nullable + public static GlobalPermission byName(String name) { + return BY_NAME.get(name); + } + + /** + * Extracts the {@code @RequiresCapability} or {@code @RequiresAnyCapability} annotation. + * + * @param pluginName name of the declaring plugin. May be {@code null} or {@code "gerrit"} for + * classes originating from the core server. + * @param clazz target class to extract annotation from. + * @return empty set if no annotations were found, or a collection of permissions, any of which + * are suitable to enable access. + * @throws PermissionBackendException the annotation could not be parsed. + */ + public static Set<GlobalOrPluginPermission> fromAnnotation( + @Nullable String pluginName, Class<?> clazz) throws PermissionBackendException { + RequiresCapability rc = findAnnotation(clazz, RequiresCapability.class); + RequiresAnyCapability rac = findAnnotation(clazz, RequiresAnyCapability.class); + if (rc != null && rac != null) { + log.error( + String.format( + "Class %s uses both @%s and @%s", + clazz.getName(), + RequiresCapability.class.getSimpleName(), + RequiresAnyCapability.class.getSimpleName())); + throw new PermissionBackendException("cannot extract permission"); + } else if (rc != null) { + return Collections.singleton( + resolve( + pluginName, + rc.value(), + rc.scope(), + rc.fallBackToAdmin(), + clazz, + RequiresCapability.class)); + } else if (rac != null) { + Set<GlobalOrPluginPermission> r = new LinkedHashSet<>(); + for (String capability : rac.value()) { + r.add( + resolve( + pluginName, + capability, + rac.scope(), + rac.fallBackToAdmin(), + clazz, + RequiresAnyCapability.class)); + } + return Collections.unmodifiableSet(r); + } else { + return Collections.emptySet(); + } + } + + public static Set<GlobalOrPluginPermission> fromAnnotation(Class<?> clazz) + throws PermissionBackendException { + return fromAnnotation(null, clazz); + } + + private final String name; + + GlobalPermission(String name) { + this.name = name; + } + + /** @return name used in {@code project.config} permissions. */ + @Override + public String permissionName() { + return name; + } + + @Override + public String describeForException() { + return toString().toLowerCase(Locale.US).replace('_', ' '); + } + + private static GlobalOrPluginPermission resolve( + @Nullable String pluginName, + String capability, + CapabilityScope scope, + boolean fallBackToAdmin, + Class<?> clazz, + Class<?> annotationClass) + throws PermissionBackendException { + if (pluginName != null + && !"gerrit".equals(pluginName) + && (scope == CapabilityScope.PLUGIN || scope == CapabilityScope.CONTEXT)) { + return new PluginPermission(pluginName, capability, fallBackToAdmin); + } + + if (scope == CapabilityScope.PLUGIN) { + log.error( + String.format( + "Class %s uses @%s(scope=%s), but is not within a plugin", + clazz.getName(), annotationClass.getSimpleName(), scope.name())); + throw new PermissionBackendException("cannot extract permission"); + } + + GlobalPermission perm = byName(capability); + if (perm == null) { + log.error( + String.format("Class %s requires unknown capability %s", clazz.getName(), capability)); + throw new PermissionBackendException("cannot extract permission"); + } + return perm; + } + + @Nullable + private static <T extends Annotation> T findAnnotation(Class<?> clazz, Class<T> annotation) { + for (; clazz != null; clazz = clazz.getSuperclass()) { + T t = clazz.getAnnotation(annotation); + if (t != null) { + return t; + } + } + return null; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java new file mode 100644 index 0000000..747c997 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java
@@ -0,0 +1,273 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.permissions; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF; +import static com.google.gerrit.server.permissions.LabelPermission.ForUser.SELF; + +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.common.data.LabelValue; +import com.google.gerrit.common.data.Permission; +import com.google.gerrit.server.util.LabelVote; +import java.util.Optional; + +/** Permission representing a label. */ +public class LabelPermission implements ChangePermissionOrLabel { + public enum ForUser { + SELF, + ON_BEHALF_OF; + } + + private final ForUser forUser; + private final String name; + + /** + * Construct a reference to a label permission. + * + * @param type type description of the label. + */ + public LabelPermission(LabelType type) { + this(SELF, type); + } + + /** + * Construct a reference to a label permission. + * + * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior. + * @param type type description of the label. + */ + public LabelPermission(ForUser forUser, LabelType type) { + this(forUser, type.getName()); + } + + /** + * Construct a reference to a label permission. + * + * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}. + */ + public LabelPermission(String name) { + this(SELF, name); + } + + /** + * Construct a reference to a label permission. + * + * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior. + * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}. + */ + public LabelPermission(ForUser forUser, String name) { + this.forUser = checkNotNull(forUser, "ForUser"); + this.name = LabelType.checkName(name); + } + + /** @return {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */ + public ForUser forUser() { + return forUser; + } + + /** @return name of the label, e.g. {@code "Code-Review"}. */ + public String label() { + return name; + } + + /** @return name used in {@code project.config} permissions. */ + @Override + public Optional<String> permissionName() { + switch (forUser) { + case SELF: + return Optional.of(Permission.forLabel(name)); + case ON_BEHALF_OF: + return Optional.of(Permission.forLabelAs(name)); + } + return Optional.empty(); + } + + @Override + public String describeForException() { + if (forUser == ON_BEHALF_OF) { + return "labelAs " + name; + } + return "label " + name; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof LabelPermission) { + LabelPermission b = (LabelPermission) other; + return forUser == b.forUser && name.equals(b.name); + } + return false; + } + + @Override + public String toString() { + if (forUser == ON_BEHALF_OF) { + return "LabelAs[" + name + ']'; + } + return "Label[" + name + ']'; + } + + /** A {@link LabelPermission} at a specific value. */ + public static class WithValue implements ChangePermissionOrLabel { + private final ForUser forUser; + private final LabelVote label; + + /** + * Construct a reference to a label at a specific value. + * + * @param type description of the label. + * @param value numeric score assigned to the label. + */ + public WithValue(LabelType type, LabelValue value) { + this(SELF, type, value); + } + + /** + * Construct a reference to a label at a specific value. + * + * @param type description of the label. + * @param value numeric score assigned to the label. + */ + public WithValue(LabelType type, short value) { + this(SELF, type.getName(), value); + } + + /** + * Construct a reference to a label at a specific value. + * + * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior. + * @param type description of the label. + * @param value numeric score assigned to the label. + */ + public WithValue(ForUser forUser, LabelType type, LabelValue value) { + this(forUser, type.getName(), value.getValue()); + } + + /** + * Construct a reference to a label at a specific value. + * + * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior. + * @param type description of the label. + * @param value numeric score assigned to the label. + */ + public WithValue(ForUser forUser, LabelType type, short value) { + this(forUser, type.getName(), value); + } + + /** + * Construct a reference to a label at a specific value. + * + * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}. + * @param value numeric score assigned to the label. + */ + public WithValue(String name, short value) { + this(SELF, name, value); + } + + /** + * Construct a reference to a label at a specific value. + * + * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior. + * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}. + * @param value numeric score assigned to the label. + */ + public WithValue(ForUser forUser, String name, short value) { + this(forUser, LabelVote.create(name, value)); + } + + /** + * Construct a reference to a label at a specific value. + * + * @param label label name and vote. + */ + public WithValue(LabelVote label) { + this(SELF, label); + } + + /** + * Construct a reference to a label at a specific value. + * + * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior. + * @param label label name and vote. + */ + public WithValue(ForUser forUser, LabelVote label) { + this.forUser = checkNotNull(forUser, "ForUser"); + this.label = checkNotNull(label, "LabelVote"); + } + + /** @return {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */ + public ForUser forUser() { + return forUser; + } + + /** @return name of the label, e.g. {@code "Code-Review"}. */ + public String label() { + return label.label(); + } + + /** @return specific value of the label, e.g. 1 or 2. */ + public short value() { + return label.value(); + } + + /** @return name used in {@code project.config} permissions. */ + @Override + public Optional<String> permissionName() { + switch (forUser) { + case SELF: + return Optional.of(Permission.forLabel(label())); + case ON_BEHALF_OF: + return Optional.of(Permission.forLabelAs(label())); + } + return Optional.empty(); + } + + @Override + public String describeForException() { + if (forUser == ON_BEHALF_OF) { + return "labelAs " + label.formatWithEquals(); + } + return "label " + label.formatWithEquals(); + } + + @Override + public int hashCode() { + return label.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (other instanceof WithValue) { + WithValue b = (WithValue) other; + return forUser == b.forUser && label.equals(b.label); + } + return false; + } + + @Override + public String toString() { + if (forUser == ON_BEHALF_OF) { + return "LabelAs[" + label.format() + ']'; + } + return "Label[" + label.format() + ']'; + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java new file mode 100644 index 0000000..93db963 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -0,0 +1,434 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.permissions; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toSet; + +import com.google.common.collect.Sets; +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.reviewdb.client.Branch; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.project.DefaultPermissionBackend; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gwtorm.server.OrmException; +import com.google.inject.ImplementedBy; +import com.google.inject.Provider; +import com.google.inject.util.Providers; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Checks authorization to perform an action on a project, reference, or change. + * + * <p>{@code check} methods should be used during action handlers to verify the user is allowed to + * exercise the specified permission. For convenience in implementation {@code check} methods throw + * {@link AuthException} if the permission is denied. + * + * <p>{@code test} methods should be used when constructing replies to the client and the result + * object needs to include a true/false hint indicating the user's ability to exercise the + * permission. This is suitable for configuring UI button state, but should not be relied upon to + * guard handlers before making state changes. + * + * <p>{@code PermissionBackend} is a singleton for the server, acting as a factory for lightweight + * request instances. Implementation classes may cache supporting data inside of {@link WithUser}, + * {@link ForProject}, {@link ForRef}, and {@link ForChange} instances, in addition to storing + * within {@link CurrentUser} using a {@link com.google.gerrit.server.CurrentUser.PropertyKey}. + * {@link GlobalPermission} caching for {@link WithUser} may best cached inside {@link CurrentUser} + * as {@link WithUser} instances are frequently created. + * + * <p>Example use: + * + * <pre> + * private final PermissionBackend permissions; + * private final Provider<CurrentUser> user; + * + * @Inject + * Foo(PermissionBackend permissions, Provider<CurrentUser> user) { + * this.permissions = permissions; + * this.user = user; + * } + * + * public void apply(...) { + * permissions.user(user).change(cd).check(ChangePermission.SUBMIT); + * } + * + * public UiAction.Description getDescription(ChangeResource rsrc) { + * return new UiAction.Description() + * .setLabel("Submit") + * .setVisible(rsrc.permissions().testOrFalse(ChangePermission.SUBMIT)); + * } + * </pre> + */ +@ImplementedBy(DefaultPermissionBackend.class) +public abstract class PermissionBackend { + private static final Logger logger = LoggerFactory.getLogger(PermissionBackend.class); + + /** @return lightweight factory scoped to answer for the specified user. */ + public abstract WithUser user(CurrentUser user); + + /** @return lightweight factory scoped to answer for the specified user. */ + public <U extends CurrentUser> WithUser user(Provider<U> user) { + return user(checkNotNull(user, "Provider<CurrentUser>").get()); + } + + /** PermissionBackend with an optional per-request ReviewDb handle. */ + public abstract static class AcceptsReviewDb<T> { + protected Provider<ReviewDb> db; + + public T database(Provider<ReviewDb> db) { + if (db != null) { + this.db = db; + } + return self(); + } + + public T database(ReviewDb db) { + return database(Providers.of(checkNotNull(db, "ReviewDb"))); + } + + @SuppressWarnings("unchecked") + private T self() { + return (T) this; + } + } + + /** PermissionBackend scoped to a specific user. */ + public abstract static class WithUser extends AcceptsReviewDb<WithUser> { + /** @return instance scoped for the specified project. */ + public abstract ForProject project(Project.NameKey project); + + /** @return instance scoped for the {@code ref}, and its parent project. */ + public ForRef ref(Branch.NameKey ref) { + return project(ref.getParentKey()).ref(ref.get()).database(db); + } + + /** @return instance scoped for the change, and its destination ref and project. */ + public ForChange change(ChangeData cd) { + try { + return ref(cd.change().getDest()).change(cd); + } catch (OrmException e) { + return FailedPermissionBackend.change("unavailable", e); + } + } + + /** @return instance scoped for the change, and its destination ref and project. */ + public ForChange change(ChangeNotes notes) { + return ref(notes.getChange().getDest()).change(notes); + } + + /** Verify scoped user can {@code perm}, throwing if denied. */ + public abstract void check(GlobalOrPluginPermission perm) + throws AuthException, PermissionBackendException; + + /** + * Verify scoped user can perform at least one listed permission. + * + * <p>If {@code any} is empty, the method completes normally and allows the caller to continue. + * Since no permissions were supplied to check, its assumed no permissions are necessary to + * continue with the caller's operation. + * + * <p>If the user has at least one of the permissions in {@code any}, the method completes + * normally, possibly without checking all listed permissions. + * + * <p>If {@code any} is non-empty and the user has none, {@link AuthException} is thrown for one + * of the failed permissions. + * + * @param any set of permissions to check. + */ + public void checkAny(Set<GlobalOrPluginPermission> any) + throws PermissionBackendException, AuthException { + for (Iterator<GlobalOrPluginPermission> itr = any.iterator(); itr.hasNext(); ) { + try { + check(itr.next()); + return; + } catch (AuthException err) { + if (!itr.hasNext()) { + throw err; + } + } + } + } + + /** Filter {@code permSet} to permissions scoped user might be able to perform. */ + public abstract <T extends GlobalOrPluginPermission> Set<T> test(Collection<T> permSet) + throws PermissionBackendException; + + public boolean test(GlobalOrPluginPermission perm) throws PermissionBackendException { + return test(Collections.singleton(perm)).contains(perm); + } + + public boolean testOrFalse(GlobalOrPluginPermission perm) { + try { + return test(perm); + } catch (PermissionBackendException e) { + logger.warn("Cannot test " + perm + "; assuming false", e); + return false; + } + } + + /** + * Filter a set of projects using {@code check(perm)}. + * + * @param perm required permission in a project to be included in result. + * @param projects candidate set of projects; may be empty. + * @return filtered set of {@code projects} where {@code check(perm)} was successful. + * @throws PermissionBackendException backend cannot access its internal state. + */ + public Set<Project.NameKey> filter(ProjectPermission perm, Collection<Project.NameKey> projects) + throws PermissionBackendException { + checkNotNull(perm, "ProjectPermission"); + checkNotNull(projects, "projects"); + Set<Project.NameKey> allowed = Sets.newHashSetWithExpectedSize(projects.size()); + for (Project.NameKey project : projects) { + try { + project(project).check(perm); + allowed.add(project); + } catch (AuthException e) { + // Do not include this project in allowed. + } + } + return allowed; + } + } + + /** PermissionBackend scoped to a user and project. */ + public abstract static class ForProject extends AcceptsReviewDb<ForProject> { + /** @return new instance rescoped to same project, but different {@code user}. */ + public abstract ForProject user(CurrentUser user); + + /** @return instance scoped for {@code ref} in this project. */ + public abstract ForRef ref(String ref); + + /** @return instance scoped for the change, and its destination ref and project. */ + public ForChange change(ChangeData cd) { + try { + return ref(cd.change().getDest().get()).change(cd); + } catch (OrmException e) { + return FailedPermissionBackend.change("unavailable", e); + } + } + + /** @return instance scoped for the change, and its destination ref and project. */ + public ForChange change(ChangeNotes notes) { + return ref(notes.getChange().getDest().get()).change(notes); + } + + /** Verify scoped user can {@code perm}, throwing if denied. */ + public abstract void check(ProjectPermission perm) + throws AuthException, PermissionBackendException; + + /** Filter {@code permSet} to permissions scoped user might be able to perform. */ + public abstract Set<ProjectPermission> test(Collection<ProjectPermission> permSet) + throws PermissionBackendException; + + public boolean test(ProjectPermission perm) throws PermissionBackendException { + return test(EnumSet.of(perm)).contains(perm); + } + + public boolean testOrFalse(ProjectPermission perm) { + try { + return test(perm); + } catch (PermissionBackendException e) { + logger.warn("Cannot test " + perm + "; assuming false", e); + return false; + } + } + } + + /** PermissionBackend scoped to a user, project and reference. */ + public abstract static class ForRef extends AcceptsReviewDb<ForRef> { + /** @return new instance rescoped to same reference, but different {@code user}. */ + public abstract ForRef user(CurrentUser user); + + /** @return instance scoped to change. */ + public abstract ForChange change(ChangeData cd); + + /** @return instance scoped to change. */ + public abstract ForChange change(ChangeNotes notes); + + /** Verify scoped user can {@code perm}, throwing if denied. */ + public abstract void check(RefPermission perm) throws AuthException, PermissionBackendException; + + /** Filter {@code permSet} to permissions scoped user might be able to perform. */ + public abstract Set<RefPermission> test(Collection<RefPermission> permSet) + throws PermissionBackendException; + + public boolean test(RefPermission perm) throws PermissionBackendException { + return test(EnumSet.of(perm)).contains(perm); + } + + /** + * Test if user may be able to perform the permission. + * + * <p>Similar to {@link #test(RefPermission)} except this method returns {@code false} instead + * of throwing an exception. + * + * @param perm the permission to test. + * @return true if the user might be able to perform the permission; false if the user may be + * missing the necessary grants or state, or if the backend threw an exception. + */ + public boolean testOrFalse(RefPermission perm) { + try { + return test(perm); + } catch (PermissionBackendException e) { + logger.warn("Cannot test " + perm + "; assuming false", e); + return false; + } + } + } + + /** PermissionBackend scoped to a user, project, reference and change. */ + public abstract static class ForChange extends AcceptsReviewDb<ForChange> { + /** @return user this instance is scoped to. */ + public abstract CurrentUser user(); + + /** @return new instance rescoped to same change, but different {@code user}. */ + public abstract ForChange user(CurrentUser user); + + /** Verify scoped user can {@code perm}, throwing if denied. */ + public abstract void check(ChangePermissionOrLabel perm) + throws AuthException, PermissionBackendException; + + /** Filter {@code permSet} to permissions scoped user might be able to perform. */ + public abstract <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet) + throws PermissionBackendException; + + public boolean test(ChangePermissionOrLabel perm) throws PermissionBackendException { + return test(Collections.singleton(perm)).contains(perm); + } + + /** + * Test if user may be able to perform the permission. + * + * <p>Similar to {@link #test(ChangePermissionOrLabel)} except this method returns {@code false} + * instead of throwing an exception. + * + * @param perm the permission to test. + * @return true if the user might be able to perform the permission; false if the user may be + * missing the necessary grants or state, or if the backend threw an exception. + */ + public boolean testOrFalse(ChangePermissionOrLabel perm) { + try { + return test(perm); + } catch (PermissionBackendException e) { + logger.warn("Cannot test " + perm + "; assuming false", e); + return false; + } + } + + /** + * Test which values of a label the user may be able to set. + * + * @param label definition of the label to test values of. + * @return set containing values the user may be able to use; may be empty if none. + * @throws PermissionBackendException if failure consulting backend configuration. + */ + public Set<LabelPermission.WithValue> test(LabelType label) throws PermissionBackendException { + return test(valuesOf(checkNotNull(label, "LabelType"))); + } + + /** + * Test which values of a group of labels the user may be able to set. + * + * @param types definition of the labels to test values of. + * @return set containing values the user may be able to use; may be empty if none. + * @throws PermissionBackendException if failure consulting backend configuration. + */ + public Set<LabelPermission.WithValue> testLabels(Collection<LabelType> types) + throws PermissionBackendException { + checkNotNull(types, "LabelType"); + return test(types.stream().flatMap((t) -> valuesOf(t).stream()).collect(toSet())); + } + + private static Set<LabelPermission.WithValue> valuesOf(LabelType label) { + return label + .getValues() + .stream() + .map((v) -> new LabelPermission.WithValue(label, v)) + .collect(toSet()); + } + + /** + * Squash a label value to the nearest allowed value. + * + * <p>For multi-valued labels like Code-Review with values -2..+2 a user may try to use +2, but + * only have permission for the -1..+1 range. The caller should have already tried: + * + * <pre> + * check(new LabelPermission.WithValue("Code-Review", 2)); + * </pre> + * + * and caught {@link AuthException}. {@code squashThenCheck} will use {@link #test(LabelType)} + * to determine potential values of Code-Review the user can use, and select the nearest value + * along the same sign, e.g. -1 for -2 and +1 for +2. + * + * @param label definition of the label to test values of. + * @param val previously denied value the user attempted. + * @return nearest allowed value, or {@code 0} if no value was allowed. + * @throws PermissionBackendException backend cannot run test or check. + */ + public short squashThenCheck(LabelType label, short val) throws PermissionBackendException { + short s = squashByTest(label, val); + if (s == 0 || s == val) { + return 0; + } + try { + check(new LabelPermission.WithValue(label, s)); + return s; + } catch (AuthException e) { + return 0; + } + } + + /** + * Squash a label value to the nearest allowed value using only test methods. + * + * <p>Tests all possible values and selects the closet available to {@code val} while matching + * the sign of {@code val}. Unlike {@code #squashThenCheck(LabelType, short)} this method only + * uses {@code test} methods and should not be used in contexts like a review handler without + * checking the resulting score. + * + * @param label definition of the label to test values of. + * @param val previously denied value the user attempted. + * @return nearest likely allowed value, or {@code 0} if no value was identified. + * @throws PermissionBackendException backend cannot run test. + */ + public short squashByTest(LabelType label, short val) throws PermissionBackendException { + return nearest(test(label), val); + } + + private static short nearest(Iterable<LabelPermission.WithValue> possible, short wanted) { + short s = 0; + for (LabelPermission.WithValue v : possible) { + if ((wanted < 0 && v.value() < 0 && wanted <= v.value() && v.value() < s) + || (wanted > 0 && v.value() > 0 && wanted >= v.value() && v.value() > s)) { + s = v.value(); + } + } + return s; + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackendException.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackendException.java new file mode 100644 index 0000000..be02a6f --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackendException.java
@@ -0,0 +1,40 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.permissions; + +import com.google.gerrit.common.Nullable; +import com.google.gerrit.server.account.GroupBackend; + +/** + * Thrown when {@link PermissionBackend} cannot compute the result. + * + * <p>This is typically a transient failure, such as a required {@link GroupBackend} not responding + * to membership requests. + */ +public class PermissionBackendException extends Exception { + private static final long serialVersionUID = 1L; + + public PermissionBackendException(String message) { + super(message); + } + + public PermissionBackendException(@Nullable Throwable cause) { + super(cause); + } + + public PermissionBackendException(String message, @Nullable Throwable cause) { + super(message, cause); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java new file mode 100644 index 0000000..3078437 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/ProjectPermission.java
@@ -0,0 +1,85 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.permissions; + +import com.google.gerrit.common.data.Permission; +import java.util.Locale; +import java.util.Optional; + +public enum ProjectPermission { + /** + * Can access at least one reference or change within the repository. + * + * <p>Checking this permission instead of {@link #READ} may require filtering to hide specific + * references or changes, which can be expensive. + */ + ACCESS, + + /** + * Can read all references in the repository. + * + * <p>This is a stronger form of {@link #ACCESS} where no filtering is required. + */ + READ(Permission.READ), + + /** + * Can create at least one reference in the project. + * + * <p>This project level permission only validates the user may create some type of reference + * within the project. The exact reference name must be checked at creation: + * + * <pre>permissionBackend + * .user(user) + * .project(proj) + * .ref(ref) + * .check(RefPermission.CREATE); + * </pre> + */ + CREATE_REF, + + /** + * Can create at least one change in the project. + * + * <p>This project level permission only validates the user may create a change for some branch + * within the project. The exact reference name must be checked at creation: + * + * <pre>permissionBackend + * .user(user) + * .project(proj) + * .ref(ref) + * .check(RefPermission.CREATE_CHANGE); + * </pre> + */ + CREATE_CHANGE; + + private final String name; + + ProjectPermission() { + name = null; + } + + ProjectPermission(String name) { + this.name = name; + } + + /** @return name used in {@code project.config} permissions. */ + public Optional<String> permissionName() { + return Optional.ofNullable(name); + } + + public String describeForException() { + return toString().toLowerCase(Locale.US).replace('_', ' '); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java new file mode 100644 index 0000000..e03272b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/RefPermission.java
@@ -0,0 +1,64 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.permissions; + +import com.google.gerrit.common.data.Permission; +import java.util.Locale; +import java.util.Optional; + +public enum RefPermission { + READ(Permission.READ), + CREATE(Permission.CREATE), + DELETE(Permission.DELETE), + UPDATE(Permission.PUSH), + FORCE_UPDATE, + + FORGE_AUTHOR(Permission.FORGE_AUTHOR), + FORGE_COMMITTER(Permission.FORGE_COMMITTER), + FORGE_SERVER(Permission.FORGE_SERVER), + MERGE, + BYPASS_REVIEW, + + /** Create a change to code review a commit. */ + CREATE_CHANGE, + + /** + * Creates changes, then also immediately submits them during {@code push}. + * + * <p>This is similar to {@link #UPDATE} except it constructs changes first, then submits them + * according to the submit strategy, which may include cherry-pick or rebase. By creating changes + * for each commit, automatic server side rebase, and post-update review are enabled. + */ + UPDATE_BY_SUBMIT; + + private final String name; + + RefPermission() { + name = null; + } + + RefPermission(String name) { + this.name = name; + } + + /** @return name used in {@code project.config} permissions. */ + public Optional<String> permissionName() { + return Optional.ofNullable(name); + } + + public String describeForException() { + return toString().toLowerCase(Locale.US).replace('_', ' '); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DelegatingClassLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DelegatingClassLoader.java new file mode 100644 index 0000000..daee9c7 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DelegatingClassLoader.java
@@ -0,0 +1,71 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.plugins; + +import com.google.common.io.ByteStreams; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Enumeration; + +public class DelegatingClassLoader extends ClassLoader { + private final ClassLoader target; + + public DelegatingClassLoader(ClassLoader parent, ClassLoader target) { + super(parent); + this.target = target; + } + + @Override + public Class<?> findClass(String name) throws ClassNotFoundException { + String path = name.replace('.', '/') + ".class"; + InputStream resource = target.getResourceAsStream(path); + if (resource != null) { + try { + byte[] bytes = ByteStreams.toByteArray(resource); + return defineClass(name, bytes, 0, bytes.length); + } catch (IOException e) { + } + } + throw new ClassNotFoundException(name); + } + + @Override + public URL getResource(String name) { + URL rtn = getParent().getResource(name); + if (rtn == null) { + rtn = target.getResource(name); + } + return rtn; + } + + @Override + public Enumeration<URL> getResources(String name) throws IOException { + Enumeration<URL> rtn = getParent().getResources(name); + if (rtn == null) { + rtn = target.getResources(name); + } + return rtn; + } + + @Override + public InputStream getResourceAsStream(String name) { + InputStream rtn = getParent().getResourceAsStream(name); + if (rtn == null) { + rtn = target.getResourceAsStream(name); + } + return rtn; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java index 1453854..4bf5aa3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/MultipleProvidersForPluginException.java
@@ -16,8 +16,8 @@ import static java.util.stream.Collectors.joining; +import com.google.common.collect.Streams; import java.nio.file.Path; -import java.util.stream.StreamSupport; class MultipleProvidersForPluginException extends IllegalArgumentException { private static final long serialVersionUID = 1L; @@ -31,7 +31,7 @@ } private static String providersListToString(Iterable<ServerPluginProvider> providersHandlers) { - return StreamSupport.stream(providersHandlers.spliterator(), false) + return Streams.stream(providersHandlers) .map(ServerPluginProvider::getProviderPluginName) .collect(joining(", ")); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java index ac57bc9..3ed8b37 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -139,7 +139,7 @@ } } - public static List<Path> listPlugins(Path pluginsDir, final String suffix) throws IOException { + public static List<Path> listPlugins(Path pluginsDir, String suffix) throws IOException { if (pluginsDir == null || !Files.exists(pluginsDir)) { return ImmutableList.of(); } @@ -432,7 +432,7 @@ String name = entry.getKey(); Path path = entry.getValue(); String fileName = path.getFileName().toString(); - if (!isJsPlugin(fileName) && !serverPluginFactory.handles(path)) { + if (!isUiPlugin(fileName) && !serverPluginFactory.handles(path)) { log.warn("No Plugin provider was found that handles this file format: {}", fileName); continue; } @@ -614,7 +614,7 @@ private Plugin loadPlugin(String name, Path srcPlugin, FileSnapshot snapshot) throws InvalidPluginException { String pluginName = srcPlugin.getFileName().toString(); - if (isJsPlugin(pluginName)) { + if (isUiPlugin(pluginName)) { return loadJsPlugin(name, srcPlugin, snapshot); } else if (serverPluginFactory.handles(srcPlugin)) { return loadServerPlugin(srcPlugin, snapshot); @@ -746,8 +746,8 @@ public String getGerritPluginName(Path srcPath) { String fileName = srcPath.getFileName().toString(); - if (isJsPlugin(fileName)) { - return fileName.substring(0, fileName.length() - 3); + if (isUiPlugin(fileName)) { + return fileName.substring(0, fileName.lastIndexOf('.')); } if (serverPluginFactory.handles(srcPath)) { return serverPluginFactory.getPluginName(srcPath); @@ -763,8 +763,8 @@ return map; } - private static boolean isJsPlugin(String name) { - return isPlugin(name, "js"); + private static boolean isUiPlugin(String name) { + return isPlugin(name, "js") || isPlugin(name, "html"); } private static boolean isPlugin(String fileName, String ext) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java index 36b0631..ff48a7d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerPlugin.java
@@ -247,7 +247,7 @@ serverManager.start(); } - private Injector newRootInjector(final PluginGuiceEnvironment env) { + private Injector newRootInjector(PluginGuiceEnvironment env) { List<Module> modules = Lists.newArrayListWithCapacity(2); if (getApiType() == ApiType.PLUGIN) { modules.add(env.getSysModule());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java index 91441d8..50b8752 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/UniversalServerPluginProvider.java
@@ -78,7 +78,7 @@ } } - private List<ServerPluginProvider> providersForHandlingPlugin(final Path srcPath) { + private List<ServerPluginProvider> providersForHandlingPlugin(Path srcPath) { List<ServerPluginProvider> providers = new ArrayList<>(); for (ServerPluginProvider serverPluginProvider : serverPluginProviders) { boolean handles = serverPluginProvider.handles(srcPath);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java index 5c0d8d7..6d77267 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
@@ -46,8 +46,5 @@ .annotatedWith(GitReceivePackGroups.class) .toProvider(GitReceivePackGroupsProvider.class) .in(SINGLETON); - - bind(ChangeControl.Factory.class); - factory(ProjectControl.AssistedFactory.class); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java index 41e8fbc..278b2af 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/BanCommit.java
@@ -17,21 +17,24 @@ import com.google.common.collect.Lists; import com.google.gerrit.common.errors.PermissionDeniedException; import com.google.gerrit.extensions.restapi.AuthException; -import com.google.gerrit.extensions.restapi.ResourceConflictException; -import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.server.git.BanCommitResult; +import com.google.gerrit.server.project.BanCommit.BanResultInfo; import com.google.gerrit.server.project.BanCommit.Input; +import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.update.RetryHelper; +import com.google.gerrit.server.update.RetryingRestModifyView; +import com.google.gerrit.server.update.UpdateException; import com.google.inject.Inject; import com.google.inject.Singleton; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.lib.ObjectId; @Singleton -public class BanCommit implements RestModifyView<ProjectResource, Input> { +public class BanCommit extends RetryingRestModifyView<ProjectResource, Input, BanResultInfo> { public static class Input { public List<String> commits; public String reason; @@ -50,13 +53,15 @@ private final com.google.gerrit.server.git.BanCommit banCommit; @Inject - BanCommit(com.google.gerrit.server.git.BanCommit banCommit) { + BanCommit(RetryHelper retryHelper, com.google.gerrit.server.git.BanCommit banCommit) { + super(retryHelper); this.banCommit = banCommit; } @Override - public BanResultInfo apply(ProjectResource rsrc, Input input) - throws UnprocessableEntityException, AuthException, ResourceConflictException, IOException { + protected BanResultInfo applyImpl( + BatchUpdate.Factory updateFactory, ProjectResource rsrc, Input input) + throws RestApiException, UpdateException, IOException { BanResultInfo r = new BanResultInfo(); if (input != null && input.commits != null && !input.commits.isEmpty()) { List<ObjectId> commitsToBan = new ArrayList<>(input.commits.size()); @@ -75,8 +80,6 @@ r.ignored = transformCommits(result.getIgnoredObjectIds()); } catch (PermissionDeniedException e) { throw new AuthException(e.getMessage()); - } catch (ConcurrentRefUpdateException e) { - throw new ResourceConflictException(e.getMessage(), e); } } return r;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java index ec114d8..94f5ebf 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -15,13 +15,18 @@ package com.google.gerrit.server.project; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF; import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; import com.google.gerrit.common.data.PermissionRange; import com.google.gerrit.common.data.RefConfigSection; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; @@ -32,13 +37,23 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.ChangePermissionOrLabel; +import com.google.gerrit.server.permissions.LabelPermission; +import com.google.gerrit.server.permissions.PermissionBackend.ForChange; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.RefPermission; import com.google.gerrit.server.query.change.ChangeData; import com.google.gwtorm.server.OrmException; 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.EnumSet; import java.util.List; +import java.util.Map; +import java.util.Set; /** Access control management for a user accessing a single change. */ public class ChangeControl { @@ -157,7 +172,7 @@ this.patchSetUtil = patchSetUtil; } - public ChangeControl forUser(final CurrentUser who) { + public ChangeControl forUser(CurrentUser who) { if (getUser().equals(who)) { return this; } @@ -200,14 +215,12 @@ /** Can this user see this change? */ public boolean isVisible(ReviewDb db, @Nullable ChangeData cd) throws OrmException { + if (getChange().isPrivate() && !isPrivateVisible(db, cd)) { + return false; + } if (getChange().getStatus() == Change.Status.DRAFT && !isDraftVisible(db, cd)) { return false; } - return isRefVisible(); - } - - /** Can the user see this change? Does not account for draft status */ - public boolean isRefVisible() { return getRefControl().isVisible(); } @@ -230,23 +243,17 @@ } /** Can this user abandon this change? */ - public boolean canAbandon(ReviewDb db) throws OrmException { + private boolean canAbandon(ReviewDb db) throws OrmException { return (isOwner() // owner (aka creator) of the change can abandon || getRefControl().isOwner() // branch owner can abandon || getProjectControl().isOwner() // project owner can abandon - || getUser().getCapabilities().canAdministrateServer() // site administers are god || getRefControl().canAbandon() // user can abandon a specific ref - ) + || getProjectControl().isAdmin()) && !isPatchSetLocked(db); } - /** Can this user change the destination branch of this change to the new ref? */ - public boolean canMoveTo(String ref, ReviewDb db) throws OrmException { - return getProjectControl().controlForRef(ref).canUpload() && canAbandon(db); - } - /** Can this user publish this draft change or any draft patch set of this change? */ - public boolean canPublish(final ReviewDb db) throws OrmException { + public boolean canPublish(ReviewDb db) throws OrmException { return (isOwner() || getRefControl().canPublishDrafts()) && isVisible(db); } @@ -258,10 +265,11 @@ switch (status) { case DRAFT: - return (isOwner() || getRefControl().canDeleteDrafts()); + return isOwner() || getRefControl().canDeleteDrafts() || getProjectControl().isAdmin(); case NEW: case ABANDONED: - return (isAdmin() || (isOwner() && getRefControl().canDeleteOwnChanges())); + return (isOwner() && getRefControl().canDeleteOwnChanges()) + || getProjectControl().isAdmin(); case MERGED: default: return false; @@ -269,15 +277,16 @@ } /** Can this user rebase this change? */ - public boolean canRebase(ReviewDb db) throws OrmException { + private boolean canRebase(ReviewDb db) throws OrmException { return (isOwner() || getRefControl().canSubmit(isOwner()) || getRefControl().canRebase()) + && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE) && !isPatchSetLocked(db); } /** Can this user restore this change? */ - public boolean canRestore(ReviewDb db) throws OrmException { - return canAbandon(db) // Anyone who can abandon the change can restore it back - && getRefControl().canUpload(); // as long as you can upload too + private boolean canRestore(ReviewDb db) throws OrmException { + // Anyone who can abandon the change can restore it, as long as they can create changes. + return canAbandon(db) && refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE); } /** All available label types for this change. */ @@ -314,8 +323,8 @@ } /** Can this user add a patch set to this change? */ - public boolean canAddPatchSet(ReviewDb db) throws OrmException { - if (!getRefControl().canUpload() + private boolean canAddPatchSet(ReviewDb db) throws OrmException { + if (!refControl.asForRef().testOrFalse(RefPermission.CREATE_CHANGE) || isPatchSetLocked(db) || !isPatchVisible(patchSetUtil.current(db, notes), db)) { return false; @@ -333,7 +342,7 @@ } for (PatchSetApproval ap : - approvalsUtil.byPatchSet(db, this, getChange().currentPatchSetId())) { + approvalsUtil.byPatchSet(db, this, getChange().currentPatchSetId(), null, null)) { LabelType type = getLabelTypes().byLabel(ap.getLabel()); if (type != null && ap.getValue() == 1 @@ -345,7 +354,7 @@ } /** Is this user the owner of the change? */ - public boolean isOwner() { + private boolean isOwner() { if (getUser().isIdentifiedUser()) { Account.Id id = getUser().asIdentifiedUser().getAccountId(); return id.equals(getChange().getOwner()); @@ -354,7 +363,7 @@ } /** Is this user assigned to this change? */ - public boolean isAssignee() { + private boolean isAssignee() { Account.Id currentAssignee = notes.getChange().getAssignee(); if (currentAssignee != null && getUser().isIdentifiedUser()) { Account.Id id = getUser().getAccountId(); @@ -364,12 +373,7 @@ } /** Is this user a reviewer for the change? */ - public boolean isReviewer(ReviewDb db) throws OrmException { - return isReviewer(db, null); - } - - /** Is this user a reviewer for the change? */ - public boolean isReviewer(ReviewDb db, @Nullable ChangeData cd) throws OrmException { + private boolean isReviewer(ReviewDb db, @Nullable ChangeData cd) throws OrmException { if (getUser().isIdentifiedUser()) { Collection<Account.Id> results = changeData(db, cd).reviewers().all(); return results.contains(getUser().getAccountId()); @@ -377,10 +381,6 @@ return false; } - public boolean isAdmin() { - return getUser().getCapabilities().canAdministrateServer(); - } - /** @return true if the user is allowed to remove this reviewer. */ public boolean canRemoveReviewer(PatchSetApproval approval) { return canRemoveReviewer(approval.getAccountId(), approval.getValue()); @@ -407,7 +407,7 @@ if (getRefControl().canRemoveReviewer() // has removal permissions || getRefControl().isOwner() // branch owner || getProjectControl().isOwner() // project owner - || getUser().getCapabilities().canAdministrateServer()) { + || getProjectControl().isAdmin()) { return true; } } @@ -416,31 +416,29 @@ } /** Can this user edit the topic name? */ - public boolean canEditTopicName() { + private boolean canEditTopicName() { if (getChange().getStatus().isOpen()) { return isOwner() // owner (aka creator) of the change can edit topic || getRefControl().isOwner() // branch owner can edit topic || getProjectControl().isOwner() // project owner can edit topic - || getUser().getCapabilities().canAdministrateServer() // site administers are god || getRefControl().canEditTopicName() // user can edit topic on a specific ref - ; + || getProjectControl().isAdmin(); } return getRefControl().canForceEditTopicName(); } /** Can this user edit the description? */ - public boolean canEditDescription() { + private boolean canEditDescription() { if (getChange().getStatus().isOpen()) { return isOwner() // owner (aka creator) of the change can edit desc || getRefControl().isOwner() // branch owner can edit desc || getProjectControl().isOwner() // project owner can edit desc - || getUser().getCapabilities().canAdministrateServer() // site administers are god - ; + || getProjectControl().isAdmin(); } return false; } - public boolean canEditAssignee() { + private boolean canEditAssignee() { return isOwner() || getProjectControl().isOwner() || getRefControl().canEditAssignee() @@ -448,20 +446,12 @@ } /** Can this user edit the hashtag name? */ - public boolean canEditHashtags() { + private boolean canEditHashtags() { return isOwner() // owner (aka creator) of the change can edit hashtags || getRefControl().isOwner() // branch owner can edit hashtags || getProjectControl().isOwner() // project owner can edit hashtags - || getUser().getCapabilities().canAdministrateServer() // site administers are god - || getRefControl().canEditHashtags(); // user can edit hashtag on a specific ref - } - - public boolean canSubmit() { - return getRefControl().canSubmit(isOwner()); - } - - public boolean canSubmitAs() { - return getRefControl().canSubmitAs(); + || getRefControl().canEditHashtags() // user can edit hashtag on a specific ref + || getProjectControl().isAdmin(); } private boolean match(String destBranch, String refPattern) { @@ -478,4 +468,155 @@ || getRefControl().canViewDrafts() || getUser().isInternalUser(); } + + private boolean isPrivateVisible(ReviewDb db, ChangeData cd) throws OrmException { + return isOwner() + || isReviewer(db, cd) + || getRefControl().canViewPrivateChanges() + || getUser().isInternalUser(); + } + + ForChange asForChange(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) { + return new ForChangeImpl(cd, db); + } + + private class ForChangeImpl extends ForChange { + private ChangeData cd; + private Map<String, PermissionRange> labels; + + ForChangeImpl(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) { + this.cd = cd; + this.db = db; + } + + private ReviewDb db() { + if (db != null) { + return db.get(); + } else if (cd != null) { + return cd.db(); + } else { + return null; + } + } + + private ChangeData changeData() { + if (cd == null) { + ReviewDb reviewDb = db(); + checkState(reviewDb != null, "need ReviewDb"); + cd = changeDataFactory.create(reviewDb, ChangeControl.this); + } + return cd; + } + + @Override + public CurrentUser user() { + return getUser(); + } + + @Override + public ForChange user(CurrentUser user) { + return user().equals(user) ? this : forUser(user).asForChange(cd, db); + } + + @Override + public void check(ChangePermissionOrLabel perm) + throws AuthException, PermissionBackendException { + if (!can(perm)) { + throw new AuthException(perm.describeForException() + " not permitted"); + } + } + + @Override + public <T extends ChangePermissionOrLabel> Set<T> test(Collection<T> permSet) + throws PermissionBackendException { + Set<T> ok = newSet(permSet); + for (T perm : permSet) { + if (can(perm)) { + ok.add(perm); + } + } + return ok; + } + + private boolean can(ChangePermissionOrLabel perm) throws PermissionBackendException { + if (perm instanceof ChangePermission) { + return can((ChangePermission) perm); + } else if (perm instanceof LabelPermission) { + return can((LabelPermission) perm); + } else if (perm instanceof LabelPermission.WithValue) { + return can((LabelPermission.WithValue) perm); + } + throw new PermissionBackendException(perm + " unsupported"); + } + + private boolean can(ChangePermission perm) throws PermissionBackendException { + try { + switch (perm) { + case READ: + return isVisible(db(), changeData()); + case ABANDON: + return canAbandon(db()); + case DELETE: + return canDelete(db(), getChange().getStatus()); + case ADD_PATCH_SET: + return canAddPatchSet(db()); + case EDIT_ASSIGNEE: + return canEditAssignee(); + case EDIT_DESCRIPTION: + return canEditDescription(); + case EDIT_HASHTAGS: + return canEditHashtags(); + case EDIT_TOPIC_NAME: + return canEditTopicName(); + case REBASE: + return canRebase(db()); + case RESTORE: + return canRestore(db()); + case SUBMIT: + return getRefControl().canSubmit(isOwner()); + + case REMOVE_REVIEWER: // TODO Honor specific removal filters? + case SUBMIT_AS: + return getRefControl().canPerform(perm.permissionName().get()); + } + } catch (OrmException e) { + throw new PermissionBackendException("unavailable", e); + } + throw new PermissionBackendException(perm + " unsupported"); + } + + private boolean can(LabelPermission perm) { + return !label(perm.permissionName().get()).isEmpty(); + } + + private boolean can(LabelPermission.WithValue perm) { + PermissionRange r = label(perm.permissionName().get()); + if (perm.forUser() == ON_BEHALF_OF && r.isEmpty()) { + return false; + } + return r.contains(perm.value()); + } + + private PermissionRange label(String permission) { + if (labels == null) { + labels = Maps.newHashMapWithExpectedSize(4); + } + PermissionRange r = labels.get(permission); + if (r == null) { + r = getRange(permission); + labels.put(permission, r); + } + return r; + } + } + + static <T extends ChangePermissionOrLabel> Set<T> newSet(Collection<T> permSet) { + if (permSet instanceof EnumSet) { + @SuppressWarnings({"unchecked", "rawtypes"}) + Set<T> s = ((EnumSet) permSet).clone(); + s.clear(); + return s; + } + return Sets.newHashSetWithExpectedSize(permSet.size()); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java index f824f81..1a81726 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java
@@ -92,7 +92,7 @@ try (Repository git = gitManager.openRepository(resource.getNameKey()); RevWalk rw = new RevWalk(git); ObjectInserter inserter = new InMemoryInserter(git)) { - Merger m = MergeUtil.newMerger(git, inserter, strategy); + Merger m = MergeUtil.newMerger(inserter, git.getConfig(), strategy); Ref destRef = git.getRefDatabase().exactRef(resource.getRef()); if (destRef == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java index 7aa5f68..2110034 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChildProjectsCollection.java
@@ -21,6 +21,7 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.extensions.restapi.TopLevelResource; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -50,7 +51,7 @@ @Override public ChildProjectResource parse(ProjectResource parent, IdString id) - throws ResourceNotFoundException, IOException { + throws ResourceNotFoundException, IOException, PermissionBackendException { ProjectResource p = projectsCollection.parse(TopLevelResource.INSTANCE, id); for (ProjectState pp : p.getControl().getProjectState().parents()) { if (parent.getNameKey().equals(pp.getProject().getNameKey())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java index 2f02728..87bea15 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfoImpl.java
@@ -31,7 +31,6 @@ import com.google.gerrit.server.config.ProjectConfigEntry; import com.google.gerrit.server.extensions.webui.UiActions; import com.google.gerrit.server.git.TransferConfig; -import com.google.inject.util.Providers; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; @@ -45,6 +44,7 @@ DynamicMap<ProjectConfigEntry> pluginConfigEntries, PluginConfigFactory cfgFactory, AllProjectsName allProjects, + UiActions uiActions, DynamicMap<RestView<ProjectResource>> views) { ProjectState projectState = control.getProjectState(); Project p = control.getProject(); @@ -58,6 +58,8 @@ InheritedBooleanInfo enableSignedPush = new InheritedBooleanInfo(); InheritedBooleanInfo requireSignedPush = new InheritedBooleanInfo(); InheritedBooleanInfo rejectImplicitMerges = new InheritedBooleanInfo(); + InheritedBooleanInfo enableReviewerByEmail = new InheritedBooleanInfo(); + InheritedBooleanInfo matchAuthorToCommitterDate = new InheritedBooleanInfo(); useContributorAgreements.value = projectState.isUseContributorAgreements(); useSignedOffBy.value = projectState.isUseSignedOffBy(); @@ -73,6 +75,8 @@ enableSignedPush.configuredValue = p.getEnableSignedPush(); requireSignedPush.configuredValue = p.getRequireSignedPush(); rejectImplicitMerges.configuredValue = p.getRejectImplicitMerges(); + enableReviewerByEmail.configuredValue = p.getEnableReviewerByEmail(); + matchAuthorToCommitterDate.configuredValue = p.getMatchAuthorToCommitterDate(); ProjectState parentState = Iterables.getFirst(projectState.parents(), null); if (parentState != null) { @@ -85,6 +89,8 @@ enableSignedPush.inheritedValue = projectState.isEnableSignedPush(); requireSignedPush.inheritedValue = projectState.isRequireSignedPush(); rejectImplicitMerges.inheritedValue = projectState.isRejectImplicitMerges(); + enableReviewerByEmail.inheritedValue = projectState.isEnableReviewerByEmail(); + matchAuthorToCommitterDate.inheritedValue = projectState.isMatchAuthorToCommitterDate(); } this.useContributorAgreements = useContributorAgreements; @@ -93,6 +99,8 @@ this.requireChangeId = requireChangeId; this.rejectImplicitMerges = rejectImplicitMerges; this.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget; + this.enableReviewerByEmail = enableReviewerByEmail; + this.matchAuthorToCommitterDate = matchAuthorToCommitterDate; if (serverEnableSignedPush) { this.enableSignedPush = enableSignedPush; this.requireSignedPush = requireSignedPush; @@ -122,11 +130,12 @@ getPluginConfig(control.getProjectState(), pluginConfigEntries, cfgFactory, allProjects); actions = new TreeMap<>(); - for (UiAction.Description d : - UiActions.from(views, new ProjectResource(control), Providers.of(control.getUser()))) { + for (UiAction.Description d : uiActions.from(views, new ProjectResource(control))) { actions.put(d.getId(), new ActionInfo(d)); } this.theme = projectState.getTheme(); + + this.extensionPanelNames = projectState.getConfig().getExtensionPanelSections(); } private Map<String, Map<String, ConfigParameterInfo>> getPluginConfig(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java index 5919ba1..0c15063 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -22,10 +22,11 @@ import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.RefNames; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.RefPermission; import com.google.gerrit.server.util.MagicBranch; import com.google.inject.Inject; import com.google.inject.Provider; @@ -50,8 +51,8 @@ } private final Provider<IdentifiedUser> identifiedUser; + private final PermissionBackend permissionBackend; private final GitRepositoryManager repoManager; - private final Provider<ReviewDb> db; private final GitReferenceUpdated referenceUpdated; private final RefValidationHelper refCreationValidator; private String ref; @@ -59,14 +60,14 @@ @Inject CreateBranch( Provider<IdentifiedUser> identifiedUser, + PermissionBackend permissionBackend, GitRepositoryManager repoManager, - Provider<ReviewDb> db, GitReferenceUpdated referenceUpdated, RefValidationHelper.Factory refHelperFactory, @Assisted String ref) { this.identifiedUser = identifiedUser; + this.permissionBackend = permissionBackend; this.repoManager = repoManager; - this.db = db; this.referenceUpdated = referenceUpdated; this.refCreationValidator = refHelperFactory.create(ReceiveCommand.Type.CREATE); this.ref = ref; @@ -116,8 +117,9 @@ } } - if (!refControl.canCreate(db.get(), repo, object)) { - throw new AuthException("Cannot create \"" + ref + "\""); + String rejectReason = refControl.canCreate(repo, object); + if (rejectReason != null) { + throw new AuthException("Cannot create \"" + ref + "\": " + rejectReason); } try { @@ -154,7 +156,7 @@ } refPrefix = RefUtil.getRefPrefix(refPrefix); } - //$FALL-THROUGH$ + // $FALL-THROUGH$ case FORCED: case IO_FAILURE: case NOT_ATTEMPTED: @@ -170,7 +172,10 @@ BranchInfo info = new BranchInfo(); info.ref = ref; info.revision = revid.getName(); - info.canDelete = refControl.canDelete() ? true : null; + info.canDelete = + permissionBackend.user(identifiedUser).ref(name).testOrFalse(RefPermission.DELETE) + ? true + : null; return info; } catch (IOException err) { log.error("Cannot create branch \"" + name + "\"", err);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java index ff7e31e..199f5c0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -55,6 +55,7 @@ import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.git.RepositoryCaseMismatchException; import com.google.gerrit.server.group.GroupsCollection; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.validators.ProjectCreationValidationListener; import com.google.gerrit.server.validators.ValidationException; import com.google.inject.Inject; @@ -148,7 +149,8 @@ @Override public Response<ProjectInfo> apply(TopLevelResource resource, ProjectInput input) throws BadRequestException, UnprocessableEntityException, ResourceConflictException, - ResourceNotFoundException, IOException, ConfigInvalidException { + ResourceNotFoundException, IOException, ConfigInvalidException, + PermissionBackendException { if (input == null) { input = new ProjectInput(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java index f674d17..6b1c0e9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateTag.java
@@ -31,6 +31,9 @@ import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.TagCache; +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.RefUtil.InvalidRevisionException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -56,6 +59,7 @@ CreateTag create(String ref); } + private final PermissionBackend permissionBackend; private final Provider<IdentifiedUser> identifiedUser; private final GitRepositoryManager repoManager; private final TagCache tagCache; @@ -64,11 +68,13 @@ @Inject CreateTag( + PermissionBackend permissionBackend, Provider<IdentifiedUser> identifiedUser, GitRepositoryManager repoManager, TagCache tagCache, GitReferenceUpdated referenceUpdated, @Assisted String ref) { + this.permissionBackend = permissionBackend; this.identifiedUser = identifiedUser; this.repoManager = repoManager; this.tagCache = tagCache; @@ -78,7 +84,7 @@ @Override public TagInfo apply(ProjectResource resource, TagInput input) - throws RestApiException, IOException { + throws RestApiException, IOException, PermissionBackendException { if (input == null) { input = new TagInput(); } @@ -92,6 +98,9 @@ ref = RefUtil.normalizeTagRef(ref); RefControl refControl = resource.getControl().controlForRef(ref); + PermissionBackend.ForRef perm = + permissionBackend.user(identifiedUser).project(resource.getNameKey()).ref(ref); + try (Repository repo = repoManager.openRepository(resource.getNameKey())) { ObjectId revid = RefUtil.parseBaseRevision(repo, resource.getNameKey(), input.revision); RevWalk rw = RefUtil.verifyConnected(repo, revid); @@ -103,8 +112,8 @@ throw new MethodNotAllowedException("Cannot create signed tag \"" + ref + "\""); } else if (isAnnotated && !refControl.canPerform(Permission.CREATE_TAG)) { throw new AuthException("Cannot create annotated tag \"" + ref + "\""); - } else if (!refControl.canPerform(Permission.CREATE)) { - throw new AuthException("Cannot create tag \"" + ref + "\""); + } else { + perm.check(RefPermission.CREATE); } if (repo.getRefDatabase().exactRef(ref) != null) { throw new ResourceConflictException("tag \"" + ref + "\" already exists"); @@ -134,7 +143,7 @@ result.getObjectId(), identifiedUser.get().getAccount()); try (RevWalk w = new RevWalk(repo)) { - return ListTags.createTagInfo(result, w, refControl); + return ListTags.createTagInfo(perm, result, w); } } } catch (InvalidRevisionException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java new file mode 100644 index 0000000..ef5e41d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
@@ -0,0 +1,208 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.project; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toSet; + +import com.google.common.collect.Sets; +import com.google.gerrit.common.data.PermissionRule; +import com.google.gerrit.common.data.PermissionRule.Action; +import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission; +import com.google.gerrit.extensions.api.access.PluginPermission; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.PeerDaemonUser; +import com.google.gerrit.server.account.CapabilityCollection; +import com.google.gerrit.server.permissions.FailedPermissionBackend; +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 com.google.inject.Singleton; +import java.io.IOException; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +@Singleton +public class DefaultPermissionBackend extends PermissionBackend { + private static final CurrentUser.PropertyKey<Boolean> IS_ADMIN = CurrentUser.PropertyKey.create(); + + private final ProjectCache projectCache; + + @Inject + DefaultPermissionBackend(ProjectCache projectCache) { + this.projectCache = projectCache; + } + + private CapabilityCollection capabilities() { + return projectCache.getAllProjects().getCapabilityCollection(); + } + + @Override + public WithUser user(CurrentUser user) { + return new WithUserImpl(checkNotNull(user, "user")); + } + + class WithUserImpl extends WithUser { + private final CurrentUser user; + private Boolean admin; + + WithUserImpl(CurrentUser user) { + this.user = checkNotNull(user, "user"); + } + + @Override + public ForProject project(Project.NameKey project) { + try { + ProjectState state = projectCache.checkedGet(project); + if (state != null) { + return state.controlFor(user).asForProject().database(db); + } + return FailedPermissionBackend.project("not found"); + } catch (IOException e) { + return FailedPermissionBackend.project("unavailable", e); + } + } + + @Override + public void check(GlobalOrPluginPermission perm) + throws AuthException, PermissionBackendException { + if (!can(perm)) { + throw new AuthException(perm.describeForException() + " not permitted"); + } + } + + @Override + public <T extends GlobalOrPluginPermission> Set<T> test(Collection<T> permSet) + throws PermissionBackendException { + Set<T> ok = newSet(permSet); + for (T perm : permSet) { + if (can(perm)) { + ok.add(perm); + } + } + return ok; + } + + private boolean can(GlobalOrPluginPermission perm) throws PermissionBackendException { + if (perm instanceof GlobalPermission) { + return can((GlobalPermission) perm); + } else if (perm instanceof PluginPermission) { + PluginPermission pluginPermission = (PluginPermission) perm; + return has(pluginPermission.permissionName()) + || (pluginPermission.fallBackToAdmin() && isAdmin()); + } + throw new PermissionBackendException(perm + " unsupported"); + } + + private boolean can(GlobalPermission perm) throws PermissionBackendException { + switch (perm) { + case ADMINISTRATE_SERVER: + return isAdmin(); + case EMAIL_REVIEWERS: + return canEmailReviewers(); + + case FLUSH_CACHES: + case KILL_TASK: + case RUN_GC: + case VIEW_CACHES: + case VIEW_QUEUE: + return has(perm.permissionName()) || can(GlobalPermission.MAINTAIN_SERVER); + + case CREATE_ACCOUNT: + case CREATE_GROUP: + case CREATE_PROJECT: + case MAINTAIN_SERVER: + case MODIFY_ACCOUNT: + case STREAM_EVENTS: + case VIEW_ALL_ACCOUNTS: + case VIEW_CONNECTIONS: + case VIEW_PLUGINS: + return has(perm.permissionName()) || isAdmin(); + + case ACCESS_DATABASE: + case RUN_AS: + return has(perm.permissionName()); + } + throw new PermissionBackendException(perm + " unsupported"); + } + + private boolean isAdmin() { + if (admin == null) { + admin = computeAdmin(); + } + return admin; + } + + private Boolean computeAdmin() { + Boolean r = user.get(IS_ADMIN); + if (r == null) { + if (user.getRealUser() != user) { + r = false; + } else if (user instanceof PeerDaemonUser) { + r = true; + } else { + r = allow(capabilities().administrateServer); + } + user.put(IS_ADMIN, r); + } + return r; + } + + private boolean canEmailReviewers() { + List<PermissionRule> email = capabilities().emailReviewers; + return allow(email) || notDenied(email); + } + + private boolean has(String permissionName) { + return allow(capabilities().getPermission(permissionName)); + } + + private boolean allow(Collection<PermissionRule> rules) { + return user.getEffectiveGroups() + .containsAnyOf( + rules + .stream() + .filter(r -> r.getAction() == Action.ALLOW) + .map(r -> r.getGroup().getUUID()) + .collect(toSet())); + } + + private boolean notDenied(Collection<PermissionRule> rules) { + Set<AccountGroup.UUID> denied = + rules + .stream() + .filter(r -> r.getAction() != Action.ALLOW) + .map(r -> r.getGroup().getUUID()) + .collect(toSet()); + return denied.isEmpty() || !user.getEffectiveGroups().containsAnyOf(denied); + } + } + + private static <T extends GlobalOrPluginPermission> Set<T> newSet(Collection<T> permSet) { + if (permSet instanceof EnumSet) { + @SuppressWarnings({"unchecked", "rawtypes"}) + Set<T> s = ((EnumSet) permSet).clone(); + s.clear(); + return s; + } + return Sets.newHashSetWithExpectedSize(permSet.size()); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java new file mode 100644 index 0000000..8fa049d --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackendModule.java
@@ -0,0 +1,39 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.project; + +import com.google.gerrit.extensions.config.FactoryModule; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.inject.AbstractModule; + +/** Binds the default {@link PermissionBackend}. */ +public class DefaultPermissionBackendModule extends AbstractModule { + @Override + protected void configure() { + install(new LegacyControlsModule()); + } + + /** Binds legacy ProjectControl, RefControl, ChangeControl. */ + public static class LegacyControlsModule extends FactoryModule { + @Override + protected void configure() { + // TODO(sop) Hide ProjectControl, RefControl, ChangeControl related bindings. + bind(ProjectControl.GenericFactory.class); + factory(ProjectControl.AssistedFactory.class); + bind(ChangeControl.GenericFactory.class); + bind(ChangeControl.Factory.class); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java index 049e2e3..8cd44d1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranch.java
@@ -16,11 +16,14 @@ import static org.eclipse.jgit.lib.Constants.R_HEADS; -import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; 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.CurrentUser; +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.DeleteBranch.Input; import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gwtorm.server.OrmException; @@ -35,19 +38,25 @@ private final Provider<InternalChangeQuery> queryProvider; private final DeleteRef.Factory deleteRefFactory; + private final Provider<CurrentUser> user; + private final PermissionBackend permissionBackend; @Inject - DeleteBranch(Provider<InternalChangeQuery> queryProvider, DeleteRef.Factory deleteRefFactory) { + DeleteBranch( + Provider<InternalChangeQuery> queryProvider, + DeleteRef.Factory deleteRefFactory, + Provider<CurrentUser> user, + PermissionBackend permissionBackend) { this.queryProvider = queryProvider; this.deleteRefFactory = deleteRefFactory; + this.user = user; + this.permissionBackend = permissionBackend; } @Override public Response<?> apply(BranchResource rsrc, Input input) - throws RestApiException, OrmException, IOException { - if (!rsrc.getControl().controlForRef(rsrc.getBranchKey()).canDelete()) { - throw new AuthException("Cannot delete branch"); - } + throws RestApiException, OrmException, IOException, PermissionBackendException { + permissionBackend.user(user).ref(rsrc.getBranchKey()).check(RefPermission.DELETE); if (!queryProvider.get().setLimit(1).byBranchOpen(rsrc.getBranchKey()).isEmpty()) { throw new ResourceConflictException("branch " + rsrc.getBranchKey() + " has open changes");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java index 4c54423..fa7e917 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteBranches.java
@@ -21,6 +21,7 @@ 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.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -37,12 +38,10 @@ @Override public Response<?> apply(ProjectResource project, DeleteBranchesInput input) - throws OrmException, IOException, RestApiException { - + throws OrmException, IOException, RestApiException, PermissionBackendException { if (input == null || input.branches == null || input.branches.isEmpty()) { throw new BadRequestException("branches must be specified"); } - deleteRefFactory.create(project).refs(input.branches).prefix(R_HEADS).delete(); return Response.none(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java index 1fadef6..9b6538c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java
@@ -19,16 +19,20 @@ import static org.eclipse.jgit.lib.Constants.R_TAGS; import static org.eclipse.jgit.transport.ReceiveCommand.Type.DELETE; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; import com.google.gerrit.server.git.GitRepositoryManager; +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.query.change.InternalChangeQuery; import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; import com.google.inject.Provider; 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 +56,7 @@ private static final long SLEEP_ON_LOCK_FAILURE_MS = 15; private final Provider<IdentifiedUser> identifiedUser; + private final PermissionBackend permissionBackend; private final GitRepositoryManager repoManager; private final GitReferenceUpdated referenceUpdated; private final RefValidationHelper refDeletionValidator; @@ -64,15 +69,17 @@ DeleteRef create(ProjectResource r); } - @AssistedInject + @Inject DeleteRef( Provider<IdentifiedUser> identifiedUser, + PermissionBackend permissionBackend, GitRepositoryManager repoManager, GitReferenceUpdated referenceUpdated, RefValidationHelper.Factory refDeletionValidatorFactory, Provider<InternalChangeQuery> queryProvider, @Assisted ProjectResource resource) { this.identifiedUser = identifiedUser; + this.permissionBackend = permissionBackend; this.repoManager = repoManager; this.referenceUpdated = referenceUpdated; this.refDeletionValidator = refDeletionValidatorFactory.create(DELETE); @@ -96,7 +103,8 @@ return this; } - public void delete() throws OrmException, IOException, ResourceConflictException { + public void delete() + throws OrmException, IOException, ResourceConflictException, PermissionBackendException { if (!refsToDelete.isEmpty()) { try (Repository r = repoManager.openRepository(resource.getNameKey())) { if (refsToDelete.size() == 1) { @@ -168,8 +176,9 @@ } private void deleteMultipleRefs(Repository r) - throws OrmException, IOException, ResourceConflictException { + throws OrmException, IOException, ResourceConflictException, PermissionBackendException { BatchRefUpdate batchUpdate = r.getRefDatabase().newBatchUpdate(); + batchUpdate.setAtomic(false); List<String> refs = prefix == null ? refsToDelete @@ -197,7 +206,7 @@ } private ReceiveCommand createDeleteCommand(ProjectResource project, Repository r, String refName) - throws OrmException, IOException, ResourceConflictException { + throws OrmException, IOException, ResourceConflictException, PermissionBackendException { Ref ref = r.getRefDatabase().getRef(refName); ReceiveCommand command; if (ref == null) { @@ -209,7 +218,13 @@ } command = new ReceiveCommand(ref.getObjectId(), ObjectId.zeroId(), ref.getName()); - if (!project.getControl().controlForRef(refName).canDelete()) { + try { + permissionBackend + .user(identifiedUser) + .project(project.getNameKey()) + .ref(refName) + .check(RefPermission.DELETE); + } catch (AuthException denied) { command.setResult( Result.REJECTED_OTHER_REASON, "it doesn't exist or you do not have permission to delete it");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTag.java index f26d40f..a05fa2e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTag.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTag.java
@@ -14,36 +14,46 @@ package com.google.gerrit.server.project; -import com.google.gerrit.extensions.restapi.AuthException; 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.CurrentUser; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.RefPermission; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; +import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; @Singleton public class DeleteTag implements RestModifyView<TagResource, DeleteTag.Input> { - private final DeleteRef.Factory deleteRefFactory; - public static class Input {} + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; + private final DeleteRef.Factory deleteRefFactory; + @Inject - DeleteTag(DeleteRef.Factory deleteRefFactory) { + DeleteTag( + PermissionBackend permissionBackend, + Provider<CurrentUser> user, + DeleteRef.Factory deleteRefFactory) { + this.permissionBackend = permissionBackend; + this.user = user; this.deleteRefFactory = deleteRefFactory; } @Override public Response<?> apply(TagResource resource, Input input) - throws OrmException, RestApiException, IOException { + throws OrmException, RestApiException, IOException, PermissionBackendException { String tag = RefUtil.normalizeTagRef(resource.getTagInfo().ref); - RefControl refControl = resource.getControl().controlForRef(tag); - - if (!refControl.canDelete()) { - throw new AuthException("Cannot delete tag"); - } - + permissionBackend + .user(user) + .project(resource.getNameKey()) + .ref(tag) + .check(RefPermission.DELETE); deleteRefFactory.create(resource).ref(tag).delete(); return Response.none(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTags.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTags.java index 75cf03f..c020351 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTags.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteTags.java
@@ -21,6 +21,7 @@ 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.permissions.PermissionBackendException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -37,12 +38,10 @@ @Override public Response<?> apply(ProjectResource project, DeleteTagsInput input) - throws OrmException, RestApiException, IOException { - + throws OrmException, RestApiException, IOException, PermissionBackendException { if (input == null || input.tags == null || input.tags.isEmpty()) { throw new BadRequestException("tags must be specified"); } - deleteRefFactory.create(project).refs(input.tags).prefix(R_TAGS).delete(); return Response.none(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java index 654ce69..f81a0f3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
@@ -76,7 +76,7 @@ return applySync(project, input); } - private Response.Accepted applyAsync(final Project.NameKey project, final Input input) { + private Response.Accepted applyAsync(Project.NameKey project, Input input) { Runnable job = new Runnable() { @Override @@ -103,7 +103,7 @@ } @SuppressWarnings("resource") - private BinaryResult applySync(final Project.NameKey project, final Input input) { + private BinaryResult applySync(Project.NameKey project, Input input) { return new BinaryResult() { @Override public void writeTo(OutputStream out) throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java index b464f68..7c0795e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetAccess.java
@@ -14,6 +14,11 @@ package com.google.gerrit.server.project; +import static com.google.gerrit.server.permissions.GlobalPermission.ADMINISTRATE_SERVER; +import static com.google.gerrit.server.permissions.ProjectPermission.CREATE_REF; +import static com.google.gerrit.server.permissions.RefPermission.CREATE_CHANGE; +import static com.google.gerrit.server.permissions.RefPermission.READ; + import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.Iterables; import com.google.gerrit.common.data.AccessSection; @@ -25,6 +30,7 @@ import com.google.gerrit.extensions.api.access.PermissionInfo; import com.google.gerrit.extensions.api.access.PermissionRuleInfo; import com.google.gerrit.extensions.api.access.ProjectAccessInfo; +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.RestReadView; @@ -37,6 +43,9 @@ import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.RefPermission; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -63,7 +72,8 @@ PermissionRule.Action.INTERACTIVE, PermissionRuleInfo.Action.INTERACTIVE); - private final Provider<CurrentUser> self; + private final Provider<CurrentUser> user; + private final PermissionBackend permissionBackend; private final GroupControl.Factory groupControlFactory; private final AllProjectsName allProjectsName; private final ProjectJson projectJson; @@ -75,6 +85,7 @@ @Inject public GetAccess( Provider<CurrentUser> self, + PermissionBackend permissionBackend, GroupControl.Factory groupControlFactory, AllProjectsName allProjectsName, ProjectCache projectCache, @@ -82,7 +93,8 @@ ProjectJson projectJson, ProjectControl.GenericFactory projectControlFactory, GroupBackend groupBackend) { - this.self = self; + this.user = self; + this.permissionBackend = permissionBackend; this.groupControlFactory = groupControlFactory; this.allProjectsName = allProjectsName; this.projectJson = projectJson; @@ -93,9 +105,10 @@ } public ProjectAccessInfo apply(Project.NameKey nameKey) - throws ResourceNotFoundException, ResourceConflictException, IOException { + throws ResourceNotFoundException, ResourceConflictException, IOException, + PermissionBackendException { try { - return this.apply(new ProjectResource(projectControlFactory.controlFor(nameKey, self.get()))); + return apply(new ProjectResource(projectControlFactory.controlFor(nameKey, user.get()))); } catch (NoSuchProjectException e) { throw new ResourceNotFoundException(nameKey.get()); } @@ -103,16 +116,18 @@ @Override public ProjectAccessInfo apply(ProjectResource rsrc) - throws ResourceNotFoundException, ResourceConflictException, IOException { + throws ResourceNotFoundException, ResourceConflictException, IOException, + PermissionBackendException { // Load the current configuration from the repository, ensuring it's the most // recent version available. If it differs from what was in the project // state, force a cache flush now. - // + Project.NameKey projectName = rsrc.getNameKey(); ProjectAccessInfo info = new ProjectAccessInfo(); + ProjectControl pc = createProjectControl(projectName); + PermissionBackend.ForProject perm = permissionBackend.user(user).project(projectName); + ProjectConfig config; - ProjectControl pc = open(projectName); - RefControl metaConfigControl = pc.controlForRef(RefNames.REFS_CONFIG); try (MetaDataUpdate md = metaDataUpdateFactory.create(projectName)) { config = ProjectConfig.read(md); @@ -120,11 +135,13 @@ md.setMessage("Update group names\n"); config.commit(md); projectCache.evict(config.getProject()); - pc = open(projectName); + pc = createProjectControl(projectName); + perm = permissionBackend.user(user).project(projectName); } else if (config.getRevision() != null && !config.getRevision().equals(pc.getProjectState().getConfig().getRevision())) { projectCache.evict(config.getProject()); - pc = open(projectName); + pc = createProjectControl(projectName); + perm = permissionBackend.user(user).project(projectName); } } catch (ConfigInvalidException e) { throw new ResourceConflictException(e.getMessage()); @@ -135,6 +152,7 @@ info.local = new HashMap<>(); info.ownerOf = new HashSet<>(); Map<AccountGroup.UUID, Boolean> visibleGroups = new HashMap<>(); + boolean checkReadConfig = check(perm, RefNames.REFS_CONFIG, READ); for (AccessSection section : config.getAccessSections()) { String name = section.getName(); @@ -143,20 +161,19 @@ info.local.put(name, createAccessSection(section)); info.ownerOf.add(name); - } else if (metaConfigControl.isVisible()) { + } else if (checkReadConfig) { info.local.put(section.getName(), createAccessSection(section)); } } else if (RefConfigSection.isValid(name)) { - RefControl rc = pc.controlForRef(name); - if (rc.isOwner()) { + if (pc.controlForRef(name).isOwner()) { info.local.put(name, createAccessSection(section)); info.ownerOf.add(name); - } else if (metaConfigControl.isVisible()) { + } else if (checkReadConfig) { info.local.put(name, createAccessSection(section)); - } else if (rc.isVisible()) { + } else if (check(perm, name, READ)) { // Filter the section to only add rules describing groups that // are visible to the current-user. This includes any group the // user is a member of, as well as groups they own or that @@ -214,21 +231,32 @@ info.inheritsFrom = projectJson.format(parent.getProject()); } - if (pc.getProject().getNameKey().equals(allProjectsName)) { - if (pc.isOwner()) { - info.ownerOf.add(AccessSection.GLOBAL_CAPABILITIES); - } + if (projectName.equals(allProjectsName) + && permissionBackend.user(user).testOrFalse(ADMINISTRATE_SERVER)) { + info.ownerOf.add(AccessSection.GLOBAL_CAPABILITIES); } info.isOwner = toBoolean(pc.isOwner()); info.canUpload = - toBoolean(pc.isOwner() || (metaConfigControl.isVisible() && metaConfigControl.canUpload())); - info.canAdd = toBoolean(pc.canAddRefs()); - info.configVisible = pc.isOwner() || metaConfigControl.isVisible(); + toBoolean( + pc.isOwner() + || (checkReadConfig && perm.ref(RefNames.REFS_CONFIG).testOrFalse(CREATE_CHANGE))); + info.canAdd = toBoolean(perm.testOrFalse(CREATE_REF)); + info.configVisible = checkReadConfig || pc.isOwner(); return info; } + private static boolean check(PermissionBackend.ForProject ctx, String ref, RefPermission perm) + throws PermissionBackendException { + try { + ctx.ref(ref).check(perm); + return true; + } catch (AuthException denied) { + return false; + } + } + private AccessSectionInfo createAccessSection(AccessSection section) { AccessSectionInfo accessSectionInfo = new AccessSectionInfo(); accessSectionInfo.permissions = new HashMap<>(); @@ -252,11 +280,10 @@ return accessSectionInfo; } - private ProjectControl open(Project.NameKey projectName) - throws ResourceNotFoundException, IOException { + private ProjectControl createProjectControl(Project.NameKey projectName) + throws IOException, ResourceNotFoundException { try { - return projectControlFactory.validateFor( - projectName, ProjectControl.OWNER | ProjectControl.VISIBLE, self.get()); + return projectControlFactory.controlFor(projectName, user.get()); } catch (NoSuchProjectException e) { throw new ResourceNotFoundException(projectName.get()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java index 8192e29..b1ba281 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetConfig.java
@@ -22,6 +22,7 @@ import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.config.PluginConfigFactory; import com.google.gerrit.server.config.ProjectConfigEntry; +import com.google.gerrit.server.extensions.webui.UiActions; import com.google.gerrit.server.git.TransferConfig; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -33,6 +34,7 @@ private final DynamicMap<ProjectConfigEntry> pluginConfigEntries; private final PluginConfigFactory cfgFactory; private final AllProjectsName allProjects; + private final UiActions uiActions; private final DynamicMap<RestView<ProjectResource>> views; @Inject @@ -42,12 +44,14 @@ DynamicMap<ProjectConfigEntry> pluginConfigEntries, PluginConfigFactory cfgFactory, AllProjectsName allProjects, + UiActions uiActions, DynamicMap<RestView<ProjectResource>> views) { this.serverEnableSignedPush = serverEnableSignedPush; this.config = config; this.pluginConfigEntries = pluginConfigEntries; this.allProjects = allProjects; this.cfgFactory = cfgFactory; + this.uiActions = uiActions; this.views = views; } @@ -60,6 +64,7 @@ pluginConfigEntries, cfgFactory, allProjects, + uiActions, views); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java index 10da990f..387c966 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetContent.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.project; +import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestReadView; @@ -32,8 +33,9 @@ } @Override - public BinaryResult apply(FileResource rsrc) throws ResourceNotFoundException, IOException { + public BinaryResult apply(FileResource rsrc) + throws ResourceNotFoundException, BadRequestException, IOException { return fileContentUtil.getContent( - rsrc.getProject().getProjectState(), rsrc.getRev(), rsrc.getPath()); + rsrc.getProject().getProjectState(), rsrc.getRev(), rsrc.getPath(), null); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java index 5d0afea..44d6a4f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
@@ -15,9 +15,11 @@ package com.google.gerrit.server.project; import com.google.common.collect.Lists; -import com.google.gerrit.extensions.common.GitPerson; +import com.google.gerrit.extensions.api.projects.ReflogEntryInfo; import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.server.CommonConverters; import com.google.gerrit.server.args4j.TimestampHandler; @@ -27,13 +29,16 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; -import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.ReflogEntry; import org.eclipse.jgit.lib.ReflogReader; import org.eclipse.jgit.lib.Repository; import org.kohsuke.args4j.Option; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class GetReflog implements RestReadView<BranchResource> { + private static final Logger log = LoggerFactory.getLogger(GetReflog.class); + private final GitRepositoryManager repoManager; @Option( @@ -83,14 +88,20 @@ } @Override - public List<ReflogEntryInfo> apply(BranchResource rsrc) - throws AuthException, ResourceNotFoundException, RepositoryNotFoundException, IOException { + public List<ReflogEntryInfo> apply(BranchResource rsrc) throws RestApiException, IOException { if (!rsrc.getControl().isOwner()) { throw new AuthException("not project owner"); } try (Repository repo = repoManager.openRepository(rsrc.getNameKey())) { - ReflogReader r = repo.getReflogReader(rsrc.getRef()); + ReflogReader r; + try { + r = repo.getReflogReader(rsrc.getRef()); + } catch (UnsupportedOperationException e) { + String msg = "reflog not supported on repo " + rsrc.getNameKey().get(); + log.error(msg); + throw new MethodNotAllowedException(msg); + } if (r == null) { throw new ResourceNotFoundException(rsrc.getRef()); } @@ -109,21 +120,15 @@ } } } - return Lists.transform(entries, ReflogEntryInfo::new); + return Lists.transform(entries, e -> newReflogEntryInfo(e)); } } - public static class ReflogEntryInfo { - public String oldId; - public String newId; - public GitPerson who; - public String comment; - - public ReflogEntryInfo(ReflogEntry e) { - oldId = e.getOldId().getName(); - newId = e.getNewId().getName(); - who = CommonConverters.toGitPerson(e.getWho()); - comment = e.getComment(); - } + private ReflogEntryInfo newReflogEntryInfo(ReflogEntry e) { + return new ReflogEntryInfo( + e.getOldId().getName(), + e.getNewId().getName(), + CommonConverters.toGitPerson(e.getWho()), + e.getComment()); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java index a5b6458..ecb3ea6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -26,11 +26,14 @@ import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.extensions.webui.UiAction; import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.WebLinks; import com.google.gerrit.server.extensions.webui.UiActions; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.RefPermission; import com.google.inject.Inject; -import com.google.inject.util.Providers; +import com.google.inject.Provider; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; @@ -47,7 +50,10 @@ public class ListBranches implements RestReadView<ProjectResource> { private final GitRepositoryManager repoManager; + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; private final DynamicMap<RestView<BranchResource>> branchViews; + private final UiActions uiActions; private final WebLinks webLinks; @Option( @@ -98,10 +104,16 @@ @Inject public ListBranches( GitRepositoryManager repoManager, + PermissionBackend permissionBackend, + Provider<CurrentUser> user, DynamicMap<RestView<BranchResource>> branchViews, + UiActions uiActions, WebLinks webLinks) { this.repoManager = repoManager; + this.permissionBackend = permissionBackend; + this.user = user; this.branchViews = branchViews; + this.uiActions = uiActions; this.webLinks = webLinks; } @@ -138,6 +150,8 @@ } } + ProjectControl pctl = rsrc.getControl(); + PermissionBackend.ForProject perm = permissionBackend.user(user).project(rsrc.getNameKey()); List<BranchInfo> branches = new ArrayList<>(refs.size()); for (Ref ref : refs) { if (ref.isSymbolic()) { @@ -145,7 +159,7 @@ // showing the resolved value, show the name it references. // String target = ref.getTarget().getName(); - RefControl targetRefControl = rsrc.getControl().controlForRef(target); + RefControl targetRefControl = pctl.controlForRef(target); if (!targetRefControl.isVisible()) { continue; } @@ -159,14 +173,13 @@ branches.add(b); if (!Constants.HEAD.equals(ref.getName())) { - b.canDelete = targetRefControl.canDelete() ? true : null; + b.canDelete = perm.ref(ref.getName()).testOrFalse(RefPermission.DELETE) ? true : null; } continue; } - RefControl refControl = rsrc.getControl().controlForRef(ref.getName()); - if (refControl.isVisible()) { - branches.add(createBranchInfo(ref, refControl, targets)); + if (pctl.controlForRef(ref.getName()).isVisible()) { + branches.add(createBranchInfo(perm.ref(ref.getName()), ref, pctl, targets)); } } Collections.sort(branches, new BranchComparator()); @@ -192,24 +205,23 @@ } } - private BranchInfo createBranchInfo(Ref ref, RefControl refControl, Set<String> targets) { + private BranchInfo createBranchInfo( + PermissionBackend.ForRef perm, Ref ref, ProjectControl pctl, Set<String> targets) { BranchInfo info = new BranchInfo(); info.ref = ref.getName(); info.revision = ref.getObjectId() != null ? ref.getObjectId().name() : null; - info.canDelete = !targets.contains(ref.getName()) && refControl.canDelete() ? true : null; - for (UiAction.Description d : - UiActions.from( - branchViews, - new BranchResource(refControl.getProjectControl(), info), - Providers.of(refControl.getUser()))) { + info.canDelete = + !targets.contains(ref.getName()) && perm.testOrFalse(RefPermission.DELETE) ? true : null; + + BranchResource rsrc = new BranchResource(pctl, info); + for (UiAction.Description d : uiActions.from(branchViews, rsrc)) { if (info.actions == null) { info.actions = new TreeMap<>(); } info.actions.put(d.getId(), new ActionInfo(d)); } - List<WebLinkInfo> links = - webLinks.getBranchLinks( - refControl.getProjectControl().getProject().getName(), ref.getName()); + + List<WebLinkInfo> links = webLinks.getBranchLinks(pctl.getProject().getName(), ref.getName()); info.webLinks = links.isEmpty() ? null : links; return info; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java index c14ade6..23a4417 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListChildProjects.java
@@ -14,14 +14,21 @@ package com.google.gerrit.server.project; +import static java.util.stream.Collectors.toList; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; import com.google.gerrit.extensions.common.ProjectInfo; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.config.AllProjectsName; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; import com.google.inject.Inject; +import com.google.inject.Provider; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,20 +40,23 @@ private boolean recursive; private final ProjectCache projectCache; + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; private final AllProjectsName allProjects; private final ProjectJson json; - private final ProjectNode.Factory projectNodeFactory; @Inject ListChildProjects( ProjectCache projectCache, + PermissionBackend permissionBackend, + Provider<CurrentUser> user, AllProjectsName allProjectsName, - ProjectJson json, - ProjectNode.Factory projectNodeFactory) { + ProjectJson json) { this.projectCache = projectCache; + this.permissionBackend = permissionBackend; + this.user = user; this.allProjects = allProjectsName; this.json = json; - this.projectNodeFactory = projectNodeFactory; } public void setRecursive(boolean recursive) { @@ -54,60 +64,82 @@ } @Override - public List<ProjectInfo> apply(ProjectResource rsrc) { + public List<ProjectInfo> apply(ProjectResource rsrc) throws PermissionBackendException { if (recursive) { - return getChildProjectsRecursively(rsrc.getNameKey(), rsrc.getControl().getUser()); + return recursiveChildProjects(rsrc.getNameKey()); } - return getDirectChildProjects(rsrc.getNameKey()); + return directChildProjects(rsrc.getNameKey()); } - private List<ProjectInfo> getDirectChildProjects(Project.NameKey parent) { - List<ProjectInfo> childProjects = new ArrayList<>(); - for (Project.NameKey projectName : projectCache.all()) { - ProjectState e = projectCache.get(projectName); - if (e == null) { - // If we can't get it from the cache, pretend it's not present. - continue; - } - if (parent.equals(e.getProject().getParent(allProjects))) { - childProjects.add(json.format(e.getProject())); - } - } - return childProjects; - } - - private List<ProjectInfo> getChildProjectsRecursively(Project.NameKey parent, CurrentUser user) { - Map<Project.NameKey, ProjectNode> projects = new HashMap<>(); + private List<ProjectInfo> directChildProjects(Project.NameKey parent) + throws PermissionBackendException { + Map<Project.NameKey, Project> children = new HashMap<>(); for (Project.NameKey name : projectCache.all()) { - ProjectState p = projectCache.get(name); - if (p == null) { - // If we can't get it from the cache, pretend it's not present. - continue; - } - projects.put(name, projectNodeFactory.create(p.getProject(), p.controlFor(user).isVisible())); - } - for (ProjectNode key : projects.values()) { - ProjectNode node = projects.get(key.getParentName()); - if (node != null) { - node.addChild(key); + ProjectState c = projectCache.get(name); + if (c != null && parent.equals(c.getProject().getParent(allProjects))) { + children.put(c.getProject().getNameKey(), c.getProject()); } } - - ProjectNode n = projects.get(parent); - if (n != null) { - return getChildProjectsRecursively(n); - } - return Collections.emptyList(); + return permissionBackend + .user(user) + .filter(ProjectPermission.ACCESS, children.keySet()) + .stream() + .sorted() + .map((p) -> json.format(children.get(p))) + .collect(toList()); } - private List<ProjectInfo> getChildProjectsRecursively(ProjectNode p) { - List<ProjectInfo> allChildren = new ArrayList<>(); - for (ProjectNode c : p.getChildren()) { - if (c.isVisible()) { - allChildren.add(json.format(c.getProject())); - allChildren.addAll(getChildProjectsRecursively(c)); + private List<ProjectInfo> recursiveChildProjects(Project.NameKey parent) + throws PermissionBackendException { + Map<Project.NameKey, Project> projects = readAllProjects(); + Multimap<Project.NameKey, Project.NameKey> children = parentToChildren(projects); + PermissionBackend.WithUser perm = permissionBackend.user(user); + + List<ProjectInfo> results = new ArrayList<>(); + depthFirstFormat(results, perm, projects, children, parent); + return results; + } + + private Map<Project.NameKey, Project> readAllProjects() { + Map<Project.NameKey, Project> projects = new HashMap<>(); + for (Project.NameKey name : projectCache.all()) { + ProjectState c = projectCache.get(name); + if (c != null) { + projects.put(c.getProject().getNameKey(), c.getProject()); } } - return allChildren; + return projects; + } + + /** Map of parent project to direct child. */ + private Multimap<Project.NameKey, Project.NameKey> parentToChildren( + Map<Project.NameKey, Project> projects) { + Multimap<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()); + } + } + return m; + } + + private void depthFirstFormat( + List<ProjectInfo> results, + PermissionBackend.WithUser perm, + Map<Project.NameKey, Project> projects, + Multimap<Project.NameKey, Project.NameKey> children, + Project.NameKey parent) + throws PermissionBackendException { + List<Project.NameKey> canSee = + perm.filter(ProjectPermission.ACCESS, children.get(parent)) + .stream() + .sorted() + .collect(toList()); + children.removeAll(parent); // removing all entries prevents cycles. + + for (Project.NameKey c : canSee) { + results.add(json.format(projects.get(c))); + depthFirstFormat(results, perm, projects, children, c); + } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java index 4ea5a8f..d60a4a7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
@@ -19,12 +19,20 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.git.GitRepositoryManager; +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.project.DashboardsCollection.DashboardInfo; import com.google.inject.Inject; +import com.google.inject.Provider; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.BlobBasedConfig; @@ -41,42 +49,56 @@ private static final Logger log = LoggerFactory.getLogger(ListDashboards.class); private final GitRepositoryManager gitManager; + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; @Option(name = "--inherited", usage = "include inherited dashboards") private boolean inherited; @Inject - ListDashboards(GitRepositoryManager gitManager) { + ListDashboards( + GitRepositoryManager gitManager, + PermissionBackend permissionBackend, + Provider<CurrentUser> user) { this.gitManager = gitManager; + this.permissionBackend = permissionBackend; + this.user = user; } @Override - public List<?> apply(ProjectResource resource) throws ResourceNotFoundException, IOException { - ProjectControl ctl = resource.getControl(); - String project = ctl.getProject().getName(); + public List<?> apply(ProjectResource rsrc) + throws ResourceNotFoundException, IOException, PermissionBackendException { + String project = rsrc.getName(); if (!inherited) { - return scan(resource.getControl(), project, true); + return scan(rsrc.getControl(), project, true); } List<List<DashboardInfo>> all = new ArrayList<>(); boolean setDefault = true; - for (ProjectState ps : ctl.getProjectState().tree()) { - ctl = ps.controlFor(ctl.getUser()); - if (ctl.isVisible()) { - List<DashboardInfo> list = scan(ctl, project, setDefault); - for (DashboardInfo d : list) { - if (d.isDefault != null && Boolean.TRUE.equals(d.isDefault)) { - setDefault = false; - } + for (ProjectState ps : tree(rsrc)) { + List<DashboardInfo> list = scan(ps.controlFor(user.get()), project, setDefault); + for (DashboardInfo d : list) { + if (d.isDefault != null && Boolean.TRUE.equals(d.isDefault)) { + setDefault = false; } - if (!list.isEmpty()) { - all.add(list); - } + } + if (!list.isEmpty()) { + all.add(list); } } return all; } + private Collection<ProjectState> tree(ProjectResource rsrc) throws PermissionBackendException { + Map<Project.NameKey, ProjectState> tree = new LinkedHashMap<>(); + for (ProjectState ps : rsrc.getProjectState().tree()) { + tree.put(ps.getProject().getNameKey(), ps); + } + tree.keySet() + .retainAll(permissionBackend.user(user).filter(ProjectPermission.ACCESS, tree.keySet())); + return tree.values(); + } + private List<DashboardInfo> scan(ProjectControl ctl, String project, boolean setDefault) throws ResourceNotFoundException, IOException { Project.NameKey projectName = ctl.getProject().getNameKey();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java index a84fefd..986b4ad 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -14,15 +14,20 @@ package com.google.gerrit.server.project; +import static com.google.gerrit.extensions.client.ProjectState.HIDDEN; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.stream.Collectors.toList; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.common.errors.NoSuchGroupException; import com.google.gerrit.extensions.common.ProjectInfo; import com.google.gerrit.extensions.common.WebLinkInfo; +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.RestReadView; @@ -38,6 +43,9 @@ import com.google.gerrit.server.account.GroupControl; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.group.GroupsCollection; +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.util.RegexListSearcher; import com.google.gerrit.server.util.TreeFormatter; import com.google.gson.reflect.TypeToken; @@ -50,6 +58,8 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -79,12 +89,22 @@ boolean matches(Repository git) throws IOException { return !PERMISSIONS.matches(git); } + + @Override + boolean useMatch() { + return true; + } }, PARENT_CANDIDATES { @Override boolean matches(Repository git) { return true; } + + @Override + boolean useMatch() { + return false; + } }, PERMISSIONS { @Override @@ -94,15 +114,27 @@ && head.isSymbolic() && RefNames.REFS_CONFIG.equals(head.getLeaf().getName()); } + + @Override + boolean useMatch() { + return true; + } }, ALL { @Override boolean matches(Repository git) { return true; } + + @Override + boolean useMatch() { + return false; + } }; abstract boolean matches(Repository git) throws IOException; + + abstract boolean useMatch(); } private final CurrentUser currentUser; @@ -110,6 +142,7 @@ private final GroupsCollection groupsCollection; private final GroupControl.Factory groupControlFactory; private final GitRepositoryManager repoManager; + private final PermissionBackend permissionBackend; private final ProjectNode.Factory projectNodeFactory; private final WebLinks webLinks; @@ -229,6 +262,7 @@ GroupsCollection groupsCollection, GroupControl.Factory groupControlFactory, GitRepositoryManager repoManager, + PermissionBackend permissionBackend, ProjectNode.Factory projectNodeFactory, WebLinks webLinks) { this.currentUser = currentUser; @@ -236,6 +270,7 @@ this.groupsCollection = groupsCollection; this.groupControlFactory = groupControlFactory; this.repoManager = repoManager; + this.permissionBackend = permissionBackend; this.projectNodeFactory = projectNodeFactory; this.webLinks = webLinks; } @@ -262,7 +297,8 @@ } @Override - public Object apply(TopLevelResource resource) throws BadRequestException { + public Object apply(TopLevelResource resource) + throws BadRequestException, PermissionBackendException { if (format == OutputFormat.TEXT) { ByteArrayOutputStream buf = new ByteArrayOutputStream(); display(buf); @@ -273,141 +309,123 @@ return apply(); } - public SortedMap<String, ProjectInfo> apply() throws BadRequestException { + public SortedMap<String, ProjectInfo> apply() + throws BadRequestException, PermissionBackendException { format = OutputFormat.JSON; return display(null); } - public SortedMap<String, ProjectInfo> display(OutputStream displayOutputStream) - throws BadRequestException { + public SortedMap<String, ProjectInfo> display(@Nullable OutputStream displayOutputStream) + throws BadRequestException, PermissionBackendException { + if (groupUuid != null) { + try { + if (!groupControlFactory.controlFor(groupUuid).isVisible()) { + return Collections.emptySortedMap(); + } + } catch (NoSuchGroupException ex) { + return Collections.emptySortedMap(); + } + } + PrintWriter stdout = null; if (displayOutputStream != null) { stdout = new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8))); } + if (type == FilterType.PARENT_CANDIDATES) { + // Historically, PARENT_CANDIDATES implied showDescription. + showDescription = true; + } + int foundIndex = 0; int found = 0; TreeMap<String, ProjectInfo> output = new TreeMap<>(); Map<String, String> hiddenNames = new HashMap<>(); - Set<String> rejected = new HashSet<>(); - + Map<Project.NameKey, Boolean> accessibleParents = new HashMap<>(); + PermissionBackend.WithUser perm = permissionBackend.user(currentUser); final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>(); try { - for (final Project.NameKey projectName : scan()) { + for (Project.NameKey projectName : filter(perm)) { final ProjectState e = projectCache.get(projectName); - if (e == null) { + if (e == null || (!all && e.getProject().getState() == HIDDEN)) { // If we can't get it from the cache, pretend its not present. - // + // If all wasn't selected, and its HIDDEN, pretend its not present. continue; } final ProjectControl pctl = e.controlFor(currentUser); - if (groupUuid != null) { - try { - if (!groupControlFactory.controlFor(groupUuid).isVisible()) { - break; - } - } catch (NoSuchGroupException ex) { - break; - } - if (!pctl.getLocalGroups() - .contains(GroupReference.forGroup(groupsCollection.parseId(groupUuid.get())))) { - continue; - } + if (groupUuid != null + && !pctl.getLocalGroups() + .contains(GroupReference.forGroup(groupsCollection.parseId(groupUuid.get())))) { + continue; } ProjectInfo info = new ProjectInfo(); - if (type == FilterType.PARENT_CANDIDATES) { - ProjectState parentState = Iterables.getFirst(e.parents(), null); - if (parentState != null - && !output.keySet().contains(parentState.getProject().getName()) - && !rejected.contains(parentState.getProject().getName())) { - ProjectControl parentCtrl = parentState.controlFor(currentUser); - if (parentCtrl.isVisible() || parentCtrl.isOwner()) { - info.name = parentState.getProject().getName(); - info.description = Strings.emptyToNull(parentState.getProject().getDescription()); - info.state = parentState.getProject().getState(); + if (showTree && !format.isJson()) { + treeMap.put(projectName, projectNodeFactory.create(pctl.getProject(), true)); + continue; + } + + info.name = projectName.get(); + if (showTree && format.isJson()) { + ProjectState parent = Iterables.getFirst(e.parents(), null); + if (parent != null) { + if (isParentAccessible(accessibleParents, perm, parent)) { + info.parent = parent.getProject().getName(); } else { - rejected.add(parentState.getProject().getName()); - continue; - } - } else { - continue; - } - - } else { - final boolean isVisible = pctl.isVisible() || (all && pctl.isOwner()); - if (showTree && !format.isJson()) { - treeMap.put(projectName, projectNodeFactory.create(pctl.getProject(), isVisible)); - continue; - } - - if (!isVisible && !(showTree && pctl.isOwner())) { - // Require the project itself to be visible to the user. - // - continue; - } - - info.name = projectName.get(); - if (showTree && format.isJson()) { - ProjectState parent = Iterables.getFirst(e.parents(), null); - if (parent != null) { - ProjectControl parentCtrl = parent.controlFor(currentUser); - if (parentCtrl.isVisible() || parentCtrl.isOwner()) { - info.parent = parent.getProject().getName(); - } else { - info.parent = hiddenNames.get(parent.getProject().getName()); - if (info.parent == null) { - info.parent = "?-" + (hiddenNames.size() + 1); - hiddenNames.put(parent.getProject().getName(), info.parent); - } + info.parent = hiddenNames.get(parent.getProject().getName()); + if (info.parent == null) { + info.parent = "?-" + (hiddenNames.size() + 1); + hiddenNames.put(parent.getProject().getName(), info.parent); } } } - if (showDescription) { - info.description = Strings.emptyToNull(e.getProject().getDescription()); - } + } - info.state = e.getProject().getState(); + if (showDescription) { + info.description = Strings.emptyToNull(e.getProject().getDescription()); + } + info.state = e.getProject().getState(); - try { - if (!showBranch.isEmpty()) { - try (Repository git = repoManager.openRepository(projectName)) { - if (!type.matches(git)) { - continue; - } + try { + if (!showBranch.isEmpty()) { + try (Repository git = repoManager.openRepository(projectName)) { + if (!type.matches(git)) { + continue; + } - List<Ref> refs = getBranchRefs(projectName, pctl); - if (!hasValidRef(refs)) { - continue; - } + List<Ref> refs = getBranchRefs(projectName, pctl); + if (!hasValidRef(refs)) { + continue; + } - for (int i = 0; i < showBranch.size(); i++) { - Ref ref = refs.get(i); - if (ref != null && ref.getObjectId() != null) { - if (info.branches == null) { - info.branches = new LinkedHashMap<>(); - } - info.branches.put(showBranch.get(i), ref.getObjectId().name()); + for (int i = 0; i < showBranch.size(); i++) { + Ref ref = refs.get(i); + if (ref != null && ref.getObjectId() != null) { + if (info.branches == null) { + info.branches = new LinkedHashMap<>(); } - } - } - } else if (!showTree && type != FilterType.ALL) { - try (Repository git = repoManager.openRepository(projectName)) { - if (!type.matches(git)) { - continue; + info.branches.put(showBranch.get(i), ref.getObjectId().name()); } } } - - } catch (RepositoryNotFoundException err) { - // If the Git repository is gone, the project doesn't actually exist anymore. - continue; - } catch (IOException err) { - log.warn("Unexpected error reading " + projectName, err); - continue; + } else if (!showTree && type.useMatch()) { + try (Repository git = repoManager.openRepository(projectName)) { + if (!type.matches(git)) { + continue; + } + } } + } catch (RepositoryNotFoundException err) { + // If the Git repository is gone, the project doesn't actually exist anymore. + continue; + } catch (IOException err) { + log.warn("Unexpected error reading " + projectName, err); + continue; + } + + if (type != FilterType.PARENT_CANDIDATES) { List<WebLinkInfo> links = webLinks.getProjectLinks(projectName.get()); info.webLinks = links.isEmpty() ? null : links; } @@ -415,7 +433,6 @@ if (foundIndex++ < start) { continue; } - if (limit > 0 && ++found > limit) { break; } @@ -467,6 +484,53 @@ } } + private Collection<Project.NameKey> filter(PermissionBackend.WithUser perm) + throws BadRequestException, PermissionBackendException { + Collection<Project.NameKey> matches = Lists.newArrayList(scan()); + if (type == FilterType.PARENT_CANDIDATES) { + matches = parentsOf(matches); + } + return perm.filter(ProjectPermission.ACCESS, matches).stream().sorted().collect(toList()); + } + + private Collection<Project.NameKey> parentsOf(Collection<Project.NameKey> matches) { + Set<Project.NameKey> parents = new HashSet<>(); + for (Project.NameKey p : matches) { + ProjectState ps = projectCache.get(p); + if (ps != null) { + Project.NameKey parent = ps.getProject().getParent(); + if (parent != null) { + if (projectCache.get(parent) != null) { + parents.add(parent); + } else { + log.warn( + String.format( + "parent project %s of project %s not found", + parent.get(), ps.getProject().getName())); + } + } + } + } + return parents; + } + + private boolean isParentAccessible( + Map<Project.NameKey, Boolean> checked, PermissionBackend.WithUser perm, ProjectState p) + throws PermissionBackendException { + Project.NameKey name = p.getProject().getNameKey(); + Boolean b = checked.get(name); + if (b == null) { + try { + perm.project(name).check(ProjectPermission.ACCESS); + b = true; + } catch (AuthException denied) { + b = false; + } + checked.put(name, b); + } + return b; + } + private Iterable<Project.NameKey> scan() throws BadRequestException { if (matchPrefix != null) { checkMatchOptions(matchSubstring == null && matchRegex == null); @@ -503,12 +567,12 @@ } private void printProjectTree( - final PrintWriter stdout, final TreeMap<Project.NameKey, ProjectNode> treeMap) { + final PrintWriter stdout, TreeMap<Project.NameKey, ProjectNode> treeMap) { final SortedSet<ProjectNode> sortedNodes = new TreeSet<>(); // Builds the inheritance tree using a list. // - for (final ProjectNode key : treeMap.values()) { + for (ProjectNode key : treeMap.values()) { if (key.isAllProjects()) { sortedNodes.add(key); continue;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java index 7f1ee60..d5a8573 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
@@ -22,13 +22,13 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CommonConverters; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.SearchingChangeCacheImpl; -import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.VisibleRefFilter; -import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.RefPermission; import com.google.inject.Inject; import com.google.inject.Provider; import java.io.IOException; @@ -50,9 +50,9 @@ public class ListTags implements RestReadView<ProjectResource> { private final GitRepositoryManager repoManager; - private final Provider<ReviewDb> dbProvider; - private final TagCache tagCache; - private final ChangeNotes.Factory changeNotesFactory; + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; + private final VisibleRefFilter.Factory refFilterFactory; @Nullable private final SearchingChangeCacheImpl changeCache; @Option( @@ -103,14 +103,14 @@ @Inject public ListTags( GitRepositoryManager repoManager, - Provider<ReviewDb> dbProvider, - TagCache tagCache, - ChangeNotes.Factory changeNotesFactory, + PermissionBackend permissionBackend, + Provider<CurrentUser> user, + VisibleRefFilter.Factory refFilterFactory, @Nullable SearchingChangeCacheImpl changeCache) { this.repoManager = repoManager; - this.dbProvider = dbProvider; - this.tagCache = tagCache; - this.changeNotesFactory = changeNotesFactory; + this.permissionBackend = permissionBackend; + this.user = user; + this.refFilterFactory = refFilterFactory; this.changeCache = changeCache; } @@ -119,13 +119,14 @@ throws IOException, ResourceNotFoundException, BadRequestException { List<TagInfo> tags = new ArrayList<>(); + PermissionBackend.ForProject perm = permissionBackend.user(user).project(resource.getNameKey()); try (Repository repo = getRepository(resource.getNameKey()); RevWalk rw = new RevWalk(repo)) { - ProjectControl control = resource.getControl(); + ProjectControl pctl = resource.getControl(); Map<String, Ref> all = - visibleTags(control, repo, repo.getRefDatabase().getRefs(Constants.R_TAGS)); + visibleTags(pctl, repo, repo.getRefDatabase().getRefs(Constants.R_TAGS)); for (Ref ref : all.values()) { - tags.add(createTagInfo(ref, rw, control.controlForRef(ref.getName()))); + tags.add(createTagInfo(perm.ref(ref.getName()), ref, rw)); } } @@ -158,15 +159,22 @@ ProjectControl control = resource.getControl(); if (ref != null && !visibleTags(control, repo, ImmutableMap.of(ref.getName(), ref)).isEmpty()) { - return createTagInfo(ref, rw, control.controlForRef(ref.getName())); + return createTagInfo( + permissionBackend + .user(control.getUser()) + .project(resource.getNameKey()) + .ref(ref.getName()), + ref, + rw); } } throw new ResourceNotFoundException(id); } - public static TagInfo createTagInfo(Ref ref, RevWalk rw, RefControl control) + public static TagInfo createTagInfo(PermissionBackend.ForRef perm, Ref ref, RevWalk rw) throws MissingObjectException, IOException { RevObject object = rw.parseAny(ref.getObjectId()); + boolean canDelete = perm.testOrFalse(RefPermission.DELETE); if (object instanceof RevTag) { // Annotated or signed tag RevTag tag = (RevTag) object; @@ -177,10 +185,10 @@ tag.getObject().getName(), tag.getFullMessage().trim(), tagger != null ? CommonConverters.toGitPerson(tag.getTaggerIdent()) : null, - control.canDelete()); + canDelete); } // Lightweight tag - return new TagInfo(ref.getName(), ref.getObjectId().getName(), control.canDelete()); + return new TagInfo(ref.getName(), ref.getObjectId().getName(), canDelete); } private Repository getRepository(Project.NameKey project) @@ -194,8 +202,9 @@ private Map<String, Ref> visibleTags( ProjectControl control, Repository repo, Map<String, Ref> tags) { - return new VisibleRefFilter( - tagCache, changeNotesFactory, changeCache, repo, control, dbProvider.get(), false) + return refFilterFactory + .create(control.getProjectState(), repo) + .setShowMetadata(false) .filter(tags, true); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java index d7af195..11f3805 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -24,6 +24,7 @@ import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.RestApiModule; +import com.google.gerrit.server.change.CherryPickCommit; public class Module extends RestApiModule { @Override @@ -96,6 +97,8 @@ get(PROJECT_KIND, "config").to(GetConfig.class); put(PROJECT_KIND, "config").to(PutConfig.class); + post(COMMIT_KIND, "cherrypick").to(CherryPickCommit.class); + factory(DeleteRef.Factory.class); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchProjectException.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchProjectException.java index 61b5c05..23d8d80 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchProjectException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchProjectException.java
@@ -23,11 +23,11 @@ private static final String MESSAGE = "Project not found: "; private final Project.NameKey project; - public NoSuchProjectException(final Project.NameKey key) { + public NoSuchProjectException(Project.NameKey key) { this(key, null); } - public NoSuchProjectException(final Project.NameKey key, final Throwable why) { + public NoSuchProjectException(Project.NameKey key, Throwable why) { super(MESSAGE + key.toString(), why); project = key; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchRefException.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchRefException.java index 2c90ce86..59debde 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchRefException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchRefException.java
@@ -18,11 +18,11 @@ public class NoSuchRefException extends Exception { private static final long serialVersionUID = 1L; - public NoSuchRefException(final String ref) { + public NoSuchRefException(String ref) { this(ref, null); } - public NoSuchRefException(final String ref, final Throwable why) { + public NoSuchRefException(String ref, Throwable why) { super(ref, why); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java index 4bf3e47..9febb3f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
@@ -98,7 +98,6 @@ perUser = true; if (sm.match(ref, user)) { sectionToProject.put(sm.section, sm.project); - break; } } else if (sm.match(ref, null)) { sectionToProject.put(sm.section, sm.project);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java index 3ca7bd8..6ee143c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -120,7 +120,7 @@ } @Override - public ProjectState get(final Project.NameKey projectName) { + public ProjectState get(Project.NameKey projectName) { try { return checkedGet(projectName); } catch (IOException e) { @@ -151,7 +151,7 @@ } @Override - public void evict(final Project p) { + public void evict(Project p) { if (p != null) { byName.invalidate(p.getNameKey().get()); } @@ -159,14 +159,14 @@ /** Invalidate the cached information about the given project. */ @Override - public void evict(final Project.NameKey p) { + public void evict(Project.NameKey p) { if (p != null) { byName.invalidate(p.get()); } } @Override - public void remove(final Project p) { + public void remove(Project p) { listLock.lock(); try { SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL)); @@ -218,7 +218,7 @@ } @Override - public Iterable<Project.NameKey> byName(final String pfx) { + public Iterable<Project.NameKey> byName(String pfx) { final Iterable<Project.NameKey> src; try { src = list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java index 5e0ba28..66bbcca 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheWarmer.java
@@ -46,7 +46,7 @@ public void start() { int cpus = Runtime.getRuntime().availableProcessors(); if (config.getBoolean("cache", "projects", "loadOnStartup", false)) { - final ThreadPoolExecutor pool = + ThreadPoolExecutor pool = new ScheduledThreadPoolExecutor( config.getInt("cache", "projects", "loadThreads", cpus), new ThreadFactoryBuilder().setNameFormat("ProjectCacheLoader-%d").build()); @@ -54,25 +54,19 @@ log.info("Loading project cache"); scheduler.execute( - new Runnable() { - @Override - public void run() { - for (final Project.NameKey name : cache.all()) { - pool.execute( - new Runnable() { - @Override - public void run() { - cache.get(name); - } - }); - } - pool.shutdown(); - try { - pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); - log.info("Finished loading project cache"); - } catch (InterruptedException e) { - log.warn("Interrupted while waiting for project cache to load"); - } + () -> { + for (Project.NameKey name : cache.all()) { + pool.execute( + () -> { + cache.get(name); + }); + } + pool.shutdown(); + try { + pool.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); + log.info("Finished loading project cache"); + } catch (InterruptedException e) { + log.warn("Interrupted while waiting for project cache to load"); } }); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java index e9976c5..223073f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -14,6 +14,8 @@ package com.google.gerrit.server.project; +import static com.google.common.base.Preconditions.checkArgument; + import com.google.common.collect.Maps; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.PageLinks; @@ -25,6 +27,7 @@ import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.data.PermissionRule; import com.google.gerrit.common.data.PermissionRule.Action; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.metrics.Counter0; import com.google.gerrit.metrics.Description; import com.google.gerrit.metrics.MetricMaker; @@ -40,11 +43,17 @@ import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.GitReceivePackGroups; import com.google.gerrit.server.config.GitUploadPackGroups; -import com.google.gerrit.server.git.SearchingChangeCacheImpl; -import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.permissions.FailedPermissionBackend; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackend.ForChange; +import com.google.gerrit.server.permissions.PermissionBackend.ForProject; +import com.google.gerrit.server.permissions.PermissionBackend.ForRef; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gwtorm.server.OrmException; @@ -56,6 +65,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -70,16 +80,13 @@ /** Access control management for a user accessing a project's data. */ public class ProjectControl { - public static final int VISIBLE = 1 << 0; - public static final int OWNER = 1 << 1; - private static final Logger log = LoggerFactory.getLogger(ProjectControl.class); public static class GenericFactory { private final ProjectCache projectCache; @Inject - GenericFactory(final ProjectCache pc) { + GenericFactory(ProjectCache pc) { projectCache = pc; } @@ -91,18 +98,6 @@ } return p.controlFor(user); } - - public ProjectControl validateFor(Project.NameKey nameKey, int need, CurrentUser user) - throws NoSuchProjectException, IOException { - final ProjectControl c = controlFor(nameKey, user); - if ((need & VISIBLE) == VISIBLE && c.isVisible()) { - return c; - } - if ((need & OWNER) == OWNER && c.isOwner()) { - return c; - } - throw new NoSuchProjectException(nameKey); - } } public static class Factory { @@ -113,29 +108,9 @@ userCache = uc; } - public ProjectControl controlFor(final Project.NameKey nameKey) throws NoSuchProjectException { + public ProjectControl controlFor(Project.NameKey nameKey) throws NoSuchProjectException { return userCache.get().get(nameKey); } - - public ProjectControl validateFor(final Project.NameKey nameKey) throws NoSuchProjectException { - return validateFor(nameKey, VISIBLE); - } - - public ProjectControl ownerFor(final Project.NameKey nameKey) throws NoSuchProjectException { - return validateFor(nameKey, OWNER); - } - - public ProjectControl validateFor(final Project.NameKey nameKey, final int need) - throws NoSuchProjectException { - final ProjectControl c = controlFor(nameKey); - if ((need & VISIBLE) == VISIBLE && c.isVisible()) { - return c; - } - if ((need & OWNER) == OWNER && c.isOwner()) { - return c; - } - throw new NoSuchProjectException(nameKey); - } } public interface AssistedFactory { @@ -159,14 +134,13 @@ private final Set<AccountGroup.UUID> receiveGroups; private final String canonicalWebUrl; + private final PermissionBackend.WithUser perm; private final CurrentUser user; private final ProjectState state; - private final ChangeNotes.Factory changeNotesFactory; private final ChangeControl.Factory changeControlFactory; private final PermissionCollection.Factory permissionFilter; + private final VisibleRefFilter.Factory refFilter; private final Collection<ContributorAgreement> contributorAgreements; - private final TagCache tagCache; - @Nullable private final SearchingChangeCacheImpl changeCache; private final Provider<InternalChangeQuery> queryProvider; private final Metrics metrics; @@ -182,19 +156,16 @@ @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups, ProjectCache pc, PermissionCollection.Factory permissionFilter, - ChangeNotes.Factory changeNotesFactory, ChangeControl.Factory changeControlFactory, - TagCache tagCache, + VisibleRefFilter.Factory refFilter, Provider<InternalChangeQuery> queryProvider, - @Nullable SearchingChangeCacheImpl changeCache, @CanonicalWebUrl @Nullable String canonicalWebUrl, + PermissionBackend permissionBackend, @Assisted CurrentUser who, @Assisted ProjectState ps, Metrics metrics) { - this.changeNotesFactory = changeNotesFactory; this.changeControlFactory = changeControlFactory; - this.tagCache = tagCache; - this.changeCache = changeCache; + this.refFilter = refFilter; this.uploadGroups = uploadGroups; this.receiveGroups = receiveGroups; this.permissionFilter = permissionFilter; @@ -202,6 +173,7 @@ this.canonicalWebUrl = canonicalWebUrl; this.queryProvider = queryProvider; this.metrics = metrics; + this.perm = permissionBackend.user(who); user = who; state = ps; } @@ -270,30 +242,15 @@ } /** Returns whether the project is hidden. */ - public boolean isHidden() { + private boolean isHidden() { return getProject().getState().equals(com.google.gerrit.extensions.client.ProjectState.HIDDEN); } - /** - * Returns whether the project is readable to the current user. Note that the project could still - * be hidden. - */ - public boolean isReadable() { - return (user.isInternalUser() || canPerformOnAnyRef(Permission.READ)); - } - - /** - * Returns whether the project is accessible to the current user, i.e. readable and not hidden. - */ - public boolean isVisible() { - return isReadable() && !isHidden(); - } - - public boolean canAddRefs() { + private boolean canAddRefs() { return (canPerformOnAnyRef(Permission.CREATE) || isOwnerAnyRef()); } - public boolean canUpload() { + private boolean canCreateChanges() { for (SectionMatcher matcher : access()) { AccessSection section = matcher.section; if (section.getName().startsWith("refs/for/")) { @@ -306,19 +263,22 @@ return false; } - /** Can this user see all the refs in this projects? */ - public boolean allRefsAreVisible() { - return allRefsAreVisible(Collections.<String>emptySet()); - } - public boolean allRefsAreVisible(Set<String> ignore) { return user.isInternalUser() || canPerformOnAllRefs(Permission.READ, ignore); } - /** Is this user a project owner? Ownership does not imply {@link #isVisible()} */ + /** Is this user a project owner? */ public boolean isOwner() { - return (isDeclaredOwner() && !controlForRef("refs/*").isBlocked(Permission.OWNER)) - || user.getCapabilities().canAdministrateServer(); + return (isDeclaredOwner() && !controlForRef("refs/*").isBlocked(Permission.OWNER)) || isAdmin(); + } + + boolean isAdmin() { + try { + perm.check(GlobalPermission.ADMINISTRATE_SERVER); + return true; + } catch (AuthException | PermissionBackendException e) { + return false; + } } private boolean isDeclaredOwner() { @@ -331,7 +291,7 @@ /** Does this user have ownership on at least one reference name? */ public boolean isOwnerAnyRef() { - return canPerformOnAnyRef(Permission.OWNER) || user.getCapabilities().canAdministrateServer(); + return canPerformOnAnyRef(Permission.OWNER) || isAdmin(); } /** @return true if the user can upload to at least one reference */ @@ -354,12 +314,12 @@ return getGroups(localAccess()); } - private static Set<GroupReference> getGroups(final List<SectionMatcher> sectionMatcherList) { + private static Set<GroupReference> getGroups(List<SectionMatcher> sectionMatcherList) { final Set<GroupReference> all = new HashSet<>(); - for (final SectionMatcher matcher : sectionMatcherList) { + for (SectionMatcher matcher : sectionMatcherList) { final AccessSection section = matcher.section; - for (final Permission permission : section.getPermissions()) { - for (final PermissionRule rule : permission.getRules()) { + for (Permission permission : section.getPermissions()) { + for (PermissionRule rule : permission.getRules()) { all.add(rule.getGroup()); } } @@ -442,7 +402,7 @@ // matches every possible reference. Check all // patterns also have the permission. // - for (final String pattern : patterns) { + for (String pattern : patterns) { if (controlForRef(pattern).canPerform(permission)) { canPerform = true; } else if (ignore.contains(pattern)) { @@ -537,12 +497,12 @@ "Cannot look up change for commit " + commit.name() + " in " + getProject().getName(), e); } // Scan all visible refs. - return canReadCommitFromVisibleRef(db, repo, commit); + return canReadCommitFromVisibleRef(repo, commit); } - private boolean canReadCommitFromVisibleRef(ReviewDb db, Repository repo, RevCommit commit) { + private boolean canReadCommitFromVisibleRef(Repository repo, RevCommit commit) { try (RevWalk rw = new RevWalk(repo)) { - return isMergedIntoVisibleRef(repo, db, rw, commit, repo.getAllRefs().values()); + return isMergedIntoVisibleRef(repo, rw, commit, repo.getAllRefs().values()); } catch (IOException e) { String msg = String.format( @@ -554,10 +514,9 @@ } boolean isMergedIntoVisibleRef( - Repository repo, ReviewDb db, RevWalk rw, RevCommit commit, Collection<Ref> unfilteredRefs) + Repository repo, RevWalk rw, RevCommit commit, Collection<Ref> unfilteredRefs) throws IOException { - VisibleRefFilter filter = - new VisibleRefFilter(tagCache, changeNotesFactory, changeCache, repo, this, db, true); + VisibleRefFilter filter = refFilter.create(state, repo); Map<String, Ref> m = Maps.newHashMapWithExpectedSize(unfilteredRefs.size()); for (Ref r : unfilteredRefs) { m.put(r.getName(), r); @@ -565,4 +524,81 @@ Map<String, Ref> refs = filter.filter(m, true); return !refs.isEmpty() && IncludedInResolver.includedInOne(repo, rw, commit, refs.values()); } + + ForProject asForProject() { + return new ForProjectImpl(); + } + + private class ForProjectImpl extends ForProject { + @Override + public ForProject user(CurrentUser user) { + return forUser(user).asForProject().database(db); + } + + @Override + public ForRef ref(String ref) { + return controlForRef(ref).asForRef().database(db); + } + + @Override + public ForChange change(ChangeData cd) { + try { + checkProject(cd.change()); + return super.change(cd); + } catch (OrmException e) { + return FailedPermissionBackend.change("unavailable", e); + } + } + + @Override + public ForChange change(ChangeNotes notes) { + checkProject(notes.getChange()); + return super.change(notes); + } + + private void checkProject(Change change) { + Project.NameKey project = getProject().getNameKey(); + checkArgument( + project.equals(change.getProject()), + "expected change in project %s, not %s", + project, + change.getProject()); + } + + @Override + public void check(ProjectPermission perm) throws AuthException, PermissionBackendException { + if (!can(perm)) { + throw new AuthException(perm.describeForException() + " not permitted"); + } + } + + @Override + public Set<ProjectPermission> test(Collection<ProjectPermission> permSet) + throws PermissionBackendException { + EnumSet<ProjectPermission> ok = EnumSet.noneOf(ProjectPermission.class); + for (ProjectPermission perm : permSet) { + if (can(perm)) { + ok.add(perm); + } + } + return ok; + } + + private boolean can(ProjectPermission perm) throws PermissionBackendException { + switch (perm) { + case ACCESS: + return (!isHidden() && (user.isInternalUser() || canPerformOnAnyRef(Permission.READ))) + || isOwner(); + + case READ: + return !isHidden() && allRefsAreVisible(Collections.emptySet()); + + case CREATE_REF: + return canAddRefs(); + case CREATE_CHANGE: + return canCreateChanges(); + } + throw new PermissionBackendException(perm + " unsupported"); + } + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java index 403efd2..e1ba692 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
@@ -76,12 +76,12 @@ return children; } - public void addChild(final ProjectNode child) { + public void addChild(ProjectNode child) { children.add(child); } @Override - public int compareTo(final ProjectNode o) { + public int compareTo(ProjectNode o) { return project.getNameKey().compareTo(o.project.getNameKey()); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java index b8830a0..2601a4a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
@@ -14,7 +14,6 @@ package com.google.gerrit.server.project; -import com.google.gerrit.extensions.client.ProjectState; import com.google.gerrit.extensions.restapi.RestResource; import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.reviewdb.client.Project; @@ -42,8 +41,8 @@ return control.getProject().getNameKey(); } - public ProjectState getState() { - return control.getProject().getState(); + public ProjectState getProjectState() { + return control.getProjectState(); } public ProjectControl getControl() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java index 8b8745e..dfaa370 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -319,7 +319,7 @@ return result; } - public ProjectControl controlFor(final CurrentUser user) { + public ProjectControl controlFor(CurrentUser user) { return projectControlFactory.create(user, this); } @@ -394,6 +394,14 @@ return getInheritableBoolean(Project::getRejectImplicitMerges); } + public boolean isEnableReviewerByEmail() { + return getInheritableBoolean(Project::getEnableReviewerByEmail); + } + + public boolean isMatchAuthorToCommitterDate() { + return getInheritableBoolean(Project::getMatchAuthorToCommitterDate); + } + public LabelTypes getLabelTypes() { Map<String, LabelType> types = new LinkedHashMap<>(); for (ProjectState s : treeInOrder()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java index dcb3404..d461a7d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
@@ -14,8 +14,10 @@ package com.google.gerrit.server.project; +import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.AcceptsCreate; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.IdString; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestCollection; @@ -25,6 +27,9 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.OutputFormat; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; @@ -37,6 +42,7 @@ private final DynamicMap<RestView<ProjectResource>> views; private final Provider<ListProjects> list; private final ProjectControl.GenericFactory controlFactory; + private final PermissionBackend permissionBackend; private final Provider<CurrentUser> user; private final CreateProject.Factory createProjectFactory; @@ -45,11 +51,13 @@ DynamicMap<RestView<ProjectResource>> views, Provider<ListProjects> list, ProjectControl.GenericFactory controlFactory, + PermissionBackend permissionBackend, CreateProject.Factory factory, Provider<CurrentUser> user) { this.views = views; this.list = list; this.controlFactory = controlFactory; + this.permissionBackend = permissionBackend; this.user = user; this.createProjectFactory = factory; } @@ -61,7 +69,7 @@ @Override public ProjectResource parse(TopLevelResource parent, IdString id) - throws ResourceNotFoundException, IOException { + throws ResourceNotFoundException, IOException, PermissionBackendException { ProjectResource rsrc = _parse(id.get(), true); if (rsrc == null) { throw new ResourceNotFoundException(id); @@ -77,8 +85,10 @@ * @throws UnprocessableEntityException thrown if the project ID cannot be resolved or if the * project is not visible to the calling user * @throws IOException thrown when there is an error. + * @throws PermissionBackendException */ - public ProjectResource parse(String id) throws UnprocessableEntityException, IOException { + public ProjectResource parse(String id) + throws UnprocessableEntityException, IOException, PermissionBackendException { return parse(id, true); } @@ -86,33 +96,43 @@ * Parses a project ID from a request body and returns the project. * * @param id ID of the project, can be a project name - * @param checkVisibility Whether to check or not that project is visible to the calling user + * @param checkAccess if true, check the project is accessible by the current user * @return the project * @throws UnprocessableEntityException thrown if the project ID cannot be resolved or if the * project is not visible to the calling user and checkVisibility is true. * @throws IOException thrown when there is an error. + * @throws PermissionBackendException */ - public ProjectResource parse(String id, boolean checkVisibility) - throws UnprocessableEntityException, IOException { - ProjectResource rsrc = _parse(id, checkVisibility); + public ProjectResource parse(String id, boolean checkAccess) + throws UnprocessableEntityException, IOException, PermissionBackendException { + ProjectResource rsrc = _parse(id, checkAccess); if (rsrc == null) { throw new UnprocessableEntityException(String.format("Project Not Found: %s", id)); } return rsrc; } - private ProjectResource _parse(String id, boolean checkVisibility) throws IOException { + @Nullable + private ProjectResource _parse(String id, boolean checkAccess) + throws IOException, PermissionBackendException { if (id.endsWith(Constants.DOT_GIT_EXT)) { id = id.substring(0, id.length() - Constants.DOT_GIT_EXT.length()); } + + Project.NameKey nameKey = new Project.NameKey(id); ProjectControl ctl; try { - ctl = controlFactory.controlFor(new Project.NameKey(id), user.get()); + ctl = controlFactory.controlFor(nameKey, user.get()); } catch (NoSuchProjectException e) { return null; } - if (checkVisibility && !ctl.isVisible() && !ctl.isOwner()) { - return null; + + if (checkAccess) { + try { + permissionBackend.user(user).project(nameKey).check(ProjectPermission.ACCESS); + } catch (AuthException e) { + return null; // Pretend like not found on access denied. + } } return new ProjectResource(ctl); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java index c5ded54..aa4e488 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -22,9 +22,11 @@ import com.google.gerrit.extensions.api.projects.ConfigValue; import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType; import com.google.gerrit.extensions.registration.DynamicMap; +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.ResourceNotFoundException; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.reviewdb.client.Project; @@ -34,6 +36,7 @@ import com.google.gerrit.server.config.PluginConfig; import com.google.gerrit.server.config.PluginConfigFactory; import com.google.gerrit.server.config.ProjectConfigEntry; +import com.google.gerrit.server.extensions.webui.UiActions; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.git.TransferConfig; @@ -62,6 +65,7 @@ private final DynamicMap<ProjectConfigEntry> pluginConfigEntries; private final PluginConfigFactory cfgFactory; private final AllProjectsName allProjects; + private final UiActions uiActions; private final DynamicMap<RestView<ProjectResource>> views; private final Provider<CurrentUser> user; @@ -75,6 +79,7 @@ DynamicMap<ProjectConfigEntry> pluginConfigEntries, PluginConfigFactory cfgFactory, AllProjectsName allProjects, + UiActions uiActions, DynamicMap<RestView<ProjectResource>> views, Provider<CurrentUser> user) { this.serverEnableSignedPush = serverEnableSignedPush; @@ -85,15 +90,15 @@ this.pluginConfigEntries = pluginConfigEntries; this.cfgFactory = cfgFactory; this.allProjects = allProjects; + this.uiActions = uiActions; this.views = views; this.user = user; } @Override - public ConfigInfo apply(ProjectResource rsrc, ConfigInput input) - throws ResourceNotFoundException, BadRequestException, ResourceConflictException { + public ConfigInfo apply(ProjectResource rsrc, ConfigInput input) throws RestApiException { if (!rsrc.getControl().isOwner()) { - throw new ResourceNotFoundException(rsrc.getName()); + throw new AuthException("restricted to project owner"); } return apply(rsrc.getControl(), input); } @@ -154,6 +159,14 @@ p.setState(input.state); } + if (input.enableReviewerByEmail != null) { + p.setEnableReviewerByEmail(input.enableReviewerByEmail); + } + + if (input.matchAuthorToCommitterDate != null) { + p.setMatchAuthorToCommitterDate(input.matchAuthorToCommitterDate); + } + if (input.pluginConfigValues != null) { setPluginConfigValues(ctrl.getProjectState(), projectConfig, input.pluginConfigValues); } @@ -180,6 +193,7 @@ pluginConfigEntries, cfgFactory, allProjects, + uiActions, views); } catch (RepositoryNotFoundException notFound) { throw new ResourceNotFoundException(projectName.get());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java index 8413b5a9..2a22b1e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -14,17 +14,32 @@ package com.google.gerrit.server.project; +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.data.PermissionRange; import com.google.gerrit.common.data.PermissionRule; import com.google.gerrit.extensions.client.ProjectState; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; -import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.group.SystemGroupBackend; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.permissions.FailedPermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackend.ForChange; +import com.google.gerrit.server.permissions.PermissionBackend.ForRef; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.RefPermission; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gwtorm.server.OrmException; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -102,11 +117,18 @@ /** Can this user see this reference exists? */ public boolean isVisible() { if (isVisible == null) { - isVisible = (getUser().isInternalUser() || canPerform(Permission.READ)) && canRead(); + isVisible = + (getUser().isInternalUser() || canPerform(Permission.READ)) + && isProjectStatePermittingRead(); } return isVisible; } + /** Can this user see other users change edits? */ + public boolean isEditVisible() { + return canViewPrivateChanges(); + } + /** True if this reference is visible by all REGISTERED_USERS */ public boolean isVisibleByRegisteredUsers() { List<PermissionRule> access = relevant.getPermission(Permission.READ); @@ -129,39 +151,34 @@ return blocks.isEmpty() && !allows.isEmpty(); } - /** - * Determines whether the user can upload a change to the ref controlled by this object. - * - * @return {@code true} if the user specified can upload a change to the Git ref - */ - public boolean canUpload() { + private boolean canUpload() { return projectControl.controlForRef("refs/for/" + getRefName()).canPerform(Permission.PUSH) - && canWrite(); + && isProjectStatePermittingWrite(); } /** @return true if this user can add a new patch set to this ref */ - public boolean canAddPatchSet() { + boolean canAddPatchSet() { return projectControl .controlForRef("refs/for/" + getRefName()) .canPerform(Permission.ADD_PATCH_SET) - && canWrite(); + && isProjectStatePermittingWrite(); } /** @return true if this user can submit merge patch sets to this ref */ - public boolean canUploadMerges() { + private boolean canUploadMerges() { return projectControl .controlForRef("refs/for/" + getRefName()) .canPerform(Permission.PUSH_MERGE) - && canWrite(); + && isProjectStatePermittingWrite(); } /** @return true if this user can rebase changes on this ref */ - public boolean canRebase() { - return canPerform(Permission.REBASE) && canWrite(); + boolean canRebase() { + return canPerform(Permission.REBASE) && isProjectStatePermittingWrite(); } /** @return true if this user can submit patch sets to this ref */ - public boolean canSubmit(boolean isChangeOwner) { + boolean canSubmit(boolean isChangeOwner) { if (RefNames.REFS_CONFIG.equals(refName)) { // Always allow project owners to submit configuration changes. // Submitting configuration changes modifies the access control @@ -170,16 +187,11 @@ // granting of powers beyond submitting to the configuration. return projectControl.isOwner(); } - return canPerform(Permission.SUBMIT, isChangeOwner) && canWrite(); - } - - /** @return true if this user was granted submitAs to this ref */ - public boolean canSubmitAs() { - return canPerform(Permission.SUBMIT_AS); + return canPerform(Permission.SUBMIT, isChangeOwner) && isProjectStatePermittingWrite(); } /** @return true if the user can update the reference as a fast-forward. */ - public boolean canUpdate() { + private boolean canUpdate() { if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) { // Pushing requires being at least project owner, in addition to push. // Pushing configuration changes modifies the access control @@ -190,17 +202,16 @@ // On the AllProjects project the owner access right cannot be assigned, // this why for the AllProjects project we allow administrators to push // configuration changes if they have push without being project owner. - if (!(projectControl.getProjectState().isAllProjects() - && getUser().getCapabilities().canAdministrateServer())) { + if (!(projectControl.getProjectState().isAllProjects() && projectControl.isAdmin())) { return false; } } - return canPerform(Permission.PUSH) && canWrite(); + return canPerform(Permission.PUSH) && isProjectStatePermittingWrite(); } /** @return true if the user can rewind (force push) the reference. */ - public boolean canForceUpdate() { - if (!canWrite()) { + private boolean canForceUpdate() { + if (!isProjectStatePermittingWrite()) { return false; } @@ -218,21 +229,22 @@ case UNKNOWN: case WEB_BROWSER: default: - return getUser().getCapabilities().canAdministrateServer() - || (isOwner() && !isForceBlocked(Permission.PUSH)); + return (isOwner() && !isForceBlocked(Permission.PUSH)) || projectControl.isAdmin(); } } - public boolean canWrite() { + private boolean isProjectStatePermittingWrite() { return getProjectControl().getProject().getState().equals(ProjectState.ACTIVE); } - public boolean canRead() { - return getProjectControl().getProject().getState().equals(ProjectState.READ_ONLY) || canWrite(); + private boolean isProjectStatePermittingRead() { + return getProjectControl().getProject().getState().equals(ProjectState.READ_ONLY) + || isProjectStatePermittingWrite(); } private boolean canPushWithForce() { - if (!canWrite() || (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner())) { + if (!isProjectStatePermittingWrite() + || (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner())) { // Pushing requires being at least project owner, in addition to push. // Pushing configuration changes modifies the access control // rules. Allowing this to be done by a non-project-owner opens @@ -246,28 +258,34 @@ /** * Determines whether the user can create a new Git ref. * - * @param db db for checking change visibility. * @param repo repository on which user want to create * @param object the object the user will start the reference with. - * @return {@code true} if the user specified can create a new Git ref + * @return {@code null} if the user specified can create a new Git ref, or a String describing why + * the creation is not allowed. */ - public boolean canCreate(ReviewDb db, Repository repo, RevObject object) { - if (!canWrite()) { - return false; + @Nullable + public String canCreate(Repository repo, RevObject object) { + if (!isProjectStatePermittingWrite()) { + return "project state does not permit write"; } if (object instanceof RevCommit) { if (!canPerform(Permission.CREATE)) { - // No create permissions. - return false; + return "lacks permission: " + Permission.CREATE; } - return canCreateCommit(db, repo, (RevCommit) object); + return canCreateCommit(repo, (RevCommit) object); } else if (object instanceof RevTag) { final RevTag tag = (RevTag) object; try (RevWalk rw = new RevWalk(repo)) { rw.parseBody(tag); } catch (IOException e) { - return false; + String msg = + String.format( + "RevWalk(%s) for pushing tag %s:", + projectControl.getProject().getNameKey(), tag.name()); + log.error(msg, e); + + return "I/O exception for revwalk"; } // If tagger is present, require it matches the user's email. @@ -282,18 +300,20 @@ valid = false; } if (!valid && !canForgeCommitter()) { - return false; + return "lacks permission: " + Permission.FORGE_COMMITTER; } } RevObject tagObject = tag.getObject(); if (tagObject instanceof RevCommit) { - if (!canCreateCommit(db, repo, (RevCommit) tagObject)) { - return false; + String rejectReason = canCreateCommit(repo, (RevCommit) tagObject); + if (rejectReason != null) { + return rejectReason; } } else { - if (!canCreate(db, repo, tagObject)) { - return false; + String rejectReason = canCreate(repo, tagObject); + if (rejectReason != null) { + return rejectReason; } } @@ -301,34 +321,41 @@ // than if it doesn't have a PGP signature. // if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) { - return canPerform(Permission.CREATE_SIGNED_TAG); + return canPerform(Permission.CREATE_SIGNED_TAG) + ? null + : "lacks permission: " + Permission.CREATE_SIGNED_TAG; } - return canPerform(Permission.CREATE_TAG); - } else { - return false; + return canPerform(Permission.CREATE_TAG) ? null : "lacks permission " + Permission.CREATE_TAG; } + + return null; } - private boolean canCreateCommit(ReviewDb db, Repository repo, RevCommit commit) { + /** + * Check if the user is allowed to create a new commit object if this introduces a new commit to + * the project. If not allowed, returns a string describing why it's not allowed. + */ + @Nullable + private String canCreateCommit(Repository repo, RevCommit commit) { if (canUpdate()) { // If the user has push permissions, they can create the ref regardless // of whether they are pushing any new objects along with the create. - return true; - } else if (isMergedIntoBranchOrTag(db, repo, commit)) { + return null; + } else if (isMergedIntoBranchOrTag(repo, commit)) { // If the user has no push permissions, check whether the object is // merged into a branch or tag readable by this user. If so, they are // not effectively "pushing" more objects, so they can create the ref // even if they don't have push permission. - return true; + return null; } - return false; + return "lacks permission " + Permission.PUSH + " for creating new commit object"; } - private boolean isMergedIntoBranchOrTag(ReviewDb db, Repository repo, RevCommit commit) { + private boolean isMergedIntoBranchOrTag(Repository repo, RevCommit commit) { try (RevWalk rw = new RevWalk(repo)) { List<Ref> refs = new ArrayList<>(repo.getRefDatabase().getRefs(Constants.R_HEADS).values()); refs.addAll(repo.getRefDatabase().getRefs(Constants.R_TAGS).values()); - return projectControl.isMergedIntoVisibleRef(repo, db, rw, commit, refs); + return projectControl.isMergedIntoVisibleRef(repo, rw, commit, refs); } catch (IOException e) { String msg = String.format( @@ -344,8 +371,8 @@ * * @return {@code true} if the user specified can delete a Git ref. */ - public boolean canDelete() { - if (!canWrite() || (RefNames.REFS_CONFIG.equals(refName))) { + private boolean canDelete() { + if (!isProjectStatePermittingWrite() || (RefNames.REFS_CONFIG.equals(refName))) { // Never allow removal of the refs/meta/config branch. // Deleting the branch would destroy all Gerrit specific // metadata about the project, including its access rules. @@ -364,15 +391,15 @@ case UNKNOWN: case WEB_BROWSER: default: - return getUser().getCapabilities().canAdministrateServer() - || (isOwner() && !isForceBlocked(Permission.PUSH)) + return (isOwner() && !isForceBlocked(Permission.PUSH)) || canPushWithForce() - || canPerform(Permission.DELETE); + || canPerform(Permission.DELETE) + || projectControl.isAdmin(); } } /** @return true if this user can forge the author line in a commit. */ - public boolean canForgeAuthor() { + private boolean canForgeAuthor() { if (canForgeAuthor == null) { canForgeAuthor = canPerform(Permission.FORGE_AUTHOR); } @@ -380,7 +407,7 @@ } /** @return true if this user can forge the committer line in a commit. */ - public boolean canForgeCommitter() { + private boolean canForgeCommitter() { if (canForgeCommitter == null) { canForgeCommitter = canPerform(Permission.FORGE_COMMITTER); } @@ -388,12 +415,12 @@ } /** @return true if this user can forge the server on the committer line. */ - public boolean canForgeGerritServerIdentity() { + private boolean canForgeGerritServerIdentity() { return canPerform(Permission.FORGE_SERVER); } /** @return true if this user can abandon a change for this ref */ - public boolean canAbandon() { + boolean canAbandon() { return canPerform(Permission.ABANDON); } @@ -403,46 +430,51 @@ } /** @return true if this user can view draft changes. */ - public boolean canViewDrafts() { + boolean canViewDrafts() { return canPerform(Permission.VIEW_DRAFTS); } + /** @return true if this user can view private changes. */ + boolean canViewPrivateChanges() { + return canPerform(Permission.VIEW_PRIVATE_CHANGES); + } + /** @return true if this user can publish draft changes. */ - public boolean canPublishDrafts() { + boolean canPublishDrafts() { return canPerform(Permission.PUBLISH_DRAFTS); } /** @return true if this user can delete draft changes. */ - public boolean canDeleteDrafts() { + boolean canDeleteDrafts() { return canPerform(Permission.DELETE_DRAFTS); } /** @return true if this user can delete their own changes. */ - public boolean canDeleteOwnChanges() { + boolean canDeleteOwnChanges() { return canPerform(Permission.DELETE_OWN_CHANGES); } /** @return true if this user can edit topic names. */ - public boolean canEditTopicName() { + boolean canEditTopicName() { return canPerform(Permission.EDIT_TOPIC_NAME); } /** @return true if this user can edit hashtag names. */ - public boolean canEditHashtags() { + boolean canEditHashtags() { return canPerform(Permission.EDIT_HASHTAGS); } - public boolean canEditAssignee() { + boolean canEditAssignee() { return canPerform(Permission.EDIT_ASSIGNEE); } /** @return true if this user can force edit topic names. */ - public boolean canForceEditTopicName() { + boolean canForceEditTopicName() { return canForcePerform(Permission.EDIT_TOPIC_NAME); } /** All value ranges of any allowed label permission. */ - public List<PermissionRange> getLabelRanges(boolean isChangeOwner) { + List<PermissionRange> getLabelRanges(boolean isChangeOwner) { List<PermissionRange> r = new ArrayList<>(); for (Map.Entry<String, List<PermissionRule>> e : relevant.getDeclaredPermissions()) { if (Permission.isLabel(e.getKey())) { @@ -463,12 +495,12 @@ } /** The range of permitted values associated with a label permission. */ - public PermissionRange getRange(String permission) { + PermissionRange getRange(String permission) { return getRange(permission, false); } /** The range of permitted values associated with a label permission. */ - public PermissionRange getRange(String permission, boolean isChangeOwner) { + PermissionRange getRange(String permission, boolean isChangeOwner) { if (Permission.hasRange(permission)) { return toRange(permission, access(permission, isChangeOwner)); } @@ -642,4 +674,94 @@ effective.put(permissionName, mine); return mine; } + + ForRef asForRef() { + return new ForRefImpl(); + } + + private class ForRefImpl extends ForRef { + @Override + public ForRef user(CurrentUser user) { + return forUser(user).asForRef().database(db); + } + + @Override + public ForChange change(ChangeData cd) { + try { + return cd.changeControl().forUser(getUser()).asForChange(cd, db); + } catch (OrmException e) { + return FailedPermissionBackend.change("unavailable", e); + } + } + + @Override + public ForChange change(ChangeNotes notes) { + Project.NameKey project = getProjectControl().getProject().getNameKey(); + Change change = notes.getChange(); + checkArgument( + project.equals(change.getProject()), + "expected change in project %s, not %s", + project, + change.getProject()); + return getProjectControl().controlFor(notes).asForChange(null, db); + } + + @Override + public void check(RefPermission perm) throws AuthException, PermissionBackendException { + if (!can(perm)) { + throw new AuthException(perm.describeForException() + " not permitted"); + } + } + + @Override + public Set<RefPermission> test(Collection<RefPermission> permSet) + throws PermissionBackendException { + EnumSet<RefPermission> ok = EnumSet.noneOf(RefPermission.class); + for (RefPermission perm : permSet) { + if (can(perm)) { + ok.add(perm); + } + } + return ok; + } + + private boolean can(RefPermission perm) throws PermissionBackendException { + switch (perm) { + case READ: + return isVisible(); + case CREATE: + // TODO This isn't an accurate test. + return canPerform(perm.permissionName().get()); + case DELETE: + return canDelete(); + case UPDATE: + return canUpdate(); + case FORCE_UPDATE: + return canForceUpdate(); + + case FORGE_AUTHOR: + return canForgeAuthor(); + case FORGE_COMMITTER: + return canForgeCommitter(); + case FORGE_SERVER: + return canForgeGerritServerIdentity(); + case MERGE: + return canUploadMerges(); + + case CREATE_CHANGE: + return canUpload(); + + case UPDATE_BY_SUBMIT: + return projectControl.controlForRef("refs/for/" + getRefName()).canSubmit(true); + + case BYPASS_REVIEW: + return canForgeAuthor() + && canForgeCommitter() + && canForgeGerritServerIdentity() + && canUploadMerges() + && !projectControl.getProjectState().isUseSignedOffBy(); + } + throw new PermissionBackendException(perm + " unsupported"); + } + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java index c74efc6..6c55c37 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
@@ -41,6 +41,9 @@ import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.group.GroupsCollection; +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 com.google.inject.Provider; import com.google.inject.Singleton; @@ -54,6 +57,7 @@ @Singleton public class SetAccess implements RestModifyView<ProjectResource, ProjectAccessInput> { protected final GroupBackend groupBackend; + private final PermissionBackend permissionBackend; private final GroupsCollection groupsCollection; private final Provider<MetaDataUpdate.User> metaDataUpdateFactory; private final AllProjectsName allProjects; @@ -65,6 +69,7 @@ @Inject private SetAccess( GroupBackend groupBackend, + PermissionBackend permissionBackend, Provider<MetaDataUpdate.User> metaDataUpdateFactory, AllProjectsName allProjects, Provider<SetParent> setParent, @@ -73,6 +78,7 @@ GetAccess getAccess, Provider<IdentifiedUser> identifiedUser) { this.groupBackend = groupBackend; + this.permissionBackend = permissionBackend; this.metaDataUpdateFactory = metaDataUpdateFactory; this.allProjects = allProjects; this.setParent = setParent; @@ -85,7 +91,7 @@ @Override public ProjectAccessInfo apply(ProjectResource rsrc, ProjectAccessInput input) throws ResourceNotFoundException, ResourceConflictException, IOException, AuthException, - BadRequestException, UnprocessableEntityException { + BadRequestException, UnprocessableEntityException, PermissionBackendException { List<AccessSection> removals = getAccessSections(input.remove); List<AccessSection> additions = getAccessSections(input.add); MetaDataUpdate.User metaDataUpdateUser = metaDataUpdateFactory.get(); @@ -269,16 +275,11 @@ } private void checkGlobalCapabilityPermissions(Project.NameKey projectName) - throws BadRequestException, AuthException { - + throws BadRequestException, AuthException, PermissionBackendException { if (!allProjects.equals(projectName)) { throw new BadRequestException( "Cannot edit global capabilities for projects other than " + allProjects.get()); } - - if (!identifiedUser.get().getCapabilities().canAdministrateServer()) { - throw new AuthException( - "Editing global capabilities requires " + GlobalCapability.ADMINISTRATE_SERVER); - } + permissionBackend.user(identifiedUser).check(GlobalPermission.ADMINISTRATE_SERVER); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java index 6c45bc3..90d083b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
@@ -65,7 +65,7 @@ } @Override - public String apply(final ProjectResource rsrc, Input input) + public String apply(ProjectResource rsrc, Input input) throws AuthException, ResourceNotFoundException, BadRequestException, UnprocessableEntityException, IOException { if (!rsrc.getControl().isOwner()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java index f8d649b..56e3273 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -30,6 +30,9 @@ import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.SetParent.Input; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -45,12 +48,18 @@ } private final ProjectCache cache; + private final PermissionBackend permissionBackend; private final MetaDataUpdate.Server updateFactory; private final AllProjectsName allProjects; @Inject - SetParent(ProjectCache cache, MetaDataUpdate.Server updateFactory, AllProjectsName allProjects) { + SetParent( + ProjectCache cache, + PermissionBackend permissionBackend, + MetaDataUpdate.Server updateFactory, + AllProjectsName allProjects) { this.cache = cache; + this.permissionBackend = permissionBackend; this.updateFactory = updateFactory; this.allProjects = allProjects; } @@ -58,13 +67,13 @@ @Override public String apply(ProjectResource rsrc, Input input) throws AuthException, ResourceConflictException, ResourceNotFoundException, - UnprocessableEntityException, IOException { + UnprocessableEntityException, IOException, PermissionBackendException { return apply(rsrc, input, true); } public String apply(ProjectResource rsrc, Input input, boolean checkIfAdmin) throws AuthException, ResourceConflictException, ResourceNotFoundException, - UnprocessableEntityException, IOException { + UnprocessableEntityException, IOException, PermissionBackendException { ProjectControl ctl = rsrc.getControl(); String parentName = MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get()); @@ -96,11 +105,12 @@ } } - public void validateParentUpdate(final ProjectControl ctl, String newParent, boolean checkIfAdmin) - throws AuthException, ResourceConflictException, UnprocessableEntityException { + public void validateParentUpdate(ProjectControl ctl, String newParent, boolean checkIfAdmin) + throws AuthException, ResourceConflictException, UnprocessableEntityException, + PermissionBackendException { IdentifiedUser user = ctl.getUser().asIdentifiedUser(); - if (checkIfAdmin && !user.getCapabilities().canAdministrateServer()) { - throw new AuthException("not administrator"); + if (checkIfAdmin) { + permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER); } if (ctl.getProject().getNameKey().equals(allProjects)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java index d535062..937fe75 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -27,6 +27,7 @@ import com.google.gerrit.rules.PrologEnvironment; import com.google.gerrit.rules.StoredValues; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.query.change.ChangeData; import com.google.gwtorm.server.OrmException; import com.googlecode.prolog_cafe.exceptions.CompileException; @@ -81,6 +82,7 @@ } } + private final AccountCache accountCache; private final ChangeData cd; private final ChangeControl control; @@ -92,7 +94,8 @@ private Term submitRule; - public SubmitRuleEvaluator(ChangeData cd) throws OrmException { + public SubmitRuleEvaluator(AccountCache accountCache, ChangeData cd) throws OrmException { + this.accountCache = accountCache; this.cd = cd; this.control = cd.changeControl(); } @@ -564,6 +567,7 @@ } throw new RuleEvalException(msg, err); } + env.set(StoredValues.ACCOUNT_CACHE, accountCache); env.set(StoredValues.REVIEW_DB, cd.db()); env.set(StoredValues.CHANGE_DATA, cd); env.set(StoredValues.CHANGE_CONTROL, control);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java index 9d3005c..7c7f8a5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java
@@ -14,65 +14,61 @@ package com.google.gerrit.server.project; +import static java.util.stream.Collectors.toList; + import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.config.AllProjectsName; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.permissions.ProjectPermission; import com.google.inject.Inject; +import com.google.inject.Provider; import com.google.inject.Singleton; -import java.util.ArrayList; -import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.TreeSet; @Singleton public class SuggestParentCandidates { - private final ProjectControl.Factory projectControlFactory; private final ProjectCache projectCache; - private final AllProjectsName allProject; + private final PermissionBackend permissionBackend; + private final Provider<CurrentUser> user; + private final AllProjectsName allProjects; @Inject SuggestParentCandidates( - final ProjectControl.Factory projectControlFactory, - final ProjectCache projectCache, - final AllProjectsName allProject) { - this.projectControlFactory = projectControlFactory; + ProjectCache projectCache, + PermissionBackend permissionBackend, + Provider<CurrentUser> user, + AllProjectsName allProjects) { this.projectCache = projectCache; - this.allProject = allProject; + this.permissionBackend = permissionBackend; + this.user = user; + this.allProjects = allProjects; } - public List<Project.NameKey> getNameKeys() throws NoSuchProjectException { - List<Project> pList = getProjects(); - final List<Project.NameKey> nameKeys = new ArrayList<>(pList.size()); - for (Project p : pList) { - nameKeys.add(p.getNameKey()); - } - return nameKeys; + public List<Project.NameKey> getNameKeys() throws PermissionBackendException { + return permissionBackend + .user(user) + .filter(ProjectPermission.ACCESS, parents()) + .stream() + .sorted() + .collect(toList()); } - public List<Project> getProjects() throws NoSuchProjectException { - Set<Project> projects = - new TreeSet<>( - new Comparator<Project>() { - @Override - public int compare(Project o1, Project o2) { - return o1.getName().compareTo(o2.getName()); - } - }); + private Set<Project.NameKey> parents() { + Set<Project.NameKey> parents = new HashSet<>(); for (Project.NameKey p : projectCache.all()) { - try { - final ProjectControl control = projectControlFactory.controlFor(p); - final Project.NameKey parentK = control.getProject().getParent(); - if (parentK != null) { - ProjectControl pControl = projectControlFactory.controlFor(parentK); - if (pControl.isVisible() || pControl.isOwner()) { - projects.add(pControl.getProject()); - } + ProjectState ps = projectCache.get(p); + if (ps != null) { + Project.NameKey parent = ps.getProject().getParent(); + if (parent != null) { + parents.add(parent); } - } catch (NoSuchProjectException e) { - continue; } } - projects.add(projectControlFactory.controlFor(allProject).getProject()); - return new ArrayList<>(projects); + parents.add(allProjects); + return parents; } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java index 1bf6d8b..7d99052 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
@@ -29,11 +29,11 @@ private final int cost; @SafeVarargs - protected AndPredicate(final Predicate<T>... that) { + protected AndPredicate(Predicate<T>... that) { this(Arrays.asList(that)); } - protected AndPredicate(final Collection<? extends Predicate<T>> that) { + protected AndPredicate(Collection<? extends Predicate<T>> that) { List<Predicate<T>> t = new ArrayList<>(that.size()); int c = 0; for (Predicate<T> p : that) { @@ -62,12 +62,12 @@ } @Override - public final Predicate<T> getChild(final int i) { + public final Predicate<T> getChild(int i) { return children.get(i); } @Override - public Predicate<T> copy(final Collection<? extends Predicate<T>> children) { + public Predicate<T> copy(Collection<? extends Predicate<T>> children) { return new AndPredicate<>(children); } @@ -82,7 +82,7 @@ } @Override - public boolean match(final T object) throws OrmException { + public boolean match(T object) throws OrmException { for (Predicate<T> c : children) { checkState( c.isMatchable(), @@ -107,7 +107,7 @@ } @Override - public boolean equals(final Object other) { + public boolean equals(Object other) { if (other == null) { return false; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java index 6627687..42dcff8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/IntPredicate.java
@@ -16,29 +16,29 @@ /** Predicate to filter a field by matching integer value. */ public abstract class IntPredicate<T> extends OperatorPredicate<T> { - private final int value; + private final int intValue; - public IntPredicate(final String name, final String value) { + public IntPredicate(String name, String value) { super(name, value); - this.value = Integer.parseInt(value); + this.intValue = Integer.parseInt(value); } - public IntPredicate(final String name, final int value) { - super(name, String.valueOf(value)); - this.value = value; + public IntPredicate(String name, int intValue) { + super(name, String.valueOf(intValue)); + this.intValue = intValue; } public int intValue() { - return value; + return intValue; } @Override public int hashCode() { - return getOperator().hashCode() * 31 + value; + return getOperator().hashCode() * 31 + intValue; } @Override - public boolean equals(final Object other) { + public boolean equals(Object other) { if (other == null) { return false; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java index 3716ec1..306b4cb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
@@ -25,7 +25,7 @@ public class NotPredicate<T> extends Predicate<T> implements Matchable<T> { private final Predicate<T> that; - protected NotPredicate(final Predicate<T> that) { + protected NotPredicate(Predicate<T> that) { if (that instanceof NotPredicate) { throw new IllegalArgumentException("Double negation unsupported"); } @@ -43,7 +43,7 @@ } @Override - public final Predicate<T> getChild(final int i) { + public final Predicate<T> getChild(int i) { if (i != 0) { throw new ArrayIndexOutOfBoundsException(i); } @@ -51,7 +51,7 @@ } @Override - public Predicate<T> copy(final Collection<? extends Predicate<T>> children) { + public Predicate<T> copy(Collection<? extends Predicate<T>> children) { if (children.size() != 1) { throw new IllegalArgumentException("Expected exactly one child"); } @@ -64,7 +64,7 @@ } @Override - public boolean match(final T object) throws OrmException { + public boolean match(T object) throws OrmException { checkState( that.isMatchable(), "match invoked, but child predicate %s doesn't implement %s", @@ -84,7 +84,7 @@ } @Override - public boolean equals(final Object other) { + public boolean equals(Object other) { if (other == null) { return false; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java index 96a30ee..254aa99 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OperatorPredicate.java
@@ -18,10 +18,10 @@ /** Predicate to filter a field by matching value. */ public abstract class OperatorPredicate<T> extends Predicate<T> { - private final String name; - private final String value; + protected final String name; + protected final String value; - protected OperatorPredicate(final String name, final String value) { + public OperatorPredicate(String name, String value) { this.name = name; this.value = value; } @@ -35,7 +35,7 @@ } @Override - public Predicate<T> copy(final Collection<? extends Predicate<T>> children) { + public Predicate<T> copy(Collection<? extends Predicate<T>> children) { if (!children.isEmpty()) { throw new IllegalArgumentException("Expected 0 children"); } @@ -48,7 +48,7 @@ } @Override - public boolean equals(final Object other) { + public boolean equals(Object other) { if (other == null) { return false; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java index 4845a86..f357344 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
@@ -29,11 +29,11 @@ private final int cost; @SafeVarargs - protected OrPredicate(final Predicate<T>... that) { + protected OrPredicate(Predicate<T>... that) { this(Arrays.asList(that)); } - protected OrPredicate(final Collection<? extends Predicate<T>> that) { + protected OrPredicate(Collection<? extends Predicate<T>> that) { List<Predicate<T>> t = new ArrayList<>(that.size()); int c = 0; for (Predicate<T> p : that) { @@ -62,12 +62,12 @@ } @Override - public final Predicate<T> getChild(final int i) { + public final Predicate<T> getChild(int i) { return children.get(i); } @Override - public Predicate<T> copy(final Collection<? extends Predicate<T>> children) { + public Predicate<T> copy(Collection<? extends Predicate<T>> children) { return new OrPredicate<>(children); } @@ -82,8 +82,8 @@ } @Override - public boolean match(final T object) throws OrmException { - for (final Predicate<T> c : children) { + public boolean match(T object) throws OrmException { + for (Predicate<T> c : children) { checkState( c.isMatchable(), "match invoked, but child predicate %s doesn't implement %s", @@ -107,7 +107,7 @@ } @Override - public boolean equals(final Object other) { + public boolean equals(Object other) { if (other == null) { return false; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java index aabc066..c5b2b96 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
@@ -48,7 +48,7 @@ /** Combine the passed predicates into a single AND node. */ @SafeVarargs - public static <T> Predicate<T> and(final Predicate<T>... that) { + public static <T> Predicate<T> and(Predicate<T>... that) { if (that.length == 1) { return that[0]; } @@ -56,7 +56,7 @@ } /** Combine the passed predicates into a single AND node. */ - public static <T> Predicate<T> and(final Collection<? extends Predicate<T>> that) { + public static <T> Predicate<T> and(Collection<? extends Predicate<T>> that) { if (that.size() == 1) { return Iterables.getOnlyElement(that); } @@ -65,7 +65,7 @@ /** Combine the passed predicates into a single OR node. */ @SafeVarargs - public static <T> Predicate<T> or(final Predicate<T>... that) { + public static <T> Predicate<T> or(Predicate<T>... that) { if (that.length == 1) { return that[0]; } @@ -73,7 +73,7 @@ } /** Combine the passed predicates into a single OR node. */ - public static <T> Predicate<T> or(final Collection<? extends Predicate<T>> that) { + public static <T> Predicate<T> or(Collection<? extends Predicate<T>> that) { if (that.size() == 1) { return Iterables.getOnlyElement(that); } @@ -81,7 +81,7 @@ } /** Invert the passed node. */ - public static <T> Predicate<T> not(final Predicate<T> that) { + public static <T> Predicate<T> not(Predicate<T> that) { if (that instanceof NotPredicate) { // Negate of a negate is the original predicate. // @@ -101,7 +101,7 @@ } /** Same as {@code getChildren().get(i)} */ - public Predicate<T> getChild(final int i) { + public Predicate<T> getChild(int i) { return getChildren().get(i); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java index 62144ec..c1fecba 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryBuilder.java
@@ -48,7 +48,7 @@ * * <pre> * @Operator - * public Predicate is(final String value) { + * public Predicate is(String value) { * if ("starred".equals(value)) { * return new StarredPredicate(); * } @@ -92,7 +92,7 @@ // Class<?> c = clazz; while (c != QueryBuilder.class) { - for (final Method method : c.getDeclaredMethods()) { + for (Method method : c.getDeclaredMethods()) { if (method.getAnnotation(Operator.class) != null && Predicate.class.isAssignableFrom(method.getReturnType()) && method.getParameterTypes().length == 1 @@ -180,7 +180,7 @@ * This may be due to a syntax error, may be due to an operator not being supported, or due to * an invalid value being passed to a recognized operator. */ - public Predicate<T> parse(final String query) throws QueryParseException { + public Predicate<T> parse(String query) throws QueryParseException { if (Strings.isNullOrEmpty(query)) { throw new QueryParseException("query is empty"); } @@ -196,7 +196,7 @@ * parser. This may be due to a syntax error, may be due to an operator not being supported, * or due to an invalid value being passed to a recognized operator. */ - public List<Predicate<T>> parse(final List<String> queries) throws QueryParseException { + public List<Predicate<T>> parse(List<String> queries) throws QueryParseException { List<Predicate<T>> predicates = new ArrayList<>(queries.size()); for (String query : queries) { predicates.add(parse(query)); @@ -204,8 +204,7 @@ return predicates; } - private Predicate<T> toPredicate(final Tree r) - throws QueryParseException, IllegalArgumentException { + private Predicate<T> toPredicate(Tree r) throws QueryParseException, IllegalArgumentException { switch (r.getType()) { case AND: return and(children(r)); @@ -225,7 +224,7 @@ } } - private Predicate<T> operator(final String name, final Tree val) throws QueryParseException { + private Predicate<T> operator(String name, Tree val) throws QueryParseException { switch (val.getType()) { // Expand multiple values, "foo:(a b c)", as though they were written // out with the longer form, "foo:a foo:b foo:c". @@ -257,7 +256,7 @@ } @SuppressWarnings("unchecked") - private Predicate<T> operator(final String name, final String value) throws QueryParseException { + private Predicate<T> operator(String name, String value) throws QueryParseException { @SuppressWarnings("rawtypes") OperatorFactory f = opFactories.get(name); if (f == null) { @@ -266,7 +265,7 @@ return f.create(this, value); } - private Predicate<T> defaultField(final Tree r) throws QueryParseException { + private Predicate<T> defaultField(Tree r) throws QueryParseException { switch (r.getType()) { case SINGLE_WORD: case EXACT_PHRASE: @@ -291,12 +290,11 @@ * @return predicate representing this value. * @throws QueryParseException the parser does not recognize this value. */ - protected Predicate<T> defaultField(final String value) throws QueryParseException { + protected Predicate<T> defaultField(String value) throws QueryParseException { throw error("Unsupported query:" + value); } - private List<Predicate<T>> children(final Tree r) - throws QueryParseException, IllegalArgumentException { + private List<Predicate<T>> children(Tree r) throws QueryParseException, IllegalArgumentException { List<Predicate<T>> p = new ArrayList<>(r.getChildCount()); for (int i = 0; i < r.getChildCount(); i++) { p.add(toPredicate(r.getChild(i))); @@ -304,7 +302,7 @@ return p; } - private Tree onlyChildOf(final Tree r) throws QueryParseException { + private Tree onlyChildOf(Tree r) throws QueryParseException { if (r.getChildCount() != 1) { throw error("Expected exactly one child: " + r); } @@ -329,7 +327,7 @@ private final String name; private final Method method; - ReflectionFactory(final String name, final Method method) { + ReflectionFactory(String name, Method method) { this.name = name; this.method = method; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java index 5fb4497..0d0777f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
@@ -24,6 +24,7 @@ import com.google.gerrit.metrics.MetricMaker; import com.google.gerrit.metrics.Timer1; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.index.Index; import com.google.gerrit.server.index.IndexCollection; import com.google.gerrit.server.index.IndexConfig; @@ -61,6 +62,7 @@ protected final Provider<CurrentUser> userProvider; + private final CapabilityControl.Factory capabilityFactory; private final Metrics metrics; private final SchemaDefinitions<T> schemaDef; private final IndexConfig indexConfig; @@ -76,6 +78,7 @@ protected QueryProcessor( Provider<CurrentUser> userProvider, + CapabilityControl.Factory capabilityFactory, Metrics metrics, SchemaDefinitions<T> schemaDef, IndexConfig indexConfig, @@ -83,6 +86,7 @@ IndexRewriter<T> rewriter, String limitField) { this.userProvider = userProvider; + this.capabilityFactory = capabilityFactory; this.metrics = metrics; this.schemaDef = schemaDef; this.indexConfig = indexConfig; @@ -231,7 +235,10 @@ private int getPermittedLimit() { if (enforceVisibility) { - return userProvider.get().getCapabilities().getRange(GlobalCapability.QUERY_LIMIT).getMax(); + return capabilityFactory + .create(userProvider.get()) + .getRange(GlobalCapability.QUERY_LIMIT) + .getMax(); } return Integer.MAX_VALUE; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java index 0a74647..e5ed44d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountIsVisibleToPredicate.java
@@ -20,9 +20,9 @@ import com.google.gwtorm.server.OrmException; public class AccountIsVisibleToPredicate extends IsVisibleToPredicate<AccountState> { - private final AccountControl accountControl; + protected final AccountControl accountControl; - AccountIsVisibleToPredicate(AccountControl accountControl) { + public AccountIsVisibleToPredicate(AccountControl accountControl) { super(AccountQueryBuilder.FIELD_VISIBLETO, describe(accountControl.getUser())); this.accountControl = accountControl; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java index 796539b..6c8948f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountPredicates.java
@@ -31,7 +31,7 @@ return QueryBuilder.find(p, AccountPredicate.class, AccountField.ACTIVE.getName()) != null; } - static Predicate<AccountState> defaultPredicate(String query) { + public static Predicate<AccountState> defaultPredicate(String query) { // Adapt the capacity of this list when adding more default predicates. List<Predicate<AccountState>> preds = Lists.newArrayListWithCapacity(3); Integer id = Ints.tryParse(query); @@ -50,21 +50,28 @@ AccountField.ID, AccountQueryBuilder.FIELD_ACCOUNT, accountId.toString()); } - static Predicate<AccountState> email(String email) { + public static Predicate<AccountState> email(String email) { return new AccountPredicate( AccountField.EMAIL, AccountQueryBuilder.FIELD_EMAIL, email.toLowerCase()); } - static Predicate<AccountState> equalsName(String name) { + public static Predicate<AccountState> preferredEmail(String email) { + return new AccountPredicate( + AccountField.PREFERRED_EMAIL, + AccountQueryBuilder.FIELD_PREFERRED_EMAIL, + email.toLowerCase()); + } + + public static Predicate<AccountState> equalsName(String name) { return new AccountPredicate( AccountField.NAME_PART, AccountQueryBuilder.FIELD_NAME, name.toLowerCase()); } - static Predicate<AccountState> externalId(String externalId) { + public static Predicate<AccountState> externalId(String externalId) { return new AccountPredicate(AccountField.EXTERNAL_ID, externalId); } - static Predicate<AccountState> fullName(String fullName) { + public static Predicate<AccountState> fullName(String fullName) { return new AccountPredicate(AccountField.FULL_NAME, fullName); } @@ -72,16 +79,16 @@ return new AccountPredicate(AccountField.ACTIVE, "1"); } - static Predicate<AccountState> isInactive() { + public static Predicate<AccountState> isNotActive() { return new AccountPredicate(AccountField.ACTIVE, "0"); } - static Predicate<AccountState> username(String username) { + public static Predicate<AccountState> username(String username) { return new AccountPredicate( AccountField.USERNAME, AccountQueryBuilder.FIELD_USERNAME, username.toLowerCase()); } - static Predicate<AccountState> watchedProject(Project.NameKey project) { + public static Predicate<AccountState> watchedProject(Project.NameKey project) { return new AccountPredicate(AccountField.WATCHED_PROJECT, project.get()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java index 5ae6a67..6122277 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -36,6 +36,7 @@ public static final String FIELD_EMAIL = "email"; public static final String FIELD_LIMIT = "limit"; public static final String FIELD_NAME = "name"; + public static final String FIELD_PREFERRED_EMAIL = "preferredemail"; public static final String FIELD_USERNAME = "username"; public static final String FIELD_VISIBLETO = "visibleto"; @@ -90,7 +91,7 @@ return AccountPredicates.isActive(); } if ("inactive".equalsIgnoreCase(value)) { - return AccountPredicates.isInactive(); + return AccountPredicates.isNotActive(); } throw error("Invalid query"); } @@ -123,7 +124,7 @@ @Override protected Predicate<AccountState> defaultField(String query) { Predicate<AccountState> defaultPredicate = AccountPredicates.defaultPredicate(query); - if ("self".equalsIgnoreCase(query)) { + if ("self".equalsIgnoreCase(query) || "me".equalsIgnoreCase(query)) { try { return Predicate.or(defaultPredicate, AccountPredicates.id(self())); } catch (QueryParseException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java index d984e6d..6a9b37c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryProcessor.java
@@ -20,6 +20,7 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.AccountControl; import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.index.IndexConfig; import com.google.gerrit.server.index.IndexPredicate; import com.google.gerrit.server.index.account.AccountIndexCollection; @@ -44,6 +45,7 @@ @Inject protected AccountQueryProcessor( Provider<CurrentUser> userProvider, + CapabilityControl.Factory capabilityFactory, Metrics metrics, IndexConfig indexConfig, AccountIndexCollection indexes, @@ -51,6 +53,7 @@ AccountControl.Factory accountControlFactory) { super( userProvider, + capabilityFactory, metrics, AccountSchemaDefinitions.INSTANCE, indexConfig,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java index c2b92aa..70d8484 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/InternalAccountQuery.java
@@ -18,7 +18,7 @@ import com.google.common.collect.Lists; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.account.AccountState; -import com.google.gerrit.server.account.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalId; import com.google.gerrit.server.index.IndexConfig; import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gerrit.server.query.InternalQuery; @@ -68,10 +68,6 @@ return query(AccountPredicates.defaultPredicate(query)); } - public List<AccountState> byEmailPrefix(String emailPrefix) throws OrmException { - return query(AccountPredicates.email(emailPrefix)); - } - public List<AccountState> byExternalId(String scheme, String id) throws OrmException { return byExternalId(ExternalId.Key.create(scheme, id)); } @@ -106,6 +102,10 @@ return query(AccountPredicates.fullName(fullName)); } + public List<AccountState> byPreferredEmail(String email) throws OrmException { + return query(AccountPredicates.preferredEmail(email)); + } + public List<AccountState> byWatchedProject(Project.NameKey project) throws OrmException { return query(AccountPredicates.watchedProject(project)); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AddedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AddedPredicate.java index b3cdd6a..05bf24bd2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AddedPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AddedPredicate.java
@@ -19,7 +19,7 @@ import com.google.gwtorm.server.OrmException; public class AddedPredicate extends IntegerRangeChangePredicate { - AddedPredicate(String value) throws QueryParseException { + public AddedPredicate(String value) throws QueryParseException { super(ChangeField.ADDED, value); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java index 7d51217..b9c4694 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AfterPredicate.java
@@ -20,9 +20,9 @@ import java.util.Date; public class AfterPredicate extends TimestampRangeChangePredicate { - private final Date cut; + protected final Date cut; - AfterPredicate(String value) throws QueryParseException { + public AfterPredicate(String value) throws QueryParseException { super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_BEFORE, value); cut = parse(value); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java index 0cd76bb..6310665 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -25,9 +25,9 @@ import java.sql.Timestamp; public class AgePredicate extends TimestampRangeChangePredicate { - private final long cut; + protected final long cut; - AgePredicate(String value) { + public AgePredicate(String value) { super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_AGE, value); long s = ConfigUtil.getTimeUnit(getValue(), 0, SECONDS); @@ -46,7 +46,7 @@ } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { Change change = object.change(); return change != null && change.getLastUpdatedOn().getTime() <= cut; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.java index 38622ed..63f7467 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AssigneePredicate.java
@@ -18,16 +18,16 @@ import com.google.gerrit.server.index.change.ChangeField; import com.google.gwtorm.server.OrmException; -class AssigneePredicate extends ChangeIndexPredicate { - private final Account.Id id; +public class AssigneePredicate extends ChangeIndexPredicate { + protected final Account.Id id; - AssigneePredicate(Account.Id id) { + public AssigneePredicate(Account.Id id) { super(ChangeField.ASSIGNEE, id.toString()); this.id = id; } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { if (id.get() == ChangeField.NO_ASSIGNEE) { Account.Id assignee = object.change().getAssignee(); return assignee == null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java index dccd17e..3ee3352 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AuthorPredicate.java
@@ -22,7 +22,7 @@ import java.io.IOException; public class AuthorPredicate extends ChangeIndexPredicate { - AuthorPredicate(String value) { + public AuthorPredicate(String value) { super(AUTHOR, FIELD_AUTHOR, value.toLowerCase()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java index 9e443c9..bc57f15 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BeforePredicate.java
@@ -20,9 +20,9 @@ import java.util.Date; public class BeforePredicate extends TimestampRangeChangePredicate { - private final Date cut; + protected final Date cut; - BeforePredicate(String value) throws QueryParseException { + public BeforePredicate(String value) throws QueryParseException { super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_BEFORE, value); cut = parse(value); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BooleanPredicate.java similarity index 80% rename from gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java rename to gerrit-server/src/main/java/com/google/gerrit/server/query/change/BooleanPredicate.java index d998fa3..31e3ee1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergeablePredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BooleanPredicate.java
@@ -14,15 +14,15 @@ package com.google.gerrit.server.query.change; +import com.google.gerrit.server.index.FieldDef; import com.google.gerrit.server.index.FieldDef.FillArgs; -import com.google.gerrit.server.index.change.ChangeField; import com.google.gwtorm.server.OrmException; -class IsMergeablePredicate extends ChangeIndexPredicate { - private final FillArgs args; +public class BooleanPredicate extends ChangeIndexPredicate { + protected final FillArgs args; - IsMergeablePredicate(FillArgs args) { - super(ChangeField.MERGEABLE, "1"); + public BooleanPredicate(FieldDef<ChangeData, String> field, FillArgs args) { + super(field, "1"); this.args = args; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java index b62e3eb..6bc6573 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -33,6 +33,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.gerrit.common.Nullable; +import com.google.gerrit.common.data.LabelTypes; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.common.data.SubmitTypeRecord; import com.google.gerrit.reviewdb.client.Account; @@ -52,10 +53,12 @@ import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.ReviewerByEmailSet; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.ReviewerStatusUpdate; import com.google.gerrit.server.StarredChangesUtil; import com.google.gerrit.server.StarredChangesUtil.StarRef; +import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.change.MergeabilityCache; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MergeUtil; @@ -300,7 +303,7 @@ ChangeData cd = new ChangeData( null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, project, id); + null, null, project, id); cd.currentPatchSet = new PatchSet(new PatchSet.Id(id, currentPatchSetId)); return cd; } @@ -310,6 +313,7 @@ private final GitRepositoryManager repoManager; private final ChangeControl.GenericFactory changeControlFactory; private final IdentifiedUser.GenericFactory userFactory; + private final AccountCache accountCache; private final ProjectCache projectCache; private final MergeUtil.Factory mergeUtilFactory; private final ChangeNotes.Factory notesFactory; @@ -352,6 +356,9 @@ private StarsOf starsOf; private ImmutableMap<Account.Id, StarRef> starRefs; private ReviewerSet reviewers; + private ReviewerByEmailSet reviewersByEmail; + private ReviewerSet pendingReviewers; + private ReviewerByEmailSet pendingReviewersByEmail; private List<ReviewerStatusUpdate> reviewerUpdates; private PersonIdent author; private PersonIdent committer; @@ -365,6 +372,7 @@ GitRepositoryManager repoManager, ChangeControl.GenericFactory changeControlFactory, IdentifiedUser.GenericFactory userFactory, + AccountCache accountCache, ProjectCache projectCache, MergeUtil.Factory mergeUtilFactory, ChangeNotes.Factory notesFactory, @@ -383,6 +391,7 @@ this.repoManager = repoManager; this.changeControlFactory = changeControlFactory; this.userFactory = userFactory; + this.accountCache = accountCache; this.projectCache = projectCache; this.mergeUtilFactory = mergeUtilFactory; this.notesFactory = notesFactory; @@ -403,6 +412,7 @@ GitRepositoryManager repoManager, ChangeControl.GenericFactory changeControlFactory, IdentifiedUser.GenericFactory userFactory, + AccountCache accountCache, ProjectCache projectCache, MergeUtil.Factory mergeUtilFactory, ChangeNotes.Factory notesFactory, @@ -420,6 +430,7 @@ this.repoManager = repoManager; this.changeControlFactory = changeControlFactory; this.userFactory = userFactory; + this.accountCache = accountCache; this.projectCache = projectCache; this.mergeUtilFactory = mergeUtilFactory; this.notesFactory = notesFactory; @@ -441,6 +452,7 @@ GitRepositoryManager repoManager, ChangeControl.GenericFactory changeControlFactory, IdentifiedUser.GenericFactory userFactory, + AccountCache accountCache, ProjectCache projectCache, MergeUtil.Factory mergeUtilFactory, ChangeNotes.Factory notesFactory, @@ -458,6 +470,7 @@ this.repoManager = repoManager; this.changeControlFactory = changeControlFactory; this.userFactory = userFactory; + this.accountCache = accountCache; this.projectCache = projectCache; this.mergeUtilFactory = mergeUtilFactory; this.notesFactory = notesFactory; @@ -480,6 +493,7 @@ GitRepositoryManager repoManager, ChangeControl.GenericFactory changeControlFactory, IdentifiedUser.GenericFactory userFactory, + AccountCache accountCache, ProjectCache projectCache, MergeUtil.Factory mergeUtilFactory, ChangeNotes.Factory notesFactory, @@ -497,6 +511,7 @@ this.repoManager = repoManager; this.changeControlFactory = changeControlFactory; this.userFactory = userFactory; + this.accountCache = accountCache; this.projectCache = projectCache; this.mergeUtilFactory = mergeUtilFactory; this.notesFactory = notesFactory; @@ -520,6 +535,7 @@ GitRepositoryManager repoManager, ChangeControl.GenericFactory changeControlFactory, IdentifiedUser.GenericFactory userFactory, + AccountCache accountCache, ProjectCache projectCache, MergeUtil.Factory mergeUtilFactory, ChangeNotes.Factory notesFactory, @@ -540,6 +556,7 @@ this.repoManager = repoManager; this.changeControlFactory = changeControlFactory; this.userFactory = userFactory; + this.accountCache = accountCache; this.projectCache = projectCache; this.mergeUtilFactory = mergeUtilFactory; this.notesFactory = notesFactory; @@ -755,6 +772,10 @@ return change; } + public LabelTypes getLabelTypes() throws OrmException { + return changeControl().getLabelTypes(); + } + public ChangeNotes notes() throws OrmException { if (notes == null) { if (!lazyLoad) { @@ -793,7 +814,7 @@ try { currentApprovals = ImmutableList.copyOf( - approvalsUtil.byPatchSet(db, changeControl(), c.currentPatchSetId())); + approvalsUtil.byPatchSet(db, changeControl(), c.currentPatchSetId(), null, null)); } catch (OrmException e) { if (e.getCause() instanceof NoSuchChangeException) { currentApprovals = Collections.emptyList(); @@ -954,6 +975,60 @@ return reviewers; } + public ReviewerByEmailSet reviewersByEmail() throws OrmException { + if (reviewersByEmail == null) { + if (!lazyLoad) { + return ReviewerByEmailSet.empty(); + } + reviewersByEmail = notes().getReviewersByEmail(); + } + return reviewersByEmail; + } + + public void setReviewersByEmail(ReviewerByEmailSet reviewersByEmail) { + this.reviewersByEmail = reviewersByEmail; + } + + public ReviewerByEmailSet getReviewersByEmail() { + return reviewersByEmail; + } + + public void setPendingReviewers(ReviewerSet pendingReviewers) { + this.pendingReviewers = pendingReviewers; + } + + public ReviewerSet getPendingReviewers() { + return this.pendingReviewers; + } + + public ReviewerSet pendingReviewers() throws OrmException { + if (pendingReviewers == null) { + if (!lazyLoad) { + return ReviewerSet.empty(); + } + pendingReviewers = notes().getPendingReviewers(); + } + return pendingReviewers; + } + + public void setPendingReviewersByEmail(ReviewerByEmailSet pendingReviewersByEmail) { + this.pendingReviewersByEmail = pendingReviewersByEmail; + } + + public ReviewerByEmailSet getPendingReviewersByEmail() { + return pendingReviewersByEmail; + } + + public ReviewerByEmailSet pendingReviewersByEmail() throws OrmException { + if (pendingReviewersByEmail == null) { + if (!lazyLoad) { + return ReviewerByEmailSet.empty(); + } + pendingReviewersByEmail = notes().getPendingReviewersByEmail(); + } + return pendingReviewersByEmail; + } + public List<ReviewerStatusUpdate> reviewerUpdates() throws OrmException { if (reviewerUpdates == null) { if (!lazyLoad) { @@ -1029,7 +1104,7 @@ if (!lazyLoad) { return Collections.emptyList(); } - records = new SubmitRuleEvaluator(this).setOptions(options).evaluate(); + records = new SubmitRuleEvaluator(accountCache, this).setOptions(options).evaluate(); submitRecords.put(options, records); } return records; @@ -1046,7 +1121,7 @@ public SubmitTypeRecord submitTypeRecord() throws OrmException { if (submitTypeRecord == null) { - submitTypeRecord = new SubmitRuleEvaluator(this).getSubmitType(); + submitTypeRecord = new SubmitRuleEvaluator(accountCache, this).getSubmitType(); } return submitTypeRecord; } @@ -1065,6 +1140,8 @@ mergeable = true; } else if (c.getStatus() == Change.Status.ABANDONED) { return null; + } else if (c.isWorkInProgress()) { + return null; } else { if (!lazyLoad) { return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java index 85d433a..d541d18 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
@@ -19,8 +19,8 @@ import com.google.gwtorm.server.OrmException; /** Predicate over Change-Id strings (aka Change.Key). */ -class ChangeIdPredicate extends ChangeIndexPredicate { - ChangeIdPredicate(String id) { +public class ChangeIdPredicate extends ChangeIndexPredicate { + public ChangeIdPredicate(String id) { super(ChangeField.ID, ChangeQueryBuilder.FIELD_CHANGE, id); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java index 0604f8b..0362c85 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
@@ -14,9 +14,12 @@ package com.google.gerrit.server.query.change; +import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.index.FieldDef; import com.google.gerrit.server.index.IndexPredicate; import com.google.gerrit.server.query.Matchable; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments; public abstract class ChangeIndexPredicate extends IndexPredicate<ChangeData> implements Matchable<ChangeData> { @@ -27,4 +30,11 @@ protected ChangeIndexPredicate(FieldDef<ChangeData, ?> def, String name, String value) { super(def, name, value); } + + protected static Predicate<ChangeData> create(Arguments args, Predicate<ChangeData> p) { + if (!args.allowsDrafts) { + return Predicate.and(p, Predicate.not(new ChangeStatusPredicate(Change.Status.DRAFT))); + } + return p; + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java index 8db62a7..fa08f53 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -24,13 +24,13 @@ import com.google.gwtorm.server.OrmException; import com.google.inject.Provider; -class ChangeIsVisibleToPredicate extends IsVisibleToPredicate<ChangeData> { - private final Provider<ReviewDb> db; - private final ChangeNotes.Factory notesFactory; - private final ChangeControl.GenericFactory changeControl; - private final CurrentUser user; +public class ChangeIsVisibleToPredicate extends IsVisibleToPredicate<ChangeData> { + protected final Provider<ReviewDb> db; + protected final ChangeNotes.Factory notesFactory; + protected final ChangeControl.GenericFactory changeControl; + protected final CurrentUser user; - ChangeIsVisibleToPredicate( + public ChangeIsVisibleToPredicate( Provider<ReviewDb> db, ChangeNotes.Factory notesFactory, ChangeControl.GenericFactory changeControlFactory, @@ -43,7 +43,7 @@ } @Override - public boolean match(final ChangeData cd) throws OrmException { + public boolean match(ChangeData cd) throws OrmException { if (cd.fastIsVisibleTo(user)) { return true; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java index 5c3e8c9..161233e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -41,7 +41,6 @@ import com.google.gerrit.server.StarredChangesUtil; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountResolver; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.GroupBackends; import com.google.gerrit.server.account.VersionedAccountDestinations; @@ -57,13 +56,17 @@ import com.google.gerrit.server.index.FieldDef; import com.google.gerrit.server.index.IndexConfig; import com.google.gerrit.server.index.Schema; +import com.google.gerrit.server.index.SchemaUtil; import com.google.gerrit.server.index.change.ChangeField; import com.google.gerrit.server.index.change.ChangeIndex; import com.google.gerrit.server.index.change.ChangeIndexCollection; import com.google.gerrit.server.index.change.ChangeIndexRewriter; +import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.NotesMigration; +import com.google.gerrit.server.notedb.ReviewerStateInternal; import com.google.gerrit.server.patch.PatchListCache; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ListChildProjects; import com.google.gerrit.server.project.ProjectCache; @@ -82,9 +85,12 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Config; @@ -115,6 +121,8 @@ private static final Pattern DEF_CHANGE = Pattern.compile("^(?:[1-9][0-9]*|(?:[^~]+~[^~]+~)?[iI][0-9a-f]{4,}.*)$"); + static final int MAX_ACCOUNTS_PER_DEFAULT_FIELD = 10; + // NOTE: As new search operations are added, please keep the // SearchSuggestOracle up to date. @@ -122,6 +130,7 @@ public static final String FIELD_AGE = "age"; public static final String FIELD_ASSIGNEE = "assignee"; public static final String FIELD_AUTHOR = "author"; + public static final String FIELD_EXACTAUTHOR = "exactauthor"; public static final String FIELD_BEFORE = "before"; public static final String FIELD_CHANGE = "change"; public static final String FIELD_CHANGE_ID = "change_id"; @@ -129,6 +138,7 @@ public static final String FIELD_COMMENTBY = "commentby"; public static final String FIELD_COMMIT = "commit"; public static final String FIELD_COMMITTER = "committer"; + public static final String FIELD_EXACTCOMMITTER = "exactcommitter"; public static final String FIELD_CONFLICTS = "conflicts"; public static final String FIELD_DELETED = "deleted"; public static final String FIELD_DELTA = "delta"; @@ -149,6 +159,9 @@ public static final String FIELD_OWNERIN = "ownerin"; public static final String FIELD_PARENTPROJECT = "parentproject"; public static final String FIELD_PATH = "path"; + public static final String FIELD_PENDING_REVIEWER = "pendingreviewer"; + public static final String FIELD_PENDING_REVIEWER_BY_EMAIL = "pendingreviewerbyemail"; + public static final String FIELD_PRIVATE = "private"; public static final String FIELD_PROJECT = "project"; public static final String FIELD_PROJECTS = "projects"; public static final String FIELD_REF = "ref"; @@ -158,12 +171,14 @@ public static final String FIELD_STAR = "star"; public static final String FIELD_STARBY = "starby"; public static final String FIELD_STARREDBY = "starredby"; + public static final String FIELD_STARTED = "started"; public static final String FIELD_STATUS = "status"; public static final String FIELD_SUBMISSIONID = "submissionid"; public static final String FIELD_TR = "tr"; public static final String FIELD_UNRESOLVED_COMMENT_COUNT = "unresolved"; public static final String FIELD_VISIBLETO = "visibleto"; public static final String FIELD_WATCHEDBY = "watchedby"; + public static final String FIELD_WIP = "wip"; public static final String ARG_ID_USER = "user"; public static final String ARG_ID_GROUP = "group"; @@ -179,7 +194,7 @@ final AccountResolver accountResolver; final AllProjectsName allProjectsName; final AllUsersName allUsersName; - final CapabilityControl.Factory capabilityControlFactory; + final PermissionBackend permissionBackend; final ChangeControl.GenericFactory changeControlGenericFactory; final ChangeData.Factory changeDataFactory; final ChangeIndex index; @@ -218,7 +233,7 @@ DynamicMap<ChangeHasOperandFactory> hasOperands, IdentifiedUser.GenericFactory userFactory, Provider<CurrentUser> self, - CapabilityControl.Factory capabilityControlFactory, + PermissionBackend permissionBackend, ChangeControl.GenericFactory changeControlGenericFactory, ChangeNotes.Factory notesFactory, ChangeData.Factory changeDataFactory, @@ -250,7 +265,7 @@ hasOperands, userFactory, self, - capabilityControlFactory, + permissionBackend, changeControlGenericFactory, notesFactory, changeDataFactory, @@ -284,7 +299,7 @@ DynamicMap<ChangeHasOperandFactory> hasOperands, IdentifiedUser.GenericFactory userFactory, Provider<CurrentUser> self, - CapabilityControl.Factory capabilityControlFactory, + PermissionBackend permissionBackend, ChangeControl.GenericFactory changeControlGenericFactory, ChangeNotes.Factory notesFactory, ChangeData.Factory changeDataFactory, @@ -314,7 +329,7 @@ this.opFactories = opFactories; this.userFactory = userFactory; this.self = self; - this.capabilityControlFactory = capabilityControlFactory; + this.permissionBackend = permissionBackend; this.notesFactory = notesFactory; this.changeControlGenericFactory = changeControlGenericFactory; this.changeDataFactory = changeDataFactory; @@ -350,7 +365,7 @@ hasOperands, userFactory, Providers.of(otherUser), - capabilityControlFactory, + permissionBackend, changeControlGenericFactory, notesFactory, changeDataFactory, @@ -561,6 +576,11 @@ } if ("reviewer".equalsIgnoreCase(value)) { + if (args.getSchema().hasField(ChangeField.WIP)) { + return Predicate.and( + Predicate.not(new BooleanPredicate(ChangeField.WIP, args.fillArgs)), + ReviewerPredicate.reviewer(args, self())); + } return ReviewerPredicate.reviewer(args, self()); } @@ -569,7 +589,15 @@ } if ("mergeable".equalsIgnoreCase(value)) { - return new IsMergeablePredicate(args.fillArgs); + return new BooleanPredicate(ChangeField.MERGEABLE, args.fillArgs); + } + + if ("private".equalsIgnoreCase(value)) { + if (args.getSchema().hasField(ChangeField.PRIVATE)) { + return new BooleanPredicate(ChangeField.PRIVATE, args.fillArgs); + } + throw new QueryParseException( + "'is:private' operator is not supported by change index version"); } if ("assigned".equalsIgnoreCase(value)) { @@ -584,6 +612,25 @@ return new SubmittablePredicate(SubmitRecord.Status.OK); } + if ("ignored".equalsIgnoreCase(value)) { + return star("ignore"); + } + + if ("started".equalsIgnoreCase(value)) { + if (args.getSchema().hasField(ChangeField.STARTED)) { + return new BooleanPredicate(ChangeField.STARTED, args.fillArgs); + } + throw new QueryParseException( + "'is:started' operator is not supported by change index version"); + } + + if ("wip".equalsIgnoreCase(value)) { + if (args.getSchema().hasField(ChangeField.WIP)) { + return new BooleanPredicate(ChangeField.WIP, args.fillArgs); + } + throw new QueryParseException("'is:wip' operator is not supported by change index version"); + } + try { return status(value); } catch (IllegalArgumentException e) { @@ -685,7 +732,8 @@ } @Operator - public Predicate<ChangeData> label(String name) throws QueryParseException, OrmException { + public Predicate<ChangeData> label(String name) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { Set<Account.Id> accounts = null; AccountGroup.UUID group = null; @@ -799,7 +847,8 @@ } @Operator - public Predicate<ChangeData> starredby(String who) throws QueryParseException, OrmException { + public Predicate<ChangeData> starredby(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { return starredby(parseAccount(who)); } @@ -816,7 +865,8 @@ } @Operator - public Predicate<ChangeData> watchedby(String who) throws QueryParseException, OrmException { + public Predicate<ChangeData> watchedby(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { Set<Account.Id> m = parseAccount(who); List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size()); @@ -840,7 +890,8 @@ } @Operator - public Predicate<ChangeData> draftby(String who) throws QueryParseException, OrmException { + public Predicate<ChangeData> draftby(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { Set<Account.Id> m = parseAccount(who); List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size()); for (Account.Id id : m) { @@ -853,9 +904,14 @@ return new HasDraftByPredicate(who); } + private boolean isSelf(String who) { + return "self".equals(who) || "me".equals(who); + } + @Operator - public Predicate<ChangeData> visibleto(String who) throws QueryParseException, OrmException { - if ("self".equals(who)) { + public Predicate<ChangeData> visibleto(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { + if (isSelf(who)) { return is_visible(); } Set<Account.Id> m = args.accountResolver.findAll(args.db.get(), who); @@ -875,7 +931,7 @@ for (GroupReference ref : suggestions) { ids.add(ref.getUUID()); } - return visibleto(new SingleGroupUser(args.capabilityControlFactory, ids)); + return visibleto(new SingleGroupUser(ids)); } throw error("No user or group matches \"" + who + "\"."); @@ -891,12 +947,14 @@ } @Operator - public Predicate<ChangeData> o(String who) throws QueryParseException, OrmException { + public Predicate<ChangeData> o(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { return owner(who); } @Operator - public Predicate<ChangeData> owner(String who) throws QueryParseException, OrmException { + public Predicate<ChangeData> owner(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { return owner(parseAccount(who)); } @@ -908,8 +966,18 @@ return Predicate.or(p); } + private Predicate<ChangeData> ownerDefaultField(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { + Set<Account.Id> accounts = parseAccount(who); + if (accounts.size() > MAX_ACCOUNTS_PER_DEFAULT_FIELD) { + return Predicate.any(); + } + return owner(accounts); + } + @Operator - public Predicate<ChangeData> assignee(String who) throws QueryParseException, OrmException { + public Predicate<ChangeData> assignee(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { return assignee(parseAccount(who)); } @@ -931,23 +999,40 @@ } @Operator - public Predicate<ChangeData> r(String who) throws QueryParseException, OrmException { + public Predicate<ChangeData> r(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { return reviewer(who); } @Operator - public Predicate<ChangeData> reviewer(String who) throws QueryParseException, OrmException { - return Predicate.or( - parseAccount(who) - .stream() - .map(id -> ReviewerPredicate.reviewer(args, id)) - .collect(toList())); + public Predicate<ChangeData> reviewer(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { + return reviewer(who, false); + } + + private Predicate<ChangeData> reviewerDefaultField(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { + return reviewer(who, true); + } + + private Predicate<ChangeData> reviewer(String who, boolean forDefaultField) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { + Predicate<ChangeData> byState = + reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField); + if (Objects.equals(byState, Predicate.<ChangeData>any())) { + return Predicate.any(); + } + if (args.getSchema().hasField(ChangeField.WIP)) { + return Predicate.and( + Predicate.not(new BooleanPredicate(ChangeField.WIP, args.fillArgs)), byState); + } + return byState; } @Operator - public Predicate<ChangeData> cc(String who) throws QueryParseException, OrmException { - return Predicate.or( - parseAccount(who).stream().map(id -> ReviewerPredicate.cc(args, id)).collect(toList())); + public Predicate<ChangeData> cc(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { + return reviewerByState(who, ReviewerStateInternal.CC, false); } @Operator @@ -999,7 +1084,8 @@ } @Operator - public Predicate<ChangeData> commentby(String who) throws QueryParseException, OrmException { + public Predicate<ChangeData> commentby(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { return commentby(parseAccount(who)); } @@ -1012,7 +1098,8 @@ } @Operator - public Predicate<ChangeData> from(String who) throws QueryParseException, OrmException { + public Predicate<ChangeData> from(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { Set<Account.Id> ownerIds = parseAccount(who); return Predicate.or(owner(ownerIds), commentby(ownerIds)); } @@ -1036,7 +1123,8 @@ } @Operator - public Predicate<ChangeData> reviewedby(String who) throws QueryParseException, OrmException { + public Predicate<ChangeData> reviewedby(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { return IsReviewedPredicate.create(parseAccount(who)); } @@ -1059,13 +1147,21 @@ } @Operator - public Predicate<ChangeData> author(String who) { - return new AuthorPredicate(who); + public Predicate<ChangeData> author(String who) throws QueryParseException { + if (args.getSchema().hasField(ChangeField.EXACT_AUTHOR)) { + return getAuthorOrCommitterPredicate( + who.trim(), ExactAuthorPredicate::new, AuthorPredicate::new); + } + return getAuthorOrCommitterFullTextPredicate(who.trim(), AuthorPredicate::new); } @Operator - public Predicate<ChangeData> committer(String who) { - return new CommitterPredicate(who); + public Predicate<ChangeData> committer(String who) throws QueryParseException { + if (args.getSchema().hasField(ChangeField.EXACT_COMMITTER)) { + return getAuthorOrCommitterPredicate( + who.trim(), ExactCommitterPredicate::new, CommitterPredicate::new); + } + return getAuthorOrCommitterFullTextPredicate(who.trim(), CommitterPredicate::new); } @Operator @@ -1106,19 +1202,25 @@ // Adapt the capacity of this list when adding more default predicates. List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(11); try { - predicates.add(owner(query)); - } catch (OrmException | QueryParseException e) { + Predicate<ChangeData> p = ownerDefaultField(query); + if (!Objects.equals(p, Predicate.<ChangeData>any())) { + predicates.add(p); + } + } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) { // Skip. } try { - predicates.add(reviewer(query)); - } catch (OrmException | QueryParseException e) { + Predicate<ChangeData> p = reviewerDefaultField(query); + if (!Objects.equals(p, Predicate.<ChangeData>any())) { + predicates.add(p); + } + } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) { // Skip. } predicates.add(file(query)); try { predicates.add(label(query)); - } catch (OrmException | QueryParseException e) { + } catch (OrmException | IOException | ConfigInvalidException | QueryParseException e) { // Skip. } predicates.add(commit(query)); @@ -1133,8 +1235,33 @@ return Predicate.or(predicates); } - private Set<Account.Id> parseAccount(String who) throws QueryParseException, OrmException { - if ("self".equals(who)) { + private Predicate<ChangeData> getAuthorOrCommitterPredicate( + String who, + Function<String, Predicate<ChangeData>> exactPredicateFunc, + Function<String, Predicate<ChangeData>> fullPredicateFunc) + throws QueryParseException { + if (Address.tryParse(who) != null) { + return exactPredicateFunc.apply(who); + } + return getAuthorOrCommitterFullTextPredicate(who, fullPredicateFunc); + } + + private Predicate<ChangeData> getAuthorOrCommitterFullTextPredicate( + String who, Function<String, Predicate<ChangeData>> fullPredicateFunc) + throws QueryParseException { + Set<String> parts = SchemaUtil.getNameParts(who); + if (parts.isEmpty()) { + throw error("invalid value"); + } + + List<Predicate<ChangeData>> predicates = + parts.stream().map(fullPredicateFunc).collect(Collectors.toList()); + return Predicate.and(predicates); + } + + private Set<Account.Id> parseAccount(String who) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { + if (isSelf(who)) { return Collections.singleton(self()); } Set<Account.Id> matches = args.accountResolver.findAll(args.db.get(), who); @@ -1176,4 +1303,44 @@ private Account.Id self() throws QueryParseException { return args.getIdentifiedUser().getAccountId(); } + + public Predicate<ChangeData> reviewerByState( + String who, ReviewerStateInternal state, boolean forDefaultField) + throws QueryParseException, OrmException, IOException, ConfigInvalidException { + Predicate<ChangeData> reviewerByEmailPredicate = null; + if (args.index.getSchema().hasField(ChangeField.REVIEWER_BY_EMAIL)) { + Address address = Address.tryParse(who); + if (address != null) { + reviewerByEmailPredicate = ReviewerByEmailPredicate.forState(args, address, state); + } + } + + Predicate<ChangeData> reviewerPredicate = null; + try { + Set<Account.Id> accounts = parseAccount(who); + if (!forDefaultField || accounts.size() <= MAX_ACCOUNTS_PER_DEFAULT_FIELD) { + reviewerPredicate = + Predicate.or( + accounts + .stream() + .map(id -> ReviewerPredicate.forState(args, id, state)) + .collect(toList())); + } + } catch (QueryParseException e) { + // Propagate this exception only if we can't use 'who' to query by email + if (reviewerByEmailPredicate == null) { + throw e; + } + } + + if (reviewerPredicate != null && reviewerByEmailPredicate != null) { + return Predicate.or(reviewerPredicate, reviewerByEmailPredicate); + } else if (reviewerPredicate != null) { + return reviewerPredicate; + } else if (reviewerByEmailPredicate != null) { + return reviewerByEmailPredicate; + } else { + return Predicate.any(); + } + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java index 91a37d5..d8c872d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryProcessor.java
@@ -17,8 +17,11 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_LIMIT; +import com.google.gerrit.extensions.common.PluginDefinedInfo; +import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.index.IndexConfig; import com.google.gerrit.server.index.IndexPredicate; import com.google.gerrit.server.index.QueryOptions; @@ -32,12 +35,26 @@ import com.google.gerrit.server.query.QueryProcessor; import com.google.inject.Inject; import com.google.inject.Provider; +import java.util.ArrayList; +import java.util.List; import java.util.Set; -public class ChangeQueryProcessor extends QueryProcessor<ChangeData> { +public class ChangeQueryProcessor extends QueryProcessor<ChangeData> + implements PluginDefinedAttributesFactory { + /** + * Register a ChangeAttributeFactory in a config Module like this: + * + * <p>bind(ChangeAttributeFactory.class) .annotatedWith(Exports.named("export-name")) + * .to(YourClass.class); + */ + public interface ChangeAttributeFactory { + PluginDefinedInfo create(ChangeData a, ChangeQueryProcessor qp, String plugin); + } + private final Provider<ReviewDb> db; private final ChangeControl.GenericFactory changeControlFactory; private final ChangeNotes.Factory notesFactory; + private final DynamicMap<ChangeAttributeFactory> attributeFactories; static { // It is assumed that basic rewrites do not touch visibleto predicates. @@ -49,15 +66,18 @@ @Inject ChangeQueryProcessor( Provider<CurrentUser> userProvider, + CapabilityControl.Factory capabilityFactory, Metrics metrics, IndexConfig indexConfig, ChangeIndexCollection indexes, ChangeIndexRewriter rewriter, Provider<ReviewDb> db, ChangeControl.GenericFactory changeControlFactory, - ChangeNotes.Factory notesFactory) { + ChangeNotes.Factory notesFactory, + DynamicMap<ChangeAttributeFactory> attributeFactories) { super( userProvider, + capabilityFactory, metrics, ChangeSchemaDefinitions.INSTANCE, indexConfig, @@ -67,6 +87,7 @@ this.db = db; this.changeControlFactory = changeControlFactory; this.notesFactory = notesFactory; + this.attributeFactories = attributeFactories; } @Override @@ -82,6 +103,30 @@ } @Override + public List<PluginDefinedInfo> create(ChangeData cd) { + List<PluginDefinedInfo> plugins = new ArrayList<>(attributeFactories.plugins().size()); + for (String plugin : attributeFactories.plugins()) { + for (Provider<ChangeAttributeFactory> provider : + attributeFactories.byPlugin(plugin).values()) { + PluginDefinedInfo pda = null; + try { + pda = provider.get().create(cd, this, plugin); + } catch (RuntimeException e) { + /* Eat runtime exceptions so that queries don't fail. */ + } + if (pda != null) { + pda.name = plugin; + plugins.add(pda); + } + } + } + if (plugins.isEmpty()) { + plugins = null; + } + return plugins; + } + + @Override protected Predicate<ChangeData> enforceVisibility(Predicate<ChangeData> pred) { return new AndChangeSource( pred,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java index 9c16777..e9564bd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -36,9 +36,9 @@ * <p>Status names are looked up by prefix case-insensitively. */ public final class ChangeStatusPredicate extends ChangeIndexPredicate { - private static final TreeMap<String, Predicate<ChangeData>> PREDICATES; - private static final Predicate<ChangeData> CLOSED; - private static final Predicate<ChangeData> OPEN; + protected static final TreeMap<String, Predicate<ChangeData>> PREDICATES; + protected static final Predicate<ChangeData> CLOSED; + protected static final Predicate<ChangeData> OPEN; static { PREDICATES = new TreeMap<>(); @@ -84,9 +84,9 @@ return CLOSED; } - private final Change.Status status; + protected final Change.Status status; - ChangeStatusPredicate(Change.Status status) { + public ChangeStatusPredicate(Change.Status status) { super(ChangeField.STATUS, canonicalize(status)); this.status = status; } @@ -96,7 +96,7 @@ } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { Change change = object.change(); return change != null && status.equals(change.getStatus()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java index 668c6f2..7ad7afe 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentByPredicate.java
@@ -21,10 +21,10 @@ import com.google.gwtorm.server.OrmException; import java.util.Objects; -class CommentByPredicate extends ChangeIndexPredicate { - private final Account.Id id; +public class CommentByPredicate extends ChangeIndexPredicate { + protected final Account.Id id; - CommentByPredicate(Account.Id id) { + public CommentByPredicate(Account.Id id) { super(ChangeField.COMMENTBY, id.toString()); this.id = id; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java index 4779a16..85efe90 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -21,10 +21,10 @@ import com.google.gerrit.server.query.QueryParseException; import com.google.gwtorm.server.OrmException; -class CommentPredicate extends ChangeIndexPredicate { - private final ChangeIndex index; +public class CommentPredicate extends ChangeIndexPredicate { + protected final ChangeIndex index; - CommentPredicate(ChangeIndex index, String value) { + public CommentPredicate(ChangeIndex index, String value) { super(ChangeField.COMMENT, value); this.index = index; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java index 1188d5d..d2537ca 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -22,7 +22,7 @@ import com.google.gerrit.server.index.FieldDef; import com.google.gwtorm.server.OrmException; -class CommitPredicate extends ChangeIndexPredicate { +public class CommitPredicate extends ChangeIndexPredicate { static FieldDef<ChangeData, ?> commitField(String id) { if (id.length() == OBJECT_ID_STRING_LENGTH) { return EXACT_COMMIT; @@ -30,12 +30,12 @@ return COMMIT; } - CommitPredicate(String id) { + public CommitPredicate(String id) { super(commitField(id), id); } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { String id = getValue().toLowerCase(); for (PatchSet p : object.patchSets()) { if (equals(p, id)) { @@ -45,7 +45,7 @@ return false; } - private boolean equals(PatchSet p, String id) { + protected boolean equals(PatchSet p, String id) { boolean exact = getField() == EXACT_COMMIT; String rev = p.getRevision() != null ? p.getRevision().get() : null; return (exact && id.equals(rev)) || (!exact && rev != null && rev.startsWith(id));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java index cd1f3b2..797cb9d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitterPredicate.java
@@ -22,7 +22,7 @@ import java.io.IOException; public class CommitterPredicate extends ChangeIndexPredicate { - CommitterPredicate(String value) { + public CommitterPredicate(String value) { super(COMMITTER, FIELD_COMMITTER, value.toLowerCase()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java index 9b45890..9e8f77b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -45,26 +45,26 @@ import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.TreeFilter; -class ConflictsPredicate extends OrPredicate<ChangeData> { +public class ConflictsPredicate extends OrPredicate<ChangeData> { // UI code may depend on this string, so use caution when changing. - private static final String TOO_MANY_FILES = "too many files to find conflicts"; + protected static final String TOO_MANY_FILES = "too many files to find conflicts"; - private final String value; + protected final String value; - ConflictsPredicate(Arguments args, String value, List<Change> changes) + public ConflictsPredicate(Arguments args, String value, List<Change> changes) throws QueryParseException, OrmException { super(predicates(args, value, changes)); this.value = value; } - private static List<Predicate<ChangeData>> predicates( + public static List<Predicate<ChangeData>> predicates( final Arguments args, String value, List<Change> changes) throws QueryParseException, OrmException { int indexTerms = 0; List<Predicate<ChangeData>> changePredicates = Lists.newArrayListWithCapacity(changes.size()); final Provider<ReviewDb> db = args.db; - for (final Change c : changes) { + for (Change c : changes) { final ChangeDataCache changeDataCache = new ChangeDataCache(c, db, args.changeDataFactory, args.projectCache); List<String> files = listFiles(c, args, changeDataCache); @@ -160,7 +160,7 @@ return changePredicates; } - private static List<String> listFiles(Change c, Arguments args, ChangeDataCache changeDataCache) + public static List<String> listFiles(Change c, Arguments args, ChangeDataCache changeDataCache) throws OrmException { try (Repository repo = args.repoManager.openRepository(c.getProject()); RevWalk rw = new RevWalk(repo)) { @@ -200,17 +200,17 @@ return ChangeQueryBuilder.FIELD_CONFLICTS + ":" + value; } - private static class ChangeDataCache { - private final Change change; - private final Provider<ReviewDb> db; - private final ChangeData.Factory changeDataFactory; - private final ProjectCache projectCache; + public static class ChangeDataCache { + protected final Change change; + protected final Provider<ReviewDb> db; + protected final ChangeData.Factory changeDataFactory; + protected final ProjectCache projectCache; - private ObjectId testAgainst; - private ProjectState projectState; - private Iterable<ObjectId> alreadyAccepted; + protected ObjectId testAgainst; + protected ProjectState projectState; + protected Iterable<ObjectId> alreadyAccepted; - ChangeDataCache( + public ChangeDataCache( Change change, Provider<ReviewDb> db, ChangeData.Factory changeDataFactory, @@ -221,7 +221,7 @@ this.projectCache = projectCache; } - ObjectId getTestAgainst() throws OrmException { + protected ObjectId getTestAgainst() throws OrmException { if (testAgainst == null) { testAgainst = ObjectId.fromString( @@ -230,7 +230,7 @@ return testAgainst; } - ProjectState getProjectState() { + protected ProjectState getProjectState() { if (projectState == null) { projectState = projectCache.get(change.getProject()); if (projectState == null) { @@ -240,7 +240,7 @@ return projectState; } - Iterable<ObjectId> getAlreadyAccepted(Repository repo) throws IOException { + protected Iterable<ObjectId> getAlreadyAccepted(Repository repo) throws IOException { if (alreadyAccepted == null) { alreadyAccepted = SubmitDryRun.getAlreadyAccepted(repo); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeletedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeletedPredicate.java index 9e49269..9c46da8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeletedPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeletedPredicate.java
@@ -19,7 +19,7 @@ import com.google.gwtorm.server.OrmException; public class DeletedPredicate extends IntegerRangeChangePredicate { - DeletedPredicate(String value) throws QueryParseException { + public DeletedPredicate(String value) throws QueryParseException { super(ChangeField.DELETED, value); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeltaPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeltaPredicate.java index ce33225..68a4b84 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeltaPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DeltaPredicate.java
@@ -19,7 +19,7 @@ import com.google.gwtorm.server.OrmException; public class DeltaPredicate extends IntegerRangeChangePredicate { - DeltaPredicate(String value) throws QueryParseException { + public DeltaPredicate(String value) throws QueryParseException { super(ChangeField.DELTA, value); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java index 809e7a1..7f969e1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/DestinationPredicate.java
@@ -19,16 +19,16 @@ import com.google.gwtorm.server.OrmException; import java.util.Set; -class DestinationPredicate extends ChangeOperatorPredicate { - Set<Branch.NameKey> destinations; +public class DestinationPredicate extends ChangeOperatorPredicate { + protected Set<Branch.NameKey> destinations; - DestinationPredicate(Set<Branch.NameKey> destinations, String value) { + public DestinationPredicate(Set<Branch.NameKey> destinations, String value) { super(ChangeQueryBuilder.FIELD_DESTINATION, value); this.destinations = destinations; } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { Change change = object.change(); if (change == null) { return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EditByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EditByPredicate.java index 8be5235..3238dc9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EditByPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EditByPredicate.java
@@ -18,10 +18,10 @@ import com.google.gerrit.server.index.change.ChangeField; import com.google.gwtorm.server.OrmException; -class EditByPredicate extends ChangeIndexPredicate { - private final Account.Id id; +public class EditByPredicate extends ChangeIndexPredicate { + protected final Account.Id id; - EditByPredicate(Account.Id id) { + public EditByPredicate(Account.Id id) { super(ChangeField.EDITBY, id.toString()); this.id = id; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java index fb6c56b..66958695 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
@@ -19,8 +19,8 @@ import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments; import com.google.gwtorm.server.OrmException; -class EqualsFilePredicate extends ChangeIndexPredicate { - static Predicate<ChangeData> create(Arguments args, String value) { +public class EqualsFilePredicate extends ChangeIndexPredicate { + public static Predicate<ChangeData> create(Arguments args, String value) { Predicate<ChangeData> eqPath = new EqualsPathPredicate(ChangeQueryBuilder.FIELD_FILE, value); if (!args.getSchema().hasField(ChangeField.FILE_PART)) { return eqPath; @@ -28,11 +28,8 @@ return Predicate.or(eqPath, new EqualsFilePredicate(value)); } - private final String value; - private EqualsFilePredicate(String value) { super(ChangeField.FILE_PART, ChangeQueryBuilder.FIELD_FILE, value); - this.value = value; } @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java index 1189e87..1917d6f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -16,7 +16,6 @@ import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; -import com.google.gerrit.common.data.Permission; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Change; @@ -24,26 +23,28 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.index.change.ChangeField; -import com.google.gerrit.server.project.ChangeControl; -import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gerrit.server.permissions.ChangePermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectState; import com.google.gwtorm.server.OrmException; import com.google.inject.Provider; -class EqualsLabelPredicate extends ChangeIndexPredicate { - private final ProjectCache projectCache; - private final ChangeControl.GenericFactory ccFactory; - private final IdentifiedUser.GenericFactory userFactory; - private final Provider<ReviewDb> dbProvider; - private final String label; - private final int expVal; - private final Account.Id account; - private final AccountGroup.UUID group; +public class EqualsLabelPredicate extends ChangeIndexPredicate { + protected final ProjectCache projectCache; + protected final PermissionBackend permissionBackend; + protected final IdentifiedUser.GenericFactory userFactory; + protected final Provider<ReviewDb> dbProvider; + protected final String label; + protected final int expVal; + protected final Account.Id account; + protected final AccountGroup.UUID group; - EqualsLabelPredicate(LabelPredicate.Args args, String label, int expVal, Account.Id account) { - super(args.field, ChangeField.formatLabel(label, expVal, account)); - this.ccFactory = args.ccFactory; + public EqualsLabelPredicate( + LabelPredicate.Args args, String label, int expVal, Account.Id account) { + super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account)); + this.permissionBackend = args.permissionBackend; this.projectCache = args.projectCache; this.userFactory = args.userFactory; this.dbProvider = args.dbProvider; @@ -78,7 +79,7 @@ for (PatchSetApproval p : object.currentApprovals()) { if (labelType.matches(p)) { hasVote = true; - if (match(c, p.getValue(), p.getAccountId(), labelType)) { + if (match(object, p.getValue(), p.getAccountId(), labelType)) { return true; } } @@ -91,7 +92,7 @@ return false; } - private static LabelType type(LabelTypes types, String toFind) { + protected static LabelType type(LabelTypes types, String toFind) { if (types.byLabel(toFind) != null) { return types.byLabel(toFind); } @@ -104,40 +105,28 @@ return null; } - private boolean match(Change change, int value, Account.Id approver, LabelType type) - throws OrmException { - int psVal = value; - if (psVal == expVal) { - // Double check the value is still permitted for the user. - // - IdentifiedUser reviewer = userFactory.create(approver); - try { - ChangeControl cc = ccFactory.controlFor(dbProvider.get(), change, reviewer); - if (!cc.isVisible(dbProvider.get())) { - // The user can't see the change anymore. - // - return false; - } - psVal = cc.getRange(Permission.forLabel(type.getName())).squash(psVal); - } catch (NoSuchChangeException e) { - // The project has disappeared. - // - return false; - } - - if (account != null && !account.equals(approver)) { - return false; - } - - if (group != null && !reviewer.getEffectiveGroups().contains(group)) { - return false; - } - - if (psVal == expVal) { - return true; - } + protected boolean match(ChangeData cd, short value, Account.Id approver, LabelType type) { + if (value != expVal) { + return false; } - return false; + + if (account != null && !account.equals(approver)) { + return false; + } + + IdentifiedUser reviewer = userFactory.create(approver); + if (group != null && !reviewer.getEffectiveGroups().contains(group)) { + return false; + } + + // Double check the value is still permitted for the user. + try { + PermissionBackend.ForChange perm = + permissionBackend.user(reviewer).database(dbProvider).change(cd); + return perm.test(ChangePermission.READ) && expVal == perm.squashByTest(type, value); + } catch (PermissionBackendException e) { + return false; + } } @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java index 9d841f3..56ed797 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
@@ -19,12 +19,9 @@ import java.util.Collections; import java.util.List; -class EqualsPathPredicate extends ChangeIndexPredicate { - private final String value; - - EqualsPathPredicate(String fieldName, String value) { +public class EqualsPathPredicate extends ChangeIndexPredicate { + public EqualsPathPredicate(String fieldName, String value) { super(ChangeField.PATH, fieldName, value); - this.value = value; } @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java new file mode 100644 index 0000000..bca5d3b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
@@ -0,0 +1,43 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.query.change; + +import static com.google.gerrit.server.index.change.ChangeField.EXACT_AUTHOR; +import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_EXACTAUTHOR; + +import com.google.gerrit.server.index.change.ChangeField; +import com.google.gwtorm.server.OrmException; +import java.io.IOException; +import java.util.Locale; + +public class ExactAuthorPredicate extends ChangeIndexPredicate { + public ExactAuthorPredicate(String value) { + super(EXACT_AUTHOR, FIELD_EXACTAUTHOR, value.toLowerCase(Locale.US)); + } + + @Override + public boolean match(ChangeData object) throws OrmException { + try { + return ChangeField.getAuthorNameAndEmail(object).contains(getValue()); + } catch (IOException e) { + throw new OrmException(e); + } + } + + @Override + public int getCost() { + return 1; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java new file mode 100644 index 0000000..3fae5e5 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
@@ -0,0 +1,43 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.query.change; + +import static com.google.gerrit.server.index.change.ChangeField.EXACT_COMMITTER; +import static com.google.gerrit.server.query.change.ChangeQueryBuilder.FIELD_EXACTCOMMITTER; + +import com.google.gerrit.server.index.change.ChangeField; +import com.google.gwtorm.server.OrmException; +import java.io.IOException; +import java.util.Locale; + +public class ExactCommitterPredicate extends ChangeIndexPredicate { + public ExactCommitterPredicate(String value) { + super(EXACT_COMMITTER, FIELD_EXACTCOMMITTER, value.toLowerCase(Locale.US)); + } + + @Override + public boolean match(ChangeData object) throws OrmException { + try { + return ChangeField.getCommitterNameAndEmail(object).contains(getValue()); + } catch (IOException e) { + throw new OrmException(e); + } + } + + @Override + public int getCost() { + return 1; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java index 510910e..138cce5 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
@@ -19,13 +19,13 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gwtorm.server.OrmException; -class ExactTopicPredicate extends ChangeIndexPredicate { - ExactTopicPredicate(String topic) { +public class ExactTopicPredicate extends ChangeIndexPredicate { + public ExactTopicPredicate(String topic) { super(EXACT_TOPIC, topic); } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { Change change = object.change(); if (change == null) { return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java index 5651544..c6908dc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
@@ -24,16 +24,16 @@ import com.google.gerrit.server.query.QueryParseException; import com.google.gwtorm.server.OrmException; -class FuzzyTopicPredicate extends ChangeIndexPredicate { - private final ChangeIndex index; +public class FuzzyTopicPredicate extends ChangeIndexPredicate { + protected final ChangeIndex index; - FuzzyTopicPredicate(String topic, ChangeIndex index) { + public FuzzyTopicPredicate(String topic, ChangeIndex index) { super(FUZZY_TOPIC, topic); this.index = index; } @Override - public boolean match(final ChangeData cd) throws OrmException { + public boolean match(ChangeData cd) throws OrmException { Change change = cd.change(); if (change == null) { return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java index 54e1c97..d2645dc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/GroupPredicate.java
@@ -19,8 +19,8 @@ import com.google.gwtorm.server.OrmException; import java.util.List; -class GroupPredicate extends ChangeIndexPredicate { - GroupPredicate(String group) { +public class GroupPredicate extends ChangeIndexPredicate { + public GroupPredicate(String group) { super(ChangeField.GROUP, group); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java index 244589c..e422b74 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
@@ -18,10 +18,10 @@ import com.google.gerrit.server.index.change.ChangeField; import com.google.gwtorm.server.OrmException; -class HasDraftByPredicate extends ChangeIndexPredicate { - private final Account.Id accountId; +public class HasDraftByPredicate extends ChangeIndexPredicate { + protected final Account.Id accountId; - HasDraftByPredicate(Account.Id accountId) { + public HasDraftByPredicate(Account.Id accountId) { super(ChangeField.DRAFTBY, accountId.toString()); this.accountId = accountId; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasStarsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasStarsPredicate.java index eb3a137..b17fffd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasStarsPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
@@ -19,9 +19,9 @@ import com.google.gwtorm.server.OrmException; public class HasStarsPredicate extends ChangeIndexPredicate { - private final Account.Id accountId; + protected final Account.Id accountId; - HasStarsPredicate(Account.Id accountId) { + public HasStarsPredicate(Account.Id accountId) { super(ChangeField.STARBY, accountId.toString()); this.accountId = accountId; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HashtagPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HashtagPredicate.java index 4fd4156..bea5688 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HashtagPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HashtagPredicate.java
@@ -18,13 +18,13 @@ import com.google.gerrit.server.index.change.ChangeField; import com.google.gwtorm.server.OrmException; -class HashtagPredicate extends ChangeIndexPredicate { - HashtagPredicate(String hashtag) { +public class HashtagPredicate extends ChangeIndexPredicate { + public HashtagPredicate(String hashtag) { super(ChangeField.HASHTAG, HashtagsUtil.cleanupHashtag(hashtag)); } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { for (String hashtag : object.notes().load().getHashtags()) { if (hashtag.equalsIgnoreCase(getValue())) { return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java index fa2f5fe..42e60a4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -169,7 +169,7 @@ } private Iterable<ChangeData> byCommitsOnBranchNotMergedFromDatabase( - Repository repo, final ReviewDb db, final Branch.NameKey branch, Collection<String> hashes) + Repository repo, ReviewDb db, Branch.NameKey branch, Collection<String> hashes) throws OrmException, IOException { Set<Change.Id> changeIds = Sets.newHashSetWithExpectedSize(hashes.size()); String lastPrefix = null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java index 50e5bd9..28fb7cf 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsMergePredicate.java
@@ -24,7 +24,7 @@ import org.eclipse.jgit.revwalk.RevWalk; public class IsMergePredicate extends ChangeOperatorPredicate { - private final Arguments args; + protected final Arguments args; public IsMergePredicate(Arguments args, String value) { super(ChangeQueryBuilder.FIELD_MERGE, value);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java index 92de09a..8b6c8e6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
@@ -25,14 +25,14 @@ import java.util.List; import java.util.Set; -class IsReviewedPredicate extends ChangeIndexPredicate { - private static final Account.Id NOT_REVIEWED = new Account.Id(ChangeField.NOT_REVIEWED); +public class IsReviewedPredicate extends ChangeIndexPredicate { + protected static final Account.Id NOT_REVIEWED = new Account.Id(ChangeField.NOT_REVIEWED); - static Predicate<ChangeData> create() { + public static Predicate<ChangeData> create() { return Predicate.not(new IsReviewedPredicate(NOT_REVIEWED)); } - static Predicate<ChangeData> create(Collection<Account.Id> ids) { + public static Predicate<ChangeData> create(Collection<Account.Id> ids) { List<Predicate<ChangeData>> predicates = new ArrayList<>(ids.size()); for (Account.Id id : ids) { predicates.add(new IsReviewedPredicate(id)); @@ -40,7 +40,7 @@ return Predicate.or(predicates); } - private final Account.Id id; + protected final Account.Id id; private IsReviewedPredicate(Account.Id id) { super(REVIEWEDBY, Integer.toString(id.get()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java index 17a6347..e9b2899 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsUnresolvedPredicate.java
@@ -19,11 +19,11 @@ import com.google.gwtorm.server.OrmException; public class IsUnresolvedPredicate extends IntegerRangeChangePredicate { - IsUnresolvedPredicate() throws QueryParseException { + public IsUnresolvedPredicate() throws QueryParseException { this(">0"); } - IsUnresolvedPredicate(String value) throws QueryParseException { + public IsUnresolvedPredicate(String value) throws QueryParseException { super(ChangeField.UNRESOLVED_COMMENT_COUNT, value); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java index dda834b..a1a5070 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -26,23 +26,23 @@ import java.util.Collections; import java.util.List; -class IsWatchedByPredicate extends AndPredicate<ChangeData> { - private static String describe(CurrentUser user) { +public class IsWatchedByPredicate extends AndPredicate<ChangeData> { + protected static String describe(CurrentUser user) { if (user.isIdentifiedUser()) { return user.getAccountId().toString(); } return user.toString(); } - private final CurrentUser user; + protected final CurrentUser user; - IsWatchedByPredicate(ChangeQueryBuilder.Arguments args, boolean checkIsVisible) + public IsWatchedByPredicate(ChangeQueryBuilder.Arguments args, boolean checkIsVisible) throws QueryParseException { super(filters(args, checkIsVisible)); this.user = args.getUser(); } - private static List<Predicate<ChangeData>> filters( + protected static List<Predicate<ChangeData>> filters( ChangeQueryBuilder.Arguments args, boolean checkIsVisible) throws QueryParseException { List<Predicate<ChangeData>> r = new ArrayList<>(); ChangeQueryBuilder builder = new ChangeQueryBuilder(args); @@ -89,7 +89,7 @@ } } - private static Collection<ProjectWatchKey> getWatches(ChangeQueryBuilder.Arguments args) + protected static Collection<ProjectWatchKey> getWatches(ChangeQueryBuilder.Arguments args) throws QueryParseException { CurrentUser user = args.getUser(); if (user.isIdentifiedUser()) { @@ -98,7 +98,7 @@ return Collections.<ProjectWatchKey>emptySet(); } - private static List<Predicate<ChangeData>> none() { + protected static List<Predicate<ChangeData>> none() { Predicate<ChangeData> any = any(); return ImmutableList.of(not(any)); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java index 2fbaa1e..bd342d7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -19,8 +19,7 @@ import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.index.FieldDef; -import com.google.gerrit.server.index.change.ChangeField; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.query.OrPredicate; @@ -34,29 +33,29 @@ import java.util.Set; public class LabelPredicate extends OrPredicate<ChangeData> { - private static final int MAX_LABEL_VALUE = 4; + protected static final int MAX_LABEL_VALUE = 4; - static class Args { - final FieldDef<ChangeData, ?> field; - final ProjectCache projectCache; - final ChangeControl.GenericFactory ccFactory; - final IdentifiedUser.GenericFactory userFactory; - final Provider<ReviewDb> dbProvider; - final String value; - final Set<Account.Id> accounts; - final AccountGroup.UUID group; + protected static class Args { + protected final ProjectCache projectCache; + protected final PermissionBackend permissionBackend; + protected final ChangeControl.GenericFactory ccFactory; + protected final IdentifiedUser.GenericFactory userFactory; + protected final Provider<ReviewDb> dbProvider; + protected final String value; + protected final Set<Account.Id> accounts; + protected final AccountGroup.UUID group; - private Args( - FieldDef<ChangeData, ?> field, + protected Args( ProjectCache projectCache, + PermissionBackend permissionBackend, ChangeControl.GenericFactory ccFactory, IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider, String value, Set<Account.Id> accounts, AccountGroup.UUID group) { - this.field = field; this.projectCache = projectCache; + this.permissionBackend = permissionBackend; this.ccFactory = ccFactory; this.userFactory = userFactory; this.dbProvider = dbProvider; @@ -66,22 +65,21 @@ } } - private static class Parsed { - private final String label; - private final String test; - private final int expVal; + protected static class Parsed { + protected final String label; + protected final String test; + protected final int expVal; - private Parsed(String label, String test, int expVal) { + protected Parsed(String label, String test, int expVal) { this.label = label; this.test = test; this.expVal = expVal; } } - private final String value; + protected final String value; - @SuppressWarnings("deprecation") - LabelPredicate( + public LabelPredicate( ChangeQueryBuilder.Arguments a, String value, Set<Account.Id> accounts, @@ -89,8 +87,8 @@ super( predicates( new Args( - a.getSchema().getField(ChangeField.LABEL2, ChangeField.LABEL).get(), a.projectCache, + a.permissionBackend, a.changeControlGenericFactory, a.userFactory, a.db, @@ -100,7 +98,7 @@ this.value = value; } - private static List<Predicate<ChangeData>> predicates(Args args) { + protected static List<Predicate<ChangeData>> predicates(Args args) { String v = args.value; Parsed parsed = null; @@ -140,14 +138,14 @@ return r; } - private static Predicate<ChangeData> onePredicate(Args args, String label, int expVal) { + protected static Predicate<ChangeData> onePredicate(Args args, String label, int expVal) { if (expVal != 0) { return equalsLabelPredicate(args, label, expVal); } return noLabelQuery(args, label); } - private static Predicate<ChangeData> noLabelQuery(Args args, String label) { + protected static Predicate<ChangeData> noLabelQuery(Args args, String label) { List<Predicate<ChangeData>> r = Lists.newArrayListWithCapacity(2 * MAX_LABEL_VALUE); for (int i = 1; i <= MAX_LABEL_VALUE; i++) { r.add(equalsLabelPredicate(args, label, i)); @@ -156,7 +154,7 @@ return not(or(r)); } - private static Predicate<ChangeData> equalsLabelPredicate(Args args, String label, int expVal) { + protected static Predicate<ChangeData> equalsLabelPredicate(Args args, String label, int expVal) { if (args.accounts == null || args.accounts.isEmpty()) { return new EqualsLabelPredicate(args, label, expVal, null); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java index f7f98d5..fe4d4e1 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -20,7 +20,7 @@ /** Predicate over change number (aka legacy ID or Change.Id). */ public class LegacyChangeIdPredicate extends ChangeIndexPredicate { - private final Change.Id id; + protected final Change.Id id; public LegacyChangeIdPredicate(Change.Id id) { super(LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString()); @@ -28,7 +28,7 @@ } @Override - public boolean match(final ChangeData object) { + public boolean match(ChangeData object) { return id.equals(object.getId()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java index 9e525c2..92d1ed3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -22,10 +22,10 @@ import com.google.gwtorm.server.OrmException; /** Predicate to match changes that contains specified text in commit messages body. */ -class MessagePredicate extends ChangeIndexPredicate { - private final ChangeIndex index; +public class MessagePredicate extends ChangeIndexPredicate { + protected final ChangeIndex index; - MessagePredicate(ChangeIndex index, String value) { + public MessagePredicate(ChangeIndex index, String value) { super(ChangeField.COMMIT_MESSAGE, value); this.index = index; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java index cd98087..d14d8aa 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -23,6 +23,7 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.config.TrackingFooters; import com.google.gerrit.server.data.ChangeAttribute; import com.google.gerrit.server.data.PatchSetAttribute; @@ -68,6 +69,7 @@ } private final ReviewDb db; + private final AccountCache accountCache; private final GitRepositoryManager repoManager; private final ChangeQueryBuilder queryBuilder; private final ChangeQueryProcessor queryProcessor; @@ -92,6 +94,7 @@ @Inject OutputStreamQuery( ReviewDb db, + AccountCache accountCache, GitRepositoryManager repoManager, ChangeQueryBuilder queryBuilder, ChangeQueryProcessor queryProcessor, @@ -99,6 +102,7 @@ TrackingFooters trackingFooters, CurrentUser user) { this.db = db; + this.accountCache = accountCache; this.repoManager = repoManager; this.queryBuilder = queryBuilder; this.queryProcessor = queryProcessor; @@ -244,7 +248,11 @@ if (includeSubmitRecords) { eventFactory.addSubmitRecords( - c, new SubmitRuleEvaluator(d).setAllowClosed(true).setAllowDraft(true).evaluate()); + c, + new SubmitRuleEvaluator(accountCache, d) + .setAllowClosed(true) + .setAllowDraft(true) + .evaluate()); } if (includeCommitMessage) { @@ -313,6 +321,7 @@ eventFactory.addDependencies(rw, c, d.change(), d.currentPatchSet()); } + c.plugins = queryProcessor.create(d); return c; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java index dfaac08..ff494fc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
@@ -19,20 +19,20 @@ import com.google.gerrit.server.index.change.ChangeField; import com.google.gwtorm.server.OrmException; -class OwnerPredicate extends ChangeIndexPredicate { - private final Account.Id id; +public class OwnerPredicate extends ChangeIndexPredicate { + protected final Account.Id id; - OwnerPredicate(Account.Id id) { + public OwnerPredicate(Account.Id id) { super(ChangeField.OWNER, id.toString()); this.id = id; } - Account.Id getAccountId() { + protected Account.Id getAccountId() { return id; } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { Change change = object.change(); return change != null && id.equals(change.getOwner()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java index f3239af..fec7f26 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
@@ -19,22 +19,22 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gwtorm.server.OrmException; -class OwnerinPredicate extends ChangeOperatorPredicate { - private final IdentifiedUser.GenericFactory userFactory; - private final AccountGroup.UUID uuid; +public class OwnerinPredicate extends ChangeOperatorPredicate { + protected final IdentifiedUser.GenericFactory userFactory; + protected final AccountGroup.UUID uuid; - OwnerinPredicate(IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) { + public OwnerinPredicate(IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) { super(ChangeQueryBuilder.FIELD_OWNERIN, uuid.toString()); this.userFactory = userFactory; this.uuid = uuid; } - AccountGroup.UUID getAccountGroupUUID() { + protected AccountGroup.UUID getAccountGroupUUID() { return uuid; } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { final Change change = object.change(); if (change == null) { return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java index d3a3f20..3b00c0a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ParentProjectPredicate.java
@@ -17,6 +17,7 @@ import com.google.gerrit.extensions.common.ProjectInfo; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ListChildProjects; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectResource; @@ -27,11 +28,15 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -class ParentProjectPredicate extends OrPredicate<ChangeData> { - private final String value; +public class ParentProjectPredicate extends OrPredicate<ChangeData> { + private static final Logger log = LoggerFactory.getLogger(ParentProjectPredicate.class); - ParentProjectPredicate( + protected final String value; + + public ParentProjectPredicate( ProjectCache projectCache, Provider<ListChildProjects> listChildProjects, Provider<CurrentUser> self, @@ -40,7 +45,7 @@ this.value = value; } - private static List<Predicate<ChangeData>> predicates( + protected static List<Predicate<ChangeData>> predicates( ProjectCache projectCache, Provider<ListChildProjects> listChildProjects, Provider<CurrentUser> self, @@ -52,10 +57,15 @@ List<Predicate<ChangeData>> r = new ArrayList<>(); r.add(new ProjectPredicate(projectState.getProject().getName())); - ListChildProjects children = listChildProjects.get(); - children.setRecursive(true); - for (ProjectInfo p : children.apply(new ProjectResource(projectState.controlFor(self.get())))) { - r.add(new ProjectPredicate(p.name)); + try { + ProjectResource proj = new ProjectResource(projectState.controlFor(self.get())); + ListChildProjects children = listChildProjects.get(); + children.setRecursive(true); + for (ProjectInfo p : children.apply(proj)) { + r.add(new ProjectPredicate(p.name)); + } + } catch (PermissionBackendException e) { + log.warn("cannot check permissions to expand child projects", e); } return r; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java new file mode 100644 index 0000000..a795025 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/PluginDefinedAttributesFactory.java
@@ -0,0 +1,22 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.query.change; + +import com.google.gerrit.extensions.common.PluginDefinedInfo; +import java.util.List; + +public interface PluginDefinedAttributesFactory { + List<PluginDefinedInfo> create(ChangeData cd); +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java index 644870d..09a46a4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
@@ -19,17 +19,17 @@ import com.google.gerrit.server.index.change.ChangeField; import com.google.gwtorm.server.OrmException; -class ProjectPredicate extends ChangeIndexPredicate { - ProjectPredicate(String id) { +public class ProjectPredicate extends ChangeIndexPredicate { + public ProjectPredicate(String id) { super(ChangeField.PROJECT, id); } - Project.NameKey getValueKey() { + protected Project.NameKey getValueKey() { return new Project.NameKey(getValue()); } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { Change change = object.change(); if (change == null) { return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java index 4c06d1b..28b1302 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
@@ -18,8 +18,8 @@ import com.google.gerrit.server.index.change.ChangeField; import com.google.gwtorm.server.OrmException; -class ProjectPrefixPredicate extends ChangeIndexPredicate { - ProjectPrefixPredicate(String prefix) { +public class ProjectPrefixPredicate extends ChangeIndexPredicate { + public ProjectPrefixPredicate(String prefix) { super(ChangeField.PROJECTS, prefix); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java index 7eccf45..f0ef40d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -137,13 +137,18 @@ int cnt = queries.size(); List<QueryResult<ChangeData>> results = imp.query(qb.parse(queries)); + boolean requireLazyLoad = containsAnyOf(options, ImmutableSet.of(DETAILED_LABELS, LABELS)) && !qb.getArgs().getSchema().hasField(ChangeField.STORED_SUBMIT_RECORD_LENIENT); + + ChangeJson cjson = json.create(options); + cjson.setPluginDefinedAttributesFactory(this.imp); List<List<ChangeInfo>> res = - json.create(options) + cjson .lazyLoad(requireLazyLoad || containsAnyOf(options, ChangeJson.REQUIRE_LAZY_LOAD)) .formatQueryResults(results); + for (int n = 0; n < cnt; n++) { List<ChangeInfo> info = res.get(n); if (results.get(n).more()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java index 491aed9..c9314e4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
@@ -18,13 +18,13 @@ import com.google.gerrit.server.index.change.ChangeField; import com.google.gwtorm.server.OrmException; -class RefPredicate extends ChangeIndexPredicate { - RefPredicate(String ref) { +public class RefPredicate extends ChangeIndexPredicate { + public RefPredicate(String ref) { super(ChangeField.REF, ref); } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { Change change = object.change(); if (change == null) { return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java index 5b9774c..ca21247 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
@@ -19,8 +19,8 @@ import com.google.gwtorm.server.OrmException; import java.util.List; -class RegexPathPredicate extends ChangeRegexPredicate { - RegexPathPredicate(String re) { +public class RegexPathPredicate extends ChangeRegexPredicate { + public RegexPathPredicate(String re) { super(ChangeField.PATH, re); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java index 1284e88..1efc77d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
@@ -21,10 +21,10 @@ import dk.brics.automaton.RegExp; import dk.brics.automaton.RunAutomaton; -class RegexProjectPredicate extends ChangeRegexPredicate { - private final RunAutomaton pattern; +public class RegexProjectPredicate extends ChangeRegexPredicate { + protected final RunAutomaton pattern; - RegexProjectPredicate(String re) { + public RegexProjectPredicate(String re) { super(ChangeField.PROJECT, re); if (re.startsWith("^")) { @@ -39,7 +39,7 @@ } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { Change change = object.change(); if (change == null) { return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java index 671d4cc..92abafb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
@@ -20,10 +20,10 @@ import dk.brics.automaton.RegExp; import dk.brics.automaton.RunAutomaton; -class RegexRefPredicate extends ChangeRegexPredicate { - private final RunAutomaton pattern; +public class RegexRefPredicate extends ChangeRegexPredicate { + protected final RunAutomaton pattern; - RegexRefPredicate(String re) { + public RegexRefPredicate(String re) { super(ChangeField.REF, re); if (re.startsWith("^")) { @@ -38,7 +38,7 @@ } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { Change change = object.change(); if (change == null) { return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java index a4ba059..2b58c88 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -21,10 +21,10 @@ import dk.brics.automaton.RegExp; import dk.brics.automaton.RunAutomaton; -class RegexTopicPredicate extends ChangeRegexPredicate { - private final RunAutomaton pattern; +public class RegexTopicPredicate extends ChangeRegexPredicate { + protected final RunAutomaton pattern; - RegexTopicPredicate(String re) { + public RegexTopicPredicate(String re) { super(EXACT_TOPIC, re); if (re.startsWith("^")) { @@ -39,7 +39,7 @@ } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { Change change = object.change(); if (change == null || change.getTopic() == null) { return false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java new file mode 100644 index 0000000..a040e18 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
@@ -0,0 +1,55 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.query.change; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.gerrit.server.index.change.ChangeField; +import com.google.gerrit.server.mail.Address; +import com.google.gerrit.server.notedb.ReviewerStateInternal; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments; +import com.google.gwtorm.server.OrmException; + +class ReviewerByEmailPredicate extends ChangeIndexPredicate { + + static Predicate<ChangeData> forState(Arguments args, Address adr, ReviewerStateInternal state) { + checkArgument(state != ReviewerStateInternal.REMOVED, "can't query by removed reviewer"); + return create(args, new ReviewerByEmailPredicate(state, adr)); + } + + private final ReviewerStateInternal state; + private final Address adr; + + private ReviewerByEmailPredicate(ReviewerStateInternal state, Address adr) { + super(ChangeField.REVIEWER_BY_EMAIL, ChangeField.getReviewerByEmailFieldValue(state, adr)); + this.state = state; + this.adr = adr; + } + + Address getAddress() { + return adr; + } + + @Override + public boolean match(ChangeData cd) throws OrmException { + return cd.reviewersByEmail().asTable().get(state, adr) != null; + } + + @Override + public int getCost() { + return 1; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java index 6ce02fb..f3a8619 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -14,10 +14,10 @@ package com.google.gerrit.server.query.change; +import static com.google.common.base.Preconditions.checkArgument; import static java.util.stream.Collectors.toList; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.index.change.ChangeField; import com.google.gerrit.server.notedb.ReviewerStateInternal; import com.google.gerrit.server.query.Predicate; @@ -25,8 +25,14 @@ import com.google.gwtorm.server.OrmException; import java.util.stream.Stream; -class ReviewerPredicate extends ChangeIndexPredicate { - static Predicate<ChangeData> reviewer(Arguments args, Account.Id id) { +public class ReviewerPredicate extends ChangeIndexPredicate { + protected static Predicate<ChangeData> forState( + Arguments args, Account.Id id, ReviewerStateInternal state) { + checkArgument(state != ReviewerStateInternal.REMOVED, "can't query by removed reviewer"); + return create(args, new ReviewerPredicate(state, id)); + } + + protected static Predicate<ChangeData> reviewer(Arguments args, Account.Id id) { Predicate<ChangeData> p; if (args.notesMigration.readChanges()) { // With NoteDb, Reviewer/CC are clearly distinct states, so only choose reviewer. @@ -39,14 +45,14 @@ return create(args, p); } - static Predicate<ChangeData> cc(Arguments args, Account.Id id) { + protected static Predicate<ChangeData> cc(Arguments args, Account.Id id) { // As noted above, CC is nebulous without NoteDb, but it certainly doesn't make sense to return // Reviewers for cc:foo. Most likely this will just not match anything, but let the index sort // it out. return create(args, new ReviewerPredicate(ReviewerStateInternal.CC, id)); } - private static Predicate<ChangeData> anyReviewerState(Account.Id id) { + protected static Predicate<ChangeData> anyReviewerState(Account.Id id) { return Predicate.or( Stream.of(ReviewerStateInternal.values()) .filter(s -> s != ReviewerStateInternal.REMOVED) @@ -54,17 +60,8 @@ .collect(toList())); } - private static Predicate<ChangeData> create(Arguments args, Predicate<ChangeData> p) { - if (!args.allowsDrafts) { - // TODO(dborowitz): This really belongs much higher up e.g. QueryProcessor. Also, why are we - // even doing this? - return Predicate.and(p, Predicate.not(new ChangeStatusPredicate(Change.Status.DRAFT))); - } - return p; - } - - private final ReviewerStateInternal state; - private final Account.Id id; + protected final ReviewerStateInternal state; + protected final Account.Id id; private ReviewerPredicate(ReviewerStateInternal state, Account.Id id) { super(ChangeField.REVIEWER, ChangeField.getReviewerFieldValue(state, id)); @@ -72,7 +69,7 @@ this.id = id; } - Account.Id getAccountId() { + protected Account.Id getAccountId() { return id; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java index 63e7859..11f9d89 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
@@ -19,22 +19,22 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gwtorm.server.OrmException; -class ReviewerinPredicate extends ChangeOperatorPredicate { - private final IdentifiedUser.GenericFactory userFactory; - private final AccountGroup.UUID uuid; +public class ReviewerinPredicate extends ChangeOperatorPredicate { + protected final IdentifiedUser.GenericFactory userFactory; + protected final AccountGroup.UUID uuid; - ReviewerinPredicate(IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) { + public ReviewerinPredicate(IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) { super(ChangeQueryBuilder.FIELD_REVIEWERIN, uuid.toString()); this.userFactory = userFactory; this.uuid = uuid; } - AccountGroup.UUID getAccountGroupUUID() { + protected AccountGroup.UUID getAccountGroupUUID() { return uuid; } @Override - public boolean match(final ChangeData object) throws OrmException { + public boolean match(ChangeData object) throws OrmException { for (Account.Id accountId : object.reviewers().all()) { IdentifiedUser reviewer = userFactory.create(accountId); if (reviewer.getEffectiveGroups().contains(uuid)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java index 2661b8b..a084b35 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -14,25 +14,21 @@ package com.google.gerrit.server.query.change; +import com.google.common.collect.ImmutableSet; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.CurrentUser; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.GroupMembership; import com.google.gerrit.server.account.ListGroupMembership; -import java.util.Collections; import java.util.Set; public final class SingleGroupUser extends CurrentUser { private final GroupMembership groups; - public SingleGroupUser( - CapabilityControl.Factory capabilityControlFactory, AccountGroup.UUID groupId) { - this(capabilityControlFactory, Collections.singleton(groupId)); + public SingleGroupUser(AccountGroup.UUID groupId) { + this(ImmutableSet.of(groupId)); } - public SingleGroupUser( - CapabilityControl.Factory capabilityControlFactory, Set<AccountGroup.UUID> groups) { - super(capabilityControlFactory); + public SingleGroupUser(Set<AccountGroup.UUID> groups) { this.groups = new ListGroupMembership(groups); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/StarPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/StarPredicate.java index 98965bf..12d4753 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/StarPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/StarPredicate.java
@@ -20,10 +20,10 @@ import com.google.gwtorm.server.OrmException; public class StarPredicate extends ChangeIndexPredicate { - private final Account.Id accountId; - private final String label; + protected final Account.Id accountId; + protected final String label; - StarPredicate(Account.Id accountId, String label) { + public StarPredicate(Account.Id accountId, String label) { super(ChangeField.STAR, StarredChangesUtil.StarField.create(accountId, label).toString()); this.accountId = accountId; this.label = label;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java index d8d5258..5fdeb68 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
@@ -18,9 +18,8 @@ import com.google.gerrit.server.index.change.ChangeField; import com.google.gwtorm.server.OrmException; -class SubmissionIdPredicate extends ChangeIndexPredicate { - - SubmissionIdPredicate(String changeSet) { +public class SubmissionIdPredicate extends ChangeIndexPredicate { + public SubmissionIdPredicate(String changeSet) { super(ChangeField.SUBMISSIONID, changeSet); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java index 5b01ea2..81d64e0e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
@@ -23,8 +23,8 @@ import com.google.gwtorm.server.OrmException; import java.util.Set; -class SubmitRecordPredicate extends ChangeIndexPredicate { - static Predicate<ChangeData> create( +public class SubmitRecordPredicate extends ChangeIndexPredicate { + public static Predicate<ChangeData> create( String label, SubmitRecord.Label.Status status, Set<Account.Id> accounts) { String lowerLabel = label.toLowerCase(); if (accounts == null || accounts.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java index 0812c6a..df78315 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
@@ -18,10 +18,10 @@ import com.google.gerrit.server.index.change.ChangeField; import com.google.gwtorm.server.OrmException; -class SubmittablePredicate extends ChangeIndexPredicate { - private final SubmitRecord.Status status; +public class SubmittablePredicate extends ChangeIndexPredicate { + protected final SubmitRecord.Status status; - SubmittablePredicate(SubmitRecord.Status status) { + public SubmittablePredicate(SubmitRecord.Status status) { super(ChangeField.SUBMIT_RECORD, status.name()); this.status = status; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java index afaea5c..6a5f260 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
@@ -24,12 +24,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -class TrackingIdPredicate extends ChangeIndexPredicate { +public class TrackingIdPredicate extends ChangeIndexPredicate { private static final Logger log = LoggerFactory.getLogger(TrackingIdPredicate.class); - private final TrackingFooters trackingFooters; + protected final TrackingFooters trackingFooters; - TrackingIdPredicate(TrackingFooters trackingFooters, String trackingId) { + public TrackingIdPredicate(TrackingFooters trackingFooters, String trackingId) { super(ChangeField.TR, trackingId); this.trackingFooters = trackingFooters; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java index 8f72945..3ac9c39 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupIsVisibleToPredicate.java
@@ -23,10 +23,11 @@ import com.google.gwtorm.server.OrmException; public class GroupIsVisibleToPredicate extends IsVisibleToPredicate<AccountGroup> { - private final GroupControl.GenericFactory groupControlFactory; - private final CurrentUser user; + protected final GroupControl.GenericFactory groupControlFactory; + protected final CurrentUser user; - GroupIsVisibleToPredicate(GroupControl.GenericFactory groupControlFactory, CurrentUser user) { + public GroupIsVisibleToPredicate( + GroupControl.GenericFactory groupControlFactory, CurrentUser user) { super(AccountQueryBuilder.FIELD_VISIBLETO, describe(user)); this.groupControlFactory = groupControlFactory; this.user = user;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java index 1cfab20..6229f18 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/group/GroupQueryProcessor.java
@@ -19,6 +19,7 @@ import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.GroupControl; import com.google.gerrit.server.index.IndexConfig; import com.google.gerrit.server.index.IndexPredicate; @@ -44,6 +45,7 @@ @Inject protected GroupQueryProcessor( Provider<CurrentUser> userProvider, + CapabilityControl.Factory capabilityFactory, Metrics metrics, IndexConfig indexConfig, GroupIndexCollection indexes, @@ -51,6 +53,7 @@ GroupControl.GenericFactory groupControlFactory) { super( userProvider, + capabilityFactory, metrics, GroupSchemaDefinitions.INSTANCE, indexConfig,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java index 9a56aa4..dfcacb7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/AllProjectsCreator.java
@@ -222,11 +222,11 @@ private void initSequences(Repository git, BatchRefUpdate bru) throws IOException { if (notesMigration.readChangeSequence() - && git.exactRef(REFS_SEQUENCES + Sequences.CHANGES) == null) { + && git.exactRef(REFS_SEQUENCES + Sequences.NAME_CHANGES) == null) { // Can't easily reuse the inserter from MetaDataUpdate, but this shouldn't slow down site // initialization unduly. try (ObjectInserter ins = git.newObjectInserter()) { - bru.addCommand(RepoSequence.storeNew(ins, Sequences.CHANGES, firstChangeId)); + bru.addCommand(RepoSequence.storeNew(ins, Sequences.NAME_CHANGES, firstChangeId)); ins.flush(); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DB2.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DB2.java index 9b8b736..fcf8c1f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DB2.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DB2.java
@@ -26,7 +26,7 @@ private Config cfg; @Inject - public DB2(@GerritServerConfig final Config cfg) { + public DB2(@GerritServerConfig Config cfg) { super("com.ibm.db2.jcc.DB2Driver"); this.cfg = cfg; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java index 170a5fa..67f6894 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -92,7 +92,7 @@ MULTI_USER } - private DataSource open(final Config cfg, final Context context, final DataSourceType dst) { + private DataSource open(Config cfg, Context context, DataSourceType dst) { ConfigSection dbs = new ConfigSection(cfg, "database"); String driver = dbs.optional("driver"); if (Strings.isNullOrEmpty(driver)) { @@ -161,8 +161,8 @@ } } - private void exportPoolMetrics(final BasicDataSource pool) { - final CallbackMetric1<Boolean, Integer> cnt = + private void exportPoolMetrics(BasicDataSource pool) { + CallbackMetric1<Boolean, Integer> cnt = metrics.newCallbackMetric( "sql/connection_pool/connections", Integer.class, @@ -170,13 +170,10 @@ Field.ofBoolean("active")); metrics.newTrigger( cnt, - new Runnable() { - @Override - public void run() { - synchronized (pool) { - cnt.set(true, pool.getNumActive()); - cnt.set(false, pool.getNumIdle()); - } + () -> { + synchronized (pool) { + cnt.set(true, pool.getNumActive()); + cnt.set(false, pool.getNumIdle()); } }); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java index 3cffdb1..840eaf0 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java
@@ -26,7 +26,7 @@ private final SitePaths site; @Inject - H2(final SitePaths site, @GerritServerConfig final Config cfg) { + H2(SitePaths site, @GerritServerConfig Config cfg) { super("org.h2.Driver"); this.cfg = cfg; this.site = site;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/HANA.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/HANA.java index 26c94e0..f9811c6 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/HANA.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/HANA.java
@@ -17,6 +17,7 @@ import static com.google.gerrit.server.schema.JdbcUtil.hostname; import static com.google.gerrit.server.schema.JdbcUtil.port; +import com.google.common.base.Strings; import com.google.gerrit.server.config.ConfigSection; import com.google.gerrit.server.config.GerritServerConfig; import com.google.inject.Inject; @@ -28,7 +29,7 @@ private Config cfg; @Inject - HANA(@GerritServerConfig final Config cfg) { + HANA(@GerritServerConfig Config cfg) { super("com.sap.db.jdbc.Driver"); this.cfg = cfg; } @@ -39,9 +40,11 @@ final ConfigSection dbs = new ConfigSection(cfg, "database"); b.append("jdbc:sap://"); b.append(hostname(dbs.required("hostname"))); - int instance = Integer.parseInt(dbs.required("instance")); - String port = "3" + String.format("%02d", instance) + "15"; - b.append(port(port)); + b.append(port(dbs.optional("port"))); + String database = dbs.optional("database"); + if (!Strings.isNullOrEmpty(database)) { + b.append("?databaseName=").append(database); + } return b.toString(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JDBC.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JDBC.java index a1df850..d188df4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JDBC.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JDBC.java
@@ -24,7 +24,7 @@ protected final Config cfg; @Inject - JDBC(@GerritServerConfig final Config cfg) { + JDBC(@GerritServerConfig Config cfg) { super(ConfigUtil.getRequired(cfg, "database", "driver")); this.cfg = cfg; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MaxDb.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MaxDb.java index ca5a60d..d552eb65 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MaxDb.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MaxDb.java
@@ -27,7 +27,7 @@ private Config cfg; @Inject - MaxDb(@GerritServerConfig final Config cfg) { + MaxDb(@GerritServerConfig Config cfg) { super("com.sap.dbtech.jdbc.DriverSapDB"); this.cfg = cfg; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java index fc8e176..9fc6896 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
@@ -27,7 +27,7 @@ private Config cfg; @Inject - MySql(@GerritServerConfig final Config cfg) { + MySql(@GerritServerConfig Config cfg) { super("com.mysql.jdbc.Driver"); this.cfg = cfg; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java index 43e9a3a..fd0c7fc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/NoChangesReviewDbWrapper.java
@@ -17,7 +17,6 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.CheckedFuture; import com.google.common.util.concurrent.Futures; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; @@ -51,7 +50,9 @@ return new ListResultSet<>(ImmutableList.of()); } - private static <T, K extends Key<?>> CheckedFuture<T, OrmException> emptyFuture() { + @SuppressWarnings("deprecation") + private static <T, K extends Key<?>> + com.google.common.util.concurrent.CheckedFuture<T, OrmException> emptyFuture() { return Futures.immediateCheckedFuture(null); } @@ -73,6 +74,11 @@ } @Override + public boolean changesTablesEnabled() { + return false; + } + + @Override public ChangeAccess changes() { return changes; } @@ -159,8 +165,9 @@ return empty(); } + @SuppressWarnings("deprecation") @Override - public final CheckedFuture<T, OrmException> getAsync(K key) { + public final com.google.common.util.concurrent.CheckedFuture<T, OrmException> getAsync(K key) { return emptyFuture(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java index e86f788..4ff7243 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Oracle.java
@@ -26,7 +26,7 @@ private Config cfg; @Inject - public Oracle(@GerritServerConfig final Config cfg) { + public Oracle(@GerritServerConfig Config cfg) { super("oracle.jdbc.driver.OracleDriver"); this.cfg = cfg; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgreSQL.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgreSQL.java index 23e7625..d6aee94 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgreSQL.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgreSQL.java
@@ -28,7 +28,7 @@ private Config cfg; @Inject - PostgreSQL(@GerritServerConfig final Config cfg) { + PostgreSQL(@GerritServerConfig Config cfg) { super("org.postgresql.Driver"); this.cfg = cfg; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java index 62d0f42..48ff646 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.schema; +import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroupName; @@ -75,7 +76,7 @@ indexCollection = ic; } - public void create(final ReviewDb db) throws OrmException, IOException, ConfigInvalidException { + public void create(ReviewDb db) throws OrmException, IOException, ConfigInvalidException { final JdbcSchema jdbc = (JdbcSchema) db; try (JdbcExecutor e = new JdbcExecutor(jdbc)) { jdbc.updateSchema(e); @@ -124,7 +125,8 @@ return new AccountGroup( // new AccountGroup.NameKey(name), // new AccountGroup.Id(c.nextAccountGroupId()), // - uuid); + uuid, + TimeUtil.nowTs()); } private SystemConfig initSystemConfig(ReviewDb db) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java index b60b1f7..266fbaa 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
@@ -52,14 +52,17 @@ @Inject SchemaUpdater( - SchemaFactory<ReviewDb> schema, SitePaths site, SchemaCreator creator, Injector parent) { + @ReviewDbFactory SchemaFactory<ReviewDb> schema, + SitePaths site, + SchemaCreator creator, + Injector parent) { this.schema = schema; this.site = site; this.creator = creator; this.updater = buildInjector(parent).getProvider(SchemaVersion.class); } - private static Injector buildInjector(final Injector parent) { + private static Injector buildInjector(Injector parent) { // Use DEVELOPMENT mode to allow lazy initialization of the // graph. This avoids touching ancient schema versions that // are behind this installation's current version. @@ -98,7 +101,7 @@ }); } - public void update(final UpdateUI ui) throws OrmException { + public void update(UpdateUI ui) throws OrmException { try (ReviewDb db = ReviewDbUtil.unwrapDb(schema.open())) { final SchemaVersion u = updater.get(); @@ -127,7 +130,7 @@ return updater.get(); } - private CurrentSchemaVersion getSchemaVersion(final ReviewDb db) { + private CurrentSchemaVersion getSchemaVersion(ReviewDb db) { try { return db.schemaVersion().get(new CurrentSchemaVersion.Key()); } catch (OrmException e) { @@ -135,7 +138,7 @@ } } - private void updateSystemConfig(final ReviewDb db) throws OrmException { + private void updateSystemConfig(ReviewDb db) throws OrmException { final SystemConfig sc = db.systemConfig().get(new SystemConfig.Key()); if (sc == null) { throw new OrmException("No record in system_config table");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java index a67a8a9..3cfd91c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -35,7 +35,7 @@ /** A version of the database schema. */ public abstract class SchemaVersion { /** The current schema version. */ - public static final Class<Schema_142> C = Schema_142.class; + public static final Class<Schema_154> C = Schema_154.class; public static int getBinaryVersion() { return guessVersion(C); @@ -44,7 +44,7 @@ private final Provider<? extends SchemaVersion> prior; private final int versionNbr; - protected SchemaVersion(final Provider<? extends SchemaVersion> prior) { + protected SchemaVersion(Provider<? extends SchemaVersion> prior) { this.prior = prior; this.versionNbr = guessVersion(getClass()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java index 2f3d09f..bdc15f4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
@@ -85,7 +85,7 @@ @Override public void stop() {} - private CurrentSchemaVersion getSchemaVersion(final ReviewDb db) { + private CurrentSchemaVersion getSchemaVersion(ReviewDb db) { try { return db.schemaVersion().get(new CurrentSchemaVersion.Key()); } catch (OrmException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_142.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_142.java index df808df..e67ae2f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_142.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_142.java
@@ -14,16 +14,24 @@ package com.google.gerrit.server.schema; -import com.google.gerrit.reviewdb.client.AccountExternalId; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; + +import com.google.common.base.Strings; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.HashedPassword; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gwtorm.jdbc.JdbcSchema; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; -import java.util.List; +import java.sql.Statement; public class Schema_142 extends SchemaVersion { + private static final int MAX_BATCH_SIZE = 1000; + @Inject Schema_142(Provider<Schema_141> prior) { super(prior); @@ -31,19 +39,39 @@ @Override protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException { - List<AccountExternalId> newIds = db.accountExternalIds().all().toList(); - for (AccountExternalId id : newIds) { - if (!id.isScheme(AccountExternalId.SCHEME_USERNAME)) { - continue; + try (PreparedStatement updateStmt = + ((JdbcSchema) db) + .getConnection() + .prepareStatement( + "UPDATE account_external_ids " + "SET password = ? " + "WHERE external_id = ?")) { + int batchCount = 0; + + try (Statement stmt = newStatement(db); + ResultSet rs = + stmt.executeQuery("SELECT external_id, password FROM account_external_ids")) { + while (rs.next()) { + String externalId = rs.getString("external_id"); + String password = rs.getString("password"); + if (!ExternalId.Key.parse(externalId).isScheme(SCHEME_USERNAME) + || Strings.isNullOrEmpty(password)) { + continue; + } + + HashedPassword hashed = HashedPassword.fromPassword(password); + updateStmt.setString(1, hashed.encode()); + updateStmt.setString(2, externalId); + updateStmt.addBatch(); + batchCount++; + if (batchCount >= MAX_BATCH_SIZE) { + updateStmt.executeBatch(); + batchCount = 0; + } + } } - String password = id.getPassword(); - if (password != null) { - HashedPassword hashed = HashedPassword.fromPassword(password); - id.setPassword(hashed.encode()); + if (batchCount > 0) { + updateStmt.executeBatch(); } } - - db.accountExternalIds().upsert(newIds); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_143.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_143.java new file mode 100644 index 0000000..b190b29 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_143.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +/** Add isPrivate field to change. */ +public class Schema_143 extends SchemaVersion { + @Inject + Schema_143(Provider<Schema_142> prior) { + super(prior); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_144.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_144.java new file mode 100644 index 0000000..eaa97e4d5 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_144.java
@@ -0,0 +1,102 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIdReader; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gwtorm.jdbc.JdbcSchema; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashSet; +import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.notes.NoteMap; +import org.eclipse.jgit.revwalk.RevWalk; + +public class Schema_144 extends SchemaVersion { + private static final String COMMIT_MSG = "Import external IDs from ReviewDb"; + + private final GitRepositoryManager repoManager; + private final AllUsersName allUsersName; + private final PersonIdent serverIdent; + + @Inject + Schema_144( + Provider<Schema_143> prior, + GitRepositoryManager repoManager, + AllUsersName allUsersName, + @GerritPersonIdent PersonIdent serverIdent) { + super(prior); + this.repoManager = repoManager; + this.allUsersName = allUsersName; + this.serverIdent = serverIdent; + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException { + Set<ExternalId> toAdd = new HashSet<>(); + try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement(); + ResultSet rs = + stmt.executeQuery( + "SELECT " + + "account_id, " + + "email_address, " + + "password, " + + "external_id " + + "FROM account_external_ids")) { + while (rs.next()) { + Account.Id accountId = new Account.Id(rs.getInt(1)); + String email = rs.getString(2); + String password = rs.getString(3); + String externalId = rs.getString(4); + + toAdd.add(ExternalId.create(ExternalId.Key.parse(externalId), accountId, email, password)); + } + } + + try { + try (Repository repo = repoManager.openRepository(allUsersName); + RevWalk rw = new RevWalk(repo); + ObjectInserter ins = repo.newObjectInserter()) { + ObjectId rev = ExternalIdReader.readRevision(repo); + + NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev); + + for (ExternalId extId : toAdd) { + ExternalIdsUpdate.upsert(rw, ins, noteMap, extId); + } + + ExternalIdsUpdate.commit(repo, rw, ins, rev, noteMap, COMMIT_MSG, serverIdent, serverIdent); + } + } catch (IOException | ConfigInvalidException e) { + throw new OrmException("Failed to migrate external IDs to NoteDb", e); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_145.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_145.java new file mode 100644 index 0000000..6ccb5d8 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_145.java
@@ -0,0 +1,50 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gwtorm.jdbc.JdbcSchema; +import com.google.gwtorm.schema.sql.SqlDialect; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.StatementExecutor; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.sql.SQLException; + +/** Create account_external_ids_byEmail index. */ +public class Schema_145 extends SchemaVersion { + + @Inject + Schema_145(Provider<Schema_144> prior) { + super(prior); + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException { + JdbcSchema schema = (JdbcSchema) db; + SqlDialect dialect = schema.getDialect(); + try (StatementExecutor e = newExecutor(db)) { + try { + dialect.dropIndex(e, "account_external_ids", "account_external_ids_byEmail"); + } catch (OrmException ex) { + // Ignore. The index did not exist. + } + e.execute( + "CREATE INDEX account_external_ids_byEmail" + + " ON account_external_ids" + + " (email_address)"); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java new file mode 100644 index 0000000..6ba1b65 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_146.java
@@ -0,0 +1,186 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.account.AccountsUpdate; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.Map; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Constants; +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.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevSort; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Make sure that for every account a user branch exists that has an initial empty commit with the + * registration date as commit time. + * + * <p>For accounts that don't have a user branch yet the user branch is created with an initial + * empty commit that has the registration date as commit time. + * + * <p>For accounts that already have a user branch the user branch is rewritten and an initial empty + * commit with the registration date as commit time is inserted (if such a commit doesn't exist + * yet). + */ +public class Schema_146 extends SchemaVersion { + private static final String CREATE_ACCOUNT_MSG = "Create Account"; + + private final GitRepositoryManager repoManager; + private final AllUsersName allUsersName; + private final PersonIdent serverIdent; + + @Inject + Schema_146( + Provider<Schema_145> prior, + GitRepositoryManager repoManager, + AllUsersName allUsersName, + @GerritPersonIdent PersonIdent serverIdent) { + super(prior); + this.repoManager = repoManager; + this.allUsersName = allUsersName; + this.serverIdent = serverIdent; + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException { + try (Repository repo = repoManager.openRepository(allUsersName); + RevWalk rw = new RevWalk(repo); + ObjectInserter oi = repo.newObjectInserter()) { + ObjectId emptyTree = emptyTree(oi); + + for (Map.Entry<Account.Id, Timestamp> e : scanAccounts(db).entrySet()) { + String refName = RefNames.refsUsers(e.getKey()); + Ref ref = repo.exactRef(refName); + if (ref != null) { + rewriteUserBranch(repo, rw, oi, emptyTree, ref, e.getValue()); + } else { + AccountsUpdate.createUserBranch( + repo, + allUsersName, + GitReferenceUpdated.DISABLED, + null, + oi, + serverIdent, + serverIdent, + e.getKey(), + e.getValue()); + } + } + } catch (IOException e) { + throw new OrmException("Failed to rewrite user branches.", e); + } + } + + private void rewriteUserBranch( + Repository repo, + RevWalk rw, + ObjectInserter oi, + ObjectId emptyTree, + Ref ref, + Timestamp registeredOn) + throws IOException { + ObjectId current = createInitialEmptyCommit(oi, emptyTree, registeredOn); + + rw.reset(); + rw.sort(RevSort.TOPO); + rw.sort(RevSort.REVERSE, true); + rw.markStart(rw.parseCommit(ref.getObjectId())); + + RevCommit c; + while ((c = rw.next()) != null) { + if (isInitialEmptyCommit(emptyTree, c)) { + return; + } + + CommitBuilder cb = new CommitBuilder(); + cb.setParentId(current); + cb.setTreeId(c.getTree()); + cb.setAuthor(c.getAuthorIdent()); + cb.setCommitter(c.getCommitterIdent()); + cb.setMessage(c.getFullMessage()); + cb.setEncoding(c.getEncoding()); + current = oi.insert(cb); + } + + oi.flush(); + + RefUpdate ru = repo.updateRef(ref.getName()); + ru.setExpectedOldObjectId(ref.getObjectId()); + ru.setNewObjectId(current); + ru.setForceUpdate(true); + ru.setRefLogIdent(serverIdent); + ru.setRefLogMessage(getClass().getSimpleName(), true); + Result result = ru.update(); + if (result != Result.FORCED) { + throw new IOException( + String.format("Failed to update ref %s: %s", ref.getName(), result.name())); + } + } + + private ObjectId createInitialEmptyCommit( + ObjectInserter oi, ObjectId emptyTree, Timestamp registrationDate) throws IOException { + PersonIdent ident = new PersonIdent(serverIdent, registrationDate); + + CommitBuilder cb = new CommitBuilder(); + cb.setTreeId(emptyTree); + cb.setCommitter(ident); + cb.setAuthor(ident); + cb.setMessage(CREATE_ACCOUNT_MSG); + return oi.insert(cb); + } + + private boolean isInitialEmptyCommit(ObjectId emptyTree, RevCommit c) { + return c.getParentCount() == 0 + && c.getTree().equals(emptyTree) + && c.getShortMessage().equals(CREATE_ACCOUNT_MSG); + } + + private static ObjectId emptyTree(ObjectInserter oi) throws IOException { + return oi.insert(Constants.OBJ_TREE, new byte[] {}); + } + + private Map<Account.Id, Timestamp> scanAccounts(ReviewDb db) throws SQLException { + try (Statement stmt = newStatement(db); + ResultSet rs = stmt.executeQuery("SELECT account_id, registered_on FROM accounts")) { + HashMap<Account.Id, Timestamp> m = new HashMap<>(); + while (rs.next()) { + m.put(new Account.Id(rs.getInt(1)), rs.getTimestamp(2)); + } + return m; + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java new file mode 100644 index 0000000..29ae7d5 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_147.java
@@ -0,0 +1,90 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import static java.util.stream.Collectors.toSet; + +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.account.AccountsUpdate; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; + +/** Delete user branches for which no account exists. */ +public class Schema_147 extends SchemaVersion { + private final GitRepositoryManager repoManager; + private final AllUsersName allUsersName; + private final PersonIdent serverIdent; + + @Inject + Schema_147( + Provider<Schema_146> prior, + GitRepositoryManager repoManager, + AllUsersName allUsersName, + @GerritPersonIdent PersonIdent serverIdent) { + super(prior); + this.repoManager = repoManager; + this.allUsersName = allUsersName; + this.serverIdent = serverIdent; + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException { + try (Repository repo = repoManager.openRepository(allUsersName)) { + Set<Account.Id> accountIdsFromReviewDb = scanAccounts(db); + Set<Account.Id> accountIdsFromUserBranches = + repo.getRefDatabase() + .getRefs(RefNames.REFS_USERS) + .values() + .stream() + .map(r -> Account.Id.fromRef(r.getName())) + .filter(Objects::nonNull) + .collect(toSet()); + accountIdsFromUserBranches.removeAll(accountIdsFromReviewDb); + for (Account.Id accountId : accountIdsFromUserBranches) { + AccountsUpdate.deleteUserBranch( + repo, allUsersName, GitReferenceUpdated.DISABLED, null, serverIdent, accountId); + } + } catch (IOException e) { + throw new OrmException("Failed to delete user branches for non-existing accounts.", e); + } + } + + private Set<Account.Id> scanAccounts(ReviewDb db) throws SQLException { + try (Statement stmt = newStatement(db); + ResultSet rs = stmt.executeQuery("SELECT account_id FROM accounts")) { + Set<Account.Id> ids = new HashSet<>(); + while (rs.next()) { + ids.add(new Account.Id(rs.getInt(1))); + } + return ids; + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_148.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_148.java new file mode 100644 index 0000000..abb3bb2 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_148.java
@@ -0,0 +1,99 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; + +import com.google.common.primitives.Ints; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIdReader; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.io.IOException; +import java.sql.SQLException; +import org.eclipse.jgit.errors.ConfigInvalidException; +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.Repository; +import org.eclipse.jgit.notes.Note; +import org.eclipse.jgit.notes.NoteMap; +import org.eclipse.jgit.revwalk.RevWalk; + +public class Schema_148 extends SchemaVersion { + private static final String COMMIT_MSG = "Make account IDs of external IDs human-readable"; + + private final GitRepositoryManager repoManager; + private final AllUsersName allUsersName; + private final PersonIdent serverUser; + + @Inject + Schema_148( + Provider<Schema_147> prior, + GitRepositoryManager repoManager, + AllUsersName allUsersName, + @GerritPersonIdent PersonIdent serverUser) { + super(prior); + this.repoManager = repoManager; + this.allUsersName = allUsersName; + this.serverUser = serverUser; + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException { + try (Repository repo = repoManager.openRepository(allUsersName); + RevWalk rw = new RevWalk(repo); + ObjectInserter ins = repo.newObjectInserter()) { + ObjectId rev = ExternalIdReader.readRevision(repo); + NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev); + boolean dirty = false; + for (Note note : noteMap) { + byte[] raw = + rw.getObjectReader() + .open(note.getData(), OBJ_BLOB) + .getCachedBytes(ExternalIdReader.MAX_NOTE_SZ); + try { + ExternalId extId = ExternalId.parse(note.getName(), raw); + + if (needsUpdate(extId)) { + ExternalIdsUpdate.upsert(rw, ins, noteMap, extId); + dirty = true; + } + } catch (ConfigInvalidException e) { + ui.message( + String.format("Warning: Ignoring invalid external ID note %s", note.getName())); + } + } + if (dirty) { + ExternalIdsUpdate.commit(repo, rw, ins, rev, noteMap, COMMIT_MSG, serverUser, serverUser); + } + } catch (IOException e) { + throw new OrmException("Failed to update external IDs", e); + } + } + + private static boolean needsUpdate(ExternalId extId) { + Config cfg = new Config(); + cfg.setInt("externalId", extId.key().get(), "accountId", extId.accountId().get()); + return Ints.tryParse(cfg.getString("externalId", extId.key().get(), "accountId")) == null; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_149.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_149.java new file mode 100644 index 0000000..f1ccaa6 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_149.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +/** Add workInProgress field to change. */ +public class Schema_149 extends SchemaVersion { + @Inject + Schema_149(Provider<Schema_148> prior) { + super(prior); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_150.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_150.java new file mode 100644 index 0000000..456a01a --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_150.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import com.google.inject.Inject; +import com.google.inject.Provider; + +/** Drop ACCOUNT_EXTERNAL_IDS table. */ +public class Schema_150 extends SchemaVersion { + @Inject + Schema_150(Provider<Schema_149> prior) { + super(prior); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_151.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_151.java new file mode 100644 index 0000000..2015c14 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_151.java
@@ -0,0 +1,56 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import com.google.common.collect.Streams; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit; +import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit.Key; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.ResultSet; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.sql.Timestamp; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +/** A schema which adds the 'created on' field to groups. */ +public class Schema_151 extends SchemaVersion { + @Inject + protected Schema_151(Provider<Schema_150> prior) { + super(prior); + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException { + List<AccountGroup> accountGroups = db.accountGroups().all().toList(); + for (AccountGroup accountGroup : accountGroups) { + ResultSet<AccountGroupMemberAudit> groupMemberAudits = + db.accountGroupMembersAudit().byGroup(accountGroup.getId()); + Optional<Timestamp> firstTimeMentioned = + Streams.stream(groupMemberAudits) + .map(AccountGroupMemberAudit::getKey) + .map(Key::getAddedOn) + .min(Comparator.naturalOrder()); + Timestamp createdOn = + firstTimeMentioned.orElseGet(() -> AccountGroup.auditCreationInstantTs()); + + accountGroup.setCreatedOn(createdOn); + } + db.accountGroups().update(accountGroups); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_152.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_152.java new file mode 100644 index 0000000..2ed7273 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_152.java
@@ -0,0 +1,41 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gwtorm.jdbc.JdbcSchema; +import com.google.gwtorm.schema.sql.SqlDialect; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.StatementExecutor; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.sql.SQLException; + +/** Drop unused indexes from accounts table. */ +public class Schema_152 extends SchemaVersion { + @Inject + Schema_152(Provider<Schema_151> prior) { + super(prior); + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException { + JdbcSchema schema = (JdbcSchema) db; + SqlDialect dialect = schema.getDialect(); + try (StatementExecutor e = newExecutor(db)) { + dialect.dropIndex(e, "accounts", "accounts_byFullName"); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_153.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_153.java new file mode 100644 index 0000000..8123218 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_153.java
@@ -0,0 +1,40 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.StatementExecutor; +import com.google.inject.Inject; +import com.google.inject.Provider; + +/** Add reviewStarted field to change. */ +public class Schema_153 extends SchemaVersion { + @Inject + Schema_153(Provider<Schema_152> prior) { + super(prior); + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException { + try (StatementExecutor e = newExecutor(db)) { + // Initialize review_started to a sensible default value according to + // whether change is currently WIP. No migration is needed in NoteDb, + // where the value of review_started is always derived from the history + // of assignments to work_in_progress. + e.execute("UPDATE changes SET review_started = 'Y' WHERE work_in_progress = 'N'"); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_154.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_154.java new file mode 100644 index 0000000..5a4ba13 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_154.java
@@ -0,0 +1,115 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.account.AccountConfig; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.MetaDataUpdate; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashSet; +import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.ProgressMonitor; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.TextProgressMonitor; + +/** Migrate accounts to NoteDb. */ +public class Schema_154 extends SchemaVersion { + private final GitRepositoryManager repoManager; + private final AllUsersName allUsersName; + private final Provider<PersonIdent> serverIdent; + + @Inject + Schema_154( + Provider<Schema_153> prior, + GitRepositoryManager repoManager, + AllUsersName allUsersName, + @GerritPersonIdent Provider<PersonIdent> serverIdent) { + super(prior); + this.repoManager = repoManager; + this.allUsersName = allUsersName; + this.serverIdent = serverIdent; + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException { + try { + try (Repository repo = repoManager.openRepository(allUsersName)) { + ProgressMonitor pm = new TextProgressMonitor(); + pm.beginTask("Collecting accounts", ProgressMonitor.UNKNOWN); + Set<Account> accounts = scanAccounts(db, pm); + pm.endTask(); + pm.beginTask("Migrating accounts to NoteDb", accounts.size()); + for (Account account : accounts) { + updateAccountInNoteDb(repo, account); + pm.update(1); + } + pm.endTask(); + } + } catch (IOException | ConfigInvalidException e) { + throw new OrmException("Migrating accounts to NoteDb failed", e); + } + } + + private Set<Account> scanAccounts(ReviewDb db, ProgressMonitor pm) throws SQLException { + try (Statement stmt = newStatement(db); + ResultSet rs = + stmt.executeQuery( + "SELECT account_id," + + " registered_on," + + " full_name, " + + " preferred_email," + + " status," + + " inactive" + + " FROM accounts")) { + Set<Account> s = new HashSet<>(); + while (rs.next()) { + Account a = new Account(new Account.Id(rs.getInt(1)), rs.getTimestamp(2)); + a.setFullName(rs.getString(3)); + a.setPreferredEmail(rs.getString(4)); + a.setStatus(rs.getString(5)); + a.setActive(rs.getString(6).equals("N")); + s.add(a); + pm.update(1); + } + return s; + } + } + + private void updateAccountInNoteDb(Repository allUsersRepo, Account account) + throws IOException, ConfigInvalidException { + MetaDataUpdate md = + new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, allUsersRepo); + PersonIdent ident = serverIdent.get(); + md.getCommitBuilder().setAuthor(ident); + md.getCommitBuilder().setCommitter(ident); + AccountConfig accountConfig = new AccountConfig(null, account.getId()); + accountConfig.load(allUsersRepo); + accountConfig.setAccount(account); + accountConfig.commit(md); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java index adee5fc..f4cba98 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
@@ -39,10 +39,10 @@ static final ScriptRunner NOOP = new ScriptRunner(null, null) { @Override - void run(final ReviewDb db) {} + void run(ReviewDb db) {} }; - ScriptRunner(final String scriptName, final InputStream script) { + ScriptRunner(String scriptName, InputStream script) { this.name = scriptName; try { this.commands = script != null ? parse(script) : null; @@ -51,7 +51,7 @@ } } - void run(final ReviewDb db) throws OrmException { + void run(ReviewDb db) throws OrmException { try { final JdbcSchema schema = (JdbcSchema) db; final Connection c = schema.getConnection(); @@ -73,7 +73,7 @@ } } - private List<String> parse(final InputStream in) throws IOException { + private List<String> parse(InputStream in) throws IOException { try (BufferedReader br = new BufferedReader(new InputStreamReader(in, UTF_8))) { String delimiter = ";"; List<String> commands = new ArrayList<>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java index b729b09..02ff159 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/securestore/DefaultSecureStore.java
@@ -130,7 +130,7 @@ } } - private static void saveSecure(final FileBasedConfig sec) throws IOException { + private static void saveSecure(FileBasedConfig sec) throws IOException { if (FileUtil.modified(sec)) { final byte[] out = Constants.encode(sec.toText()); final File path = sec.getFile();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAddressesModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAddressesModule.java index 70a6fce..0e5b2f8 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAddressesModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAddressesModule.java
@@ -53,7 +53,7 @@ return listen; } - for (final String desc : want) { + for (String desc : want) { try { listen.add(SocketUtil.resolve(desc, DEFAULT_PORT)); } catch (IllegalArgumentException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java index e7a7013..fdae8e9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -17,19 +17,31 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableMultiset.toImmutableMultiset; +import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ListMultimap; import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Multiset; import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.config.FactoryModule; +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.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.validators.OnSubmitValidators; +import com.google.gerrit.server.notedb.NotesMigration; +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.util.RequestId; import com.google.inject.Inject; import com.google.inject.Module; @@ -41,12 +53,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.TimeZone; import org.eclipse.jgit.lib.BatchRefUpdate; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; 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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,43 +95,99 @@ @Override public void configure() { factory(ReviewDbBatchUpdate.AssistedFactory.class); + factory(FusedNoteDbBatchUpdate.AssistedFactory.class); + factory(UnfusedNoteDbBatchUpdate.AssistedFactory.class); } }; } @Singleton public static class Factory { + private final NotesMigration migration; private final ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory; + private final FusedNoteDbBatchUpdate.AssistedFactory fusedNoteDbBatchUpdateFactory; + private final UnfusedNoteDbBatchUpdate.AssistedFactory unfusedNoteDbBatchUpdateFactory; + // TODO(dborowitz): Make this non-injectable to force all callers to use RetryHelper. @Inject - Factory(ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory) { + Factory( + NotesMigration migration, + ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory, + FusedNoteDbBatchUpdate.AssistedFactory fusedNoteDbBatchUpdateFactory, + UnfusedNoteDbBatchUpdate.AssistedFactory unfusedNoteDbBatchUpdateFactory) { + this.migration = migration; this.reviewDbBatchUpdateFactory = reviewDbBatchUpdateFactory; + this.fusedNoteDbBatchUpdateFactory = fusedNoteDbBatchUpdateFactory; + this.unfusedNoteDbBatchUpdateFactory = unfusedNoteDbBatchUpdateFactory; } public BatchUpdate create( ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when) { + if (migration.disableChangeReviewDb()) { + if (migration.fuseUpdates()) { + return fusedNoteDbBatchUpdateFactory.create(db, project, user, when); + } + return unfusedNoteDbBatchUpdateFactory.create(db, project, user, when); + } return reviewDbBatchUpdateFactory.create(db, project, user, when); } + @SuppressWarnings({"rawtypes", "unchecked"}) public void execute( Collection<BatchUpdate> updates, BatchUpdateListener listener, @Nullable RequestId requestId, boolean dryRun) throws UpdateException, RestApiException { + checkNotNull(listener); + checkDifferentProject(updates); // It's safe to downcast all members of the input collection in this case, because the only // way a caller could have gotten any BatchUpdates in the first place is to call the create // method above, which always returns instances of the type we expect. Just to be safe, // copy them into an ImmutableList so there is no chance the callee can pollute the input // collection. - @SuppressWarnings({"rawtypes", "unchecked"}) - ImmutableList<ReviewDbBatchUpdate> reviewDbUpdates = - (ImmutableList) ImmutableList.copyOf(updates); - ReviewDbBatchUpdate.execute(reviewDbUpdates, listener, requestId, dryRun); + if (migration.disableChangeReviewDb()) { + if (migration.fuseUpdates()) { + ImmutableList<FusedNoteDbBatchUpdate> noteDbUpdates = + (ImmutableList) ImmutableList.copyOf(updates); + FusedNoteDbBatchUpdate.execute(noteDbUpdates, listener, requestId, dryRun); + } else { + ImmutableList<UnfusedNoteDbBatchUpdate> noteDbUpdates = + (ImmutableList) ImmutableList.copyOf(updates); + UnfusedNoteDbBatchUpdate.execute(noteDbUpdates, listener, requestId, dryRun); + } + } else { + ImmutableList<ReviewDbBatchUpdate> reviewDbUpdates = + (ImmutableList) ImmutableList.copyOf(updates); + ReviewDbBatchUpdate.execute(reviewDbUpdates, listener, requestId, dryRun); + } + } + + 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); } } - protected static Order getOrder(Collection<? extends BatchUpdate> updates) { + static void setRequestIds( + Collection<? extends BatchUpdate> updates, @Nullable RequestId requestId) { + if (requestId != null) { + for (BatchUpdate u : updates) { + checkArgument( + u.requestId == null || u.requestId == requestId, + "refusing to overwrite RequestId %s in update with %s", + u.requestId, + requestId); + u.setRequestId(requestId); + } + } + } + + static Order getOrder(Collection<? extends BatchUpdate> updates, BatchUpdateListener listener) { Order o = null; for (BatchUpdate u : updates) { if (o == null) { @@ -126,10 +196,16 @@ throw new IllegalArgumentException("cannot mix execution orders"); } } + if (o != Order.REPO_BEFORE_DB) { + checkArgument( + listener == BatchUpdateListener.NONE, + "BatchUpdateListener not supported for order %s", + o); + } return o; } - protected static boolean getUpdateChangesInParallel(Collection<? extends BatchUpdate> updates) { + static boolean getUpdateChangesInParallel(Collection<? extends BatchUpdate> updates) { checkArgument(!updates.isEmpty()); Boolean p = null; for (BatchUpdate u : updates) { @@ -148,6 +224,28 @@ return p; } + static void wrapAndThrowException(Exception e) throws UpdateException, RestApiException { + 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); + + // Convert other common non-REST exception types with user-visible messages to corresponding + // REST exception types + if (e instanceof InvalidChangeOperationException) { + throw new ResourceConflictException(e.getMessage(), e); + } else if (e instanceof NoSuchChangeException + || e instanceof NoSuchRefException + || e instanceof NoSuchProjectException) { + throw new ResourceNotFoundException(e.getMessage(), e); + } + + // Otherwise, wrap in a generic UpdateException, which does not include a user-visible message. + throw new UpdateException(e); + } + protected GitRepositoryManager repoManager; protected final Project.NameKey project; @@ -160,18 +258,15 @@ protected final Map<Change.Id, Change> newChanges = new HashMap<>(); protected final List<RepoOnlyOp> repoOnlyOps = new ArrayList<>(); - protected Repository repo; - protected ObjectInserter inserter; - protected RevWalk revWalk; - protected ChainedReceiveCommands commands; + protected RepoView repoView; protected BatchRefUpdate batchRefUpdate; protected Order order; protected OnSubmitValidators onSubmitValidators; protected RequestId requestId; + protected PushCertificate pushCert; protected String refLogMessage; private boolean updateChangesInParallel; - private boolean closeRepo; protected BatchUpdate( GitRepositoryManager repoManager, @@ -189,18 +284,17 @@ @Override public void close() { - if (closeRepo) { - revWalk.getObjectReader().close(); - revWalk.close(); - inserter.close(); - repo.close(); + if (repoView != null) { + repoView.close(); } } public abstract void execute(BatchUpdateListener listener) throws UpdateException, RestApiException; - public abstract void execute() throws UpdateException, RestApiException; + public void execute() throws UpdateException, RestApiException { + execute(BatchUpdateListener.NONE); + } protected abstract Context newContext(); @@ -210,16 +304,17 @@ } public BatchUpdate setRepository(Repository repo, RevWalk revWalk, ObjectInserter inserter) { - checkState(this.repo == null, "repo already set"); - closeRepo = false; - this.repo = checkNotNull(repo, "repo"); - this.revWalk = checkNotNull(revWalk, "revWalk"); - this.inserter = checkNotNull(inserter, "inserter"); - commands = new ChainedReceiveCommands(repo); + checkState(this.repoView == null, "repo already set"); + repoView = new RepoView(repo, revWalk, inserter); return this; } - public BatchUpdate setRefLogMessage(String refLogMessage) { + public BatchUpdate setPushCertificate(@Nullable PushCertificate pushCert) { + this.pushCert = pushCert; + return this; + } + + public BatchUpdate setRefLogMessage(@Nullable String refLogMessage) { this.refLogMessage = refLogMessage; return this; } @@ -238,43 +333,46 @@ return this; } - /** Execute {@link BatchUpdateOp#updateChange(ChangeContext)} in parallel for each change. */ + /** + * Execute {@link BatchUpdateOp#updateChange(ChangeContext)} in parallel for each change. + * + * <p>This improves performance of writing to multiple changes in separate ReviewDb transactions. + * When only NoteDb is used, updates to all changes are written in a single batch ref update, so + * parallelization is not used and this option is ignored. + */ public BatchUpdate updateChangesInParallel() { this.updateChangesInParallel = true; return this; } protected void initRepository() throws IOException { - if (repo == null) { - this.repo = repoManager.openRepository(project); - closeRepo = true; - inserter = repo.newObjectInserter(); - revWalk = new RevWalk(inserter.newReader()); - commands = new ChainedReceiveCommands(repo); + if (repoView == null) { + repoView = new RepoView(repoManager, project); } } + protected RepoView getRepoView() throws IOException { + initRepository(); + return repoView; + } + protected CurrentUser getUser() { return user; } - protected Repository getRepository() throws IOException { - initRepository(); - return repo; + protected Optional<Account> getAccount() { + return user.isIdentifiedUser() + ? Optional.of(user.asIdentifiedUser().getAccount()) + : Optional.empty(); } protected RevWalk getRevWalk() throws IOException { initRepository(); - return revWalk; + return repoView.getRevWalk(); } - protected ObjectInserter getObjectInserter() throws IOException { - initRepository(); - return inserter; - } - - public Collection<ReceiveCommand> getRefUpdates() { - return commands.getCommands().values(); + public Map<String, ReceiveCommand> getRefUpdates() { + return repoView != null ? repoView.getCommands().getCommands() : ImmutableMap.of(); } public BatchUpdate addOp(Change.Id id, BatchUpdateOp op) { @@ -290,7 +388,7 @@ return this; } - public BatchUpdate insertChange(InsertChangeOp op) { + public BatchUpdate insertChange(InsertChangeOp op) throws IOException { Context ctx = newContext(); Change c = op.createChange(ctx); checkArgument(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdateListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdateListener.java index 765bba1..847a7ca 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdateListener.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdateListener.java
@@ -19,6 +19,8 @@ * * <p>When used during execution of multiple batch updates, the {@code after*} methods are called * after that phase has been completed for <em>all</em> updates. + * + * <p>Listeners are only supported for the {@link Order#REPO_BEFORE_DB} order. */ public interface BatchUpdateListener { public static final BatchUpdateListener NONE = new BatchUpdateListener() {};
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdateOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdateOp.java index 39e25dd..87a43a3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdateOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/BatchUpdateOp.java
@@ -22,8 +22,11 @@ * BatchUpdate#addOp(com.google.gerrit.reviewdb.client.Change.Id, BatchUpdateOp)}. * * <p>Usually, a single {@code BatchUpdateOp} instance is only associated with a single change, i.e. - * {@code addOp} is only called once with that instance. This allows an instance to communicate - * between phases by storing data in private fields. + * {@code addOp} is only called once with that instance. Additionally, each method in {@code + * BatchUpdateOp} is called at most once per {@link BatchUpdate} execution. + * + * <p>Taken together, these two properties mean an instance may communicate between phases by + * storing data in private fields, and a single instance must not be reused. */ public interface BatchUpdateOp extends RepoOnlyOp { /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/ChangeContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/ChangeContext.java index d619490..ca39763 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/update/ChangeContext.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/ChangeContext.java
@@ -44,17 +44,24 @@ ChangeUpdate getUpdate(PatchSet.Id psId); /** - * @return control for this change. The user will be the same as {@link #getUser()}, and the - * change data is read within the same transaction that {@code updateChange} is executing. + * Get the control for this change, encapsulating the user and up-to-date change data. + * + * <p>The user will be the same as {@link #getUser()}, and the change data is read within the same + * transaction that {@link BatchUpdateOp#updateChange(ChangeContext)} is executing. + * + * @return control for this change. */ ChangeControl getControl(); /** - * @param bump whether to bump the value of {@link Change#getLastUpdatedOn()} field before storing - * to ReviewDb. For NoteDb, the value is always incremented (assuming the update is not - * otherwise a no-op). + * Don't bump the value of {@link Change#getLastUpdatedOn()}. + * + * <p>If called, don't bump the timestamp before storing to ReviewDb. Only has an effect in + * ReviewDb, and the only usage should be to match the behavior of NoteDb. Specifically, in NoteDb + * the timestamp is updated if and only if the change meta graph is updated, and is not updated + * when only drafts are modified. */ - void bumpLastUpdatedOn(boolean bump); + void dontBumpLastUpdatedOn(); /** * Instruct {@link BatchUpdate} to delete this change. @@ -63,7 +70,11 @@ */ void deleteChange(); - /** @return notes corresponding to {@link #getControl()}. */ + /** + * Get notes corresponding to {@link #getControl()}. + * + * @return loaded notes instance. + */ default ChangeNotes getNotes() { return checkNotNull(getControl().getNotes()); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/Context.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/Context.java index 497b7ab..f33536d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/update/Context.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/Context.java
@@ -24,7 +24,6 @@ import java.io.IOException; import java.sql.Timestamp; import java.util.TimeZone; -import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevWalk; /** @@ -33,19 +32,22 @@ * <p>A single update may span multiple changes, but they all belong to a single repo. */ public interface Context { - /** @return the project name this update operates on. */ + /** + * Get the project name this update operates on. + * + * @return project. + */ Project.NameKey getProject(); /** - * Get an open repository instance for this project. + * Get a read-only view of the open repository for this project. * - * <p>Will be opened lazily if necessary; callers should not close the repo. In some phases of the - * update, the repository might be read-only; see {@link BatchUpdateOp} for details. + * <p>Will be opened lazily if necessary. * * @return repository instance. * @throws IOException if an error occurred opening the repo. */ - Repository getRepository() throws IOException; + RepoView getRepoView() throws IOException; /** * Get a walk for this project. @@ -57,50 +59,80 @@ */ RevWalk getRevWalk() throws IOException; - /** @return the timestamp at which this update takes place. */ + /** + * Get the timestamp at which this update takes place. + * + * @return timestamp. + */ Timestamp getWhen(); /** - * @return the time zone in which this update takes place. In the current implementation, this is - * always the time zone of the server. + * Get the time zone in which this update takes place. + * + * <p>In the current implementation, this is always the time zone of the server. + * + * @return time zone. */ TimeZone getTimeZone(); /** - * @return an open ReviewDb database. Callers should not manage transactions or call mutating - * methods on the Changes table. Mutations on other tables (including other entities in the - * change entity group) are fine. + * Get the ReviewDb database. + * + * <p>Callers should not manage transactions or call mutating methods on the Changes table. + * Mutations on other tables (including other entities in the change entity group) are fine. + * + * @return open database instance. */ ReviewDb getDb(); /** - * @return user performing the update. In the current implementation, this is always an {@link - * IdentifiedUser} or {@link com.google.gerrit.server.InternalUser}. + * Get the user performing the update. + * + * <p>In the current implementation, this is always an {@link IdentifiedUser} or {@link + * com.google.gerrit.server.InternalUser}. + * + * @return user. */ CurrentUser getUser(); - /** @return order in which operations are executed in this update. */ + /** + * Get the order in which operations are executed in this update. + * + * @return order of operations. + */ Order getOrder(); /** - * @return identified user performing the update; throws an unchecked exception if the user is not - * an {@link IdentifiedUser} + * Get the identified user performing the update. + * + * <p>Convenience method for {@code getUser().asIdentifiedUser()}. + * + * @see CurrentUser#asIdentifiedUser() + * @return user. */ default IdentifiedUser getIdentifiedUser() { return checkNotNull(getUser()).asIdentifiedUser(); } /** - * @return account of the user performing the update; throws if the user is not an {@link - * IdentifiedUser} + * Get the account of the user performing the update. + * + * <p>Convenience method for {@code getIdentifiedUser().getAccount()}. + * + * @see CurrentUser#asIdentifiedUser() + * @return account. */ default Account getAccount() { return getIdentifiedUser().getAccount(); } /** - * @return account ID of the user performing the update; throws if the user is not an {@link - * IdentifiedUser} + * Get the account ID of the user performing the update. + * + * <p>Convenience method for {@code getUser().getAccountId()} + * + * @see CurrentUser#getAccountId() + * @return account ID. */ default Account.Id getAccountId() { return getIdentifiedUser().getAccountId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java new file mode 100644 index 0000000..7db5e43 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java
@@ -0,0 +1,463 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Comparator.comparing; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +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.index.change.ChangeIndexer; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.NoteDbUpdateManager; +import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.util.RequestId; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; +import java.util.TreeMap; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; + +/** + * {@link BatchUpdate} implementation using only NoteDb that updates code refs and meta refs in a + * single {@link org.eclipse.jgit.lib.BatchRefUpdate}. + * + * <p>Used when {@code noteDb.changes.disableReviewDb=true}, at which point ReviewDb is not + * consulted during updates. + */ +class FusedNoteDbBatchUpdate extends BatchUpdate { + interface AssistedFactory { + FusedNoteDbBatchUpdate create( + ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when); + } + + static void execute( + ImmutableList<FusedNoteDbBatchUpdate> updates, + BatchUpdateListener listener, + @Nullable RequestId requestId, + boolean dryrun) + throws UpdateException, RestApiException { + if (updates.isEmpty()) { + return; + } + setRequestIds(updates, requestId); + + try { + @SuppressWarnings("deprecation") + List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures = + new ArrayList<>(); + List<ChangesHandle> handles = new ArrayList<>(updates.size()); + Order order = getOrder(updates, listener); + try { + switch (order) { + case REPO_BEFORE_DB: + for (FusedNoteDbBatchUpdate u : updates) { + u.executeUpdateRepo(); + } + listener.afterUpdateRepos(); + for (FusedNoteDbBatchUpdate u : updates) { + handles.add(u.executeChangeOps(dryrun)); + } + for (ChangesHandle h : handles) { + h.execute(); + indexFutures.addAll(h.startIndexFutures()); + } + listener.afterUpdateRefs(); + listener.afterUpdateChanges(); + break; + + case DB_BEFORE_REPO: + // Call updateChange for each op before updateRepo, but defer executing the + // NoteDbUpdateManager until after calling updateRepo. They share an inserter and + // BatchRefUpdate, so it will all execute as a single batch. But we have to let + // NoteDbUpdateManager actually execute the update, since it has to interleave it + // properly with All-Users updates. + // + // TODO(dborowitz): This may still result in multiple updates to All-Users, but that's + // currently not a big deal because multi-change batches generally aren't affecting + // drafts anyway. + for (FusedNoteDbBatchUpdate u : updates) { + handles.add(u.executeChangeOps(dryrun)); + } + for (FusedNoteDbBatchUpdate u : updates) { + u.executeUpdateRepo(); + } + for (ChangesHandle h : handles) { + // TODO(dborowitz): This isn't quite good enough: in theory updateRepo may want to + // see the results of change meta commands, but they aren't actually added to the + // BatchUpdate until the body of execute. To fix this, execute needs to be split up + // into a method that returns a BatchRefUpdate before execution. Not a big deal at the + // moment, because this order is only used for deleting changes, and those updateRepo + // implementations definitely don't need to observe the updated change meta refs. + h.execute(); + indexFutures.addAll(h.startIndexFutures()); + } + break; + default: + throw new IllegalStateException("invalid execution order: " + order); + } + } finally { + for (ChangesHandle h : handles) { + h.close(); + } + } + + ChangeIndexer.allAsList(indexFutures).get(); + + // 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 + .stream() + .filter(u -> u.batchRefUpdate != null) + .forEach( + u -> u.gitRefUpdated.fire(u.project, u.batchRefUpdate, u.getAccount().orElse(null))); + + if (!dryrun) { + for (FusedNoteDbBatchUpdate u : updates) { + u.executePostOps(); + } + } + } catch (Exception e) { + wrapAndThrowException(e); + } + } + + class ContextImpl implements Context { + @Override + public RepoView getRepoView() throws IOException { + return FusedNoteDbBatchUpdate.this.getRepoView(); + } + + @Override + public RevWalk getRevWalk() throws IOException { + return getRepoView().getRevWalk(); + } + + @Override + public Project.NameKey getProject() { + return project; + } + + @Override + public Timestamp getWhen() { + return when; + } + + @Override + public TimeZone getTimeZone() { + return tz; + } + + @Override + public ReviewDb getDb() { + return db; + } + + @Override + public CurrentUser getUser() { + return user; + } + + @Override + public Order getOrder() { + return order; + } + } + + private class RepoContextImpl extends ContextImpl implements RepoContext { + @Override + public ObjectInserter getInserter() throws IOException { + return getRepoView().getInserterWrapper(); + } + + @Override + public void addRefUpdate(ReceiveCommand cmd) throws IOException { + getRepoView().getCommands().add(cmd); + } + } + + private class ChangeContextImpl extends ContextImpl implements ChangeContext { + private final ChangeControl ctl; + private final Map<PatchSet.Id, ChangeUpdate> updates; + + private boolean deleted; + + protected ChangeContextImpl(ChangeControl ctl) { + this.ctl = checkNotNull(ctl); + updates = new TreeMap<>(comparing(PatchSet.Id::get)); + } + + @Override + public ChangeUpdate getUpdate(PatchSet.Id psId) { + ChangeUpdate u = updates.get(psId); + if (u == null) { + u = changeUpdateFactory.create(ctl, when); + if (newChanges.containsKey(ctl.getId())) { + u.setAllowWriteToNewRef(true); + } + u.setPatchSetId(psId); + updates.put(psId, u); + } + return u; + } + + @Override + public ChangeControl getControl() { + return ctl; + } + + @Override + public void dontBumpLastUpdatedOn() { + // Do nothing; NoteDb effectively updates timestamp if and only if a commit was written to the + // change meta ref. + } + + @Override + public void deleteChange() { + deleted = true; + } + } + + /** Per-change result status from {@link #executeChangeOps}. */ + private enum ChangeResult { + SKIPPED, + UPSERTED, + DELETED; + } + + private final ChangeNotes.Factory changeNotesFactory; + private final ChangeControl.GenericFactory changeControlFactory; + private final ChangeUpdate.Factory changeUpdateFactory; + private final NoteDbUpdateManager.Factory updateManagerFactory; + private final ChangeIndexer indexer; + private final GitReferenceUpdated gitRefUpdated; + private final ReviewDb db; + + @Inject + FusedNoteDbBatchUpdate( + GitRepositoryManager repoManager, + @GerritPersonIdent PersonIdent serverIdent, + ChangeNotes.Factory changeNotesFactory, + ChangeControl.GenericFactory changeControlFactory, + ChangeUpdate.Factory changeUpdateFactory, + NoteDbUpdateManager.Factory updateManagerFactory, + ChangeIndexer indexer, + GitReferenceUpdated gitRefUpdated, + @Assisted ReviewDb db, + @Assisted Project.NameKey project, + @Assisted CurrentUser user, + @Assisted Timestamp when) { + super(repoManager, serverIdent, project, user, when); + this.changeNotesFactory = changeNotesFactory; + this.changeControlFactory = changeControlFactory; + this.changeUpdateFactory = changeUpdateFactory; + this.updateManagerFactory = updateManagerFactory; + this.indexer = indexer; + this.gitRefUpdated = gitRefUpdated; + this.db = db; + } + + @Override + public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException { + execute(ImmutableList.of(this), listener, requestId, false); + } + + @Override + protected Context newContext() { + return new ContextImpl(); + } + + private void executeUpdateRepo() throws UpdateException, RestApiException { + try { + logDebug("Executing updateRepo on {} ops", ops.size()); + RepoContextImpl ctx = new RepoContextImpl(); + for (BatchUpdateOp op : ops.values()) { + op.updateRepo(ctx); + } + + logDebug("Executing updateRepo on {} RepoOnlyOps", repoOnlyOps.size()); + for (RepoOnlyOp op : repoOnlyOps) { + op.updateRepo(ctx); + } + + if (onSubmitValidators != null && !getRefUpdates().isEmpty()) { + // Validation of refs has to take place here and not at the beginning of executeRefUpdates. + // Otherwise, failing validation in a second BatchUpdate object will happen *after* the + // first update's executeRefUpdates has finished, hence after first repo's refs have been + // updated, which is too late. + onSubmitValidators.validate( + project, ctx.getRevWalk().getObjectReader(), repoView.getCommands()); + } + } catch (Exception e) { + Throwables.throwIfInstanceOf(e, RestApiException.class); + throw new UpdateException(e); + } + } + + private class ChangesHandle implements AutoCloseable { + private final NoteDbUpdateManager manager; + private final boolean dryrun; + private final Map<Change.Id, ChangeResult> results; + + ChangesHandle(NoteDbUpdateManager manager, boolean dryrun) { + this.manager = manager; + this.dryrun = dryrun; + results = new HashMap<>(); + } + + @Override + public void close() { + manager.close(); + } + + void setResult(Change.Id id, ChangeResult result) { + ChangeResult old = results.putIfAbsent(id, result); + checkArgument(old == null, "result for change %s already set: %s", id, old); + } + + void execute() throws OrmException, IOException { + FusedNoteDbBatchUpdate.this.batchRefUpdate = manager.execute(dryrun); + } + + @SuppressWarnings("deprecation") + List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> startIndexFutures() { + if (dryrun) { + return ImmutableList.of(); + } + logDebug("Reindexing {} changes", results.size()); + List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures = + new ArrayList<>(results.size()); + for (Map.Entry<Change.Id, ChangeResult> e : results.entrySet()) { + Change.Id id = e.getKey(); + switch (e.getValue()) { + case UPSERTED: + indexFutures.add(indexer.indexAsync(project, id)); + break; + case DELETED: + indexFutures.add(indexer.deleteAsync(id)); + break; + case SKIPPED: + break; + default: + throw new IllegalStateException("unexpected result: " + e.getValue()); + } + } + return indexFutures; + } + } + + private ChangesHandle executeChangeOps(boolean dryrun) throws Exception { + logDebug("Executing change ops"); + initRepository(); + Repository repo = repoView.getRepository(); + checkState( + repo.getRefDatabase().performsAtomicTransactions(), + "cannot use noteDb.changes.fuseUpdates=true with a repository that does not support atomic" + + " batch ref updates: %s", + repo); + + ChangesHandle handle = + new ChangesHandle( + updateManagerFactory + .create(project) + .setChangeRepo( + repo, repoView.getRevWalk(), repoView.getInserter(), repoView.getCommands()), + dryrun); + if (user.isIdentifiedUser()) { + handle.manager.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, tz)); + } + handle.manager.setRefLogMessage(refLogMessage); + handle.manager.setPushCertificate(pushCert); + for (Map.Entry<Change.Id, Collection<BatchUpdateOp>> e : ops.asMap().entrySet()) { + Change.Id id = e.getKey(); + ChangeContextImpl ctx = newChangeContext(id); + boolean dirty = false; + logDebug("Applying {} ops for change {}", e.getValue().size(), id); + for (BatchUpdateOp op : e.getValue()) { + dirty |= op.updateChange(ctx); + } + if (!dirty) { + logDebug("No ops reported dirty, short-circuiting"); + handle.setResult(id, ChangeResult.SKIPPED); + continue; + } + for (ChangeUpdate u : ctx.updates.values()) { + handle.manager.add(u); + } + if (ctx.deleted) { + logDebug("Change {} was deleted", id); + handle.manager.deleteChange(id); + handle.setResult(id, ChangeResult.DELETED); + } else { + handle.setResult(id, ChangeResult.UPSERTED); + } + } + return handle; + } + + private ChangeContextImpl newChangeContext(Change.Id id) throws OrmException { + logDebug("Opening change {} for update", id); + Change c = newChanges.get(id); + boolean isNew = c != null; + if (!isNew) { + // Pass a synthetic change into ChangeNotes.Factory, which will take care of checking for + // existence and populating columns from the parsed notes state. + // TODO(dborowitz): This dance made more sense when using Reviewdb; consider a nicer way. + c = ChangeNotes.Factory.newNoteDbOnlyChange(project, id); + } else { + logDebug("Change {} is new", id); + } + ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew); + ChangeControl ctl = changeControlFactory.controlFor(notes, user); + return new ChangeContextImpl(ctl); + } + + private void executePostOps() throws Exception { + ContextImpl ctx = new ContextImpl(); + for (BatchUpdateOp op : ops.values()) { + op.postUpdate(ctx); + } + + for (RepoOnlyOp op : repoOnlyOps) { + op.postUpdate(ctx); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/InsertChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/InsertChangeOp.java index 1a947e6..7060059 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/update/InsertChangeOp.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/InsertChangeOp.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.update; import com.google.gerrit.reviewdb.client.Change; +import java.io.IOException; /** * Specialization of {@link BatchUpdateOp} for creating changes. @@ -27,5 +28,5 @@ * first. */ public interface InsertChangeOp extends BatchUpdateOp { - Change createChange(Context ctx); + Change createChange(Context ctx) throws IOException; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/ReadOnlyRepository.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/ReadOnlyRepository.java deleted file mode 100644 index 37c1d60..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/update/ReadOnlyRepository.java +++ /dev/null
@@ -1,180 +0,0 @@ -// Copyright (C) 2015 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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.checkNotNull; - -import java.io.IOException; -import java.util.List; -import java.util.Map; -import org.eclipse.jgit.attributes.AttributesNodeProvider; -import org.eclipse.jgit.lib.BaseRepositoryBuilder; -import org.eclipse.jgit.lib.ObjectDatabase; -import org.eclipse.jgit.lib.ObjectInserter; -import org.eclipse.jgit.lib.ObjectReader; -import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.RefDatabase; -import org.eclipse.jgit.lib.RefRename; -import org.eclipse.jgit.lib.RefUpdate; -import org.eclipse.jgit.lib.ReflogReader; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.lib.StoredConfig; - -class ReadOnlyRepository extends Repository { - private static final String MSG = "Cannot modify a " + ReadOnlyRepository.class.getSimpleName(); - - private static BaseRepositoryBuilder<?, ?> builder(Repository r) { - checkNotNull(r); - BaseRepositoryBuilder<?, ?> builder = - new BaseRepositoryBuilder<>().setFS(r.getFS()).setGitDir(r.getDirectory()); - - if (!r.isBare()) { - builder.setWorkTree(r.getWorkTree()).setIndexFile(r.getIndexFile()); - } - return builder; - } - - private final Repository delegate; - private final RefDb refdb; - private final ObjDb objdb; - - ReadOnlyRepository(Repository delegate) { - super(builder(delegate)); - this.delegate = delegate; - this.refdb = new RefDb(delegate.getRefDatabase()); - this.objdb = new ObjDb(delegate.getObjectDatabase()); - } - - @Override - public void create(boolean bare) throws IOException { - throw new UnsupportedOperationException(MSG); - } - - @Override - public ObjectDatabase getObjectDatabase() { - return objdb; - } - - @Override - public RefDatabase getRefDatabase() { - return refdb; - } - - @Override - public StoredConfig getConfig() { - return delegate.getConfig(); - } - - @Override - public AttributesNodeProvider createAttributesNodeProvider() { - return delegate.createAttributesNodeProvider(); - } - - @Override - public void scanForRepoChanges() throws IOException { - delegate.scanForRepoChanges(); - } - - @Override - public void notifyIndexChanged() { - delegate.notifyIndexChanged(); - } - - @Override - public ReflogReader getReflogReader(String refName) throws IOException { - return delegate.getReflogReader(refName); - } - - @Override - public String getGitwebDescription() throws IOException { - return delegate.getGitwebDescription(); - } - - private static class RefDb extends RefDatabase { - private final RefDatabase delegate; - - private RefDb(RefDatabase delegate) { - this.delegate = checkNotNull(delegate); - } - - @Override - public void create() throws IOException { - throw new UnsupportedOperationException(MSG); - } - - @Override - public void close() { - delegate.close(); - } - - @Override - public boolean isNameConflicting(String name) throws IOException { - return delegate.isNameConflicting(name); - } - - @Override - public RefUpdate newUpdate(String name, boolean detach) throws IOException { - throw new UnsupportedOperationException(MSG); - } - - @Override - public RefRename newRename(String fromName, String toName) throws IOException { - throw new UnsupportedOperationException(MSG); - } - - @Override - public Ref getRef(String name) throws IOException { - return delegate.getRef(name); - } - - @Override - public Map<String, Ref> getRefs(String prefix) throws IOException { - return delegate.getRefs(prefix); - } - - @Override - public List<Ref> getAdditionalRefs() throws IOException { - return delegate.getAdditionalRefs(); - } - - @Override - public Ref peel(Ref ref) throws IOException { - return delegate.peel(ref); - } - } - - private static class ObjDb extends ObjectDatabase { - private final ObjectDatabase delegate; - - private ObjDb(ObjectDatabase delegate) { - this.delegate = checkNotNull(delegate); - } - - @Override - public ObjectInserter newInserter() { - throw new UnsupportedOperationException(MSG); - } - - @Override - public ObjectReader newReader() { - return delegate.newReader(); - } - - @Override - public void close() { - delegate.close(); - } - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RefUpdateUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RefUpdateUtil.java new file mode 100644 index 0000000..ab0b78e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RefUpdateUtil.java
@@ -0,0 +1,84 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 com.google.common.annotations.VisibleForTesting; +import com.google.gerrit.server.git.LockFailureException; +import java.io.IOException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; + +/** Static utilities for working with JGit's ref update APIs. */ +public class RefUpdateUtil { + /** + * Execute a batch ref update, throwing a checked exception if not all updates succeeded. + * + * @param bru batch update; should already have been executed. + * @throws LockFailureException if the transaction was aborted due to lock failure; see {@link + * #checkResults(BatchRefUpdate)} for details. + * @throws IOException if any result was not {@code OK}. + */ + public static void executeChecked(BatchRefUpdate bru, RevWalk rw) throws IOException { + bru.execute(rw, NullProgressMonitor.INSTANCE); + checkResults(bru); + } + + /** + * Check results of all commands in the update batch, reducing to a single exception if there was + * a failure. + * + * <p>Throws {@link LockFailureException} if at least one command failed with {@code + * LOCK_FAILURE}, and the entire transaction was aborted, i.e. any non-{@code LOCK_FAILURE} + * results, if there were any, failed with "transaction aborted". + * + * <p>In particular, if the underlying ref database does not {@link + * org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions() perform atomic transactions}, + * then a combination of {@code LOCK_FAILURE} on one ref and {@code OK} or another result on other + * refs will <em>not</em> throw {@code LockFailureException}. + * + * @param bru batch update; should already have been executed. + * @throws LockFailureException if the transaction was aborted due to lock failure. + * @throws IOException if any result was not {@code OK}. + */ + @VisibleForTesting + static void checkResults(BatchRefUpdate bru) throws IOException { + int lockFailure = 0; + int aborted = 0; + int failure = 0; + + for (ReceiveCommand cmd : bru.getCommands()) { + if (cmd.getResult() != ReceiveCommand.Result.OK) { + failure++; + } + if (cmd.getResult() == ReceiveCommand.Result.LOCK_FAILURE) { + lockFailure++; + } else if (cmd.getResult() == ReceiveCommand.Result.REJECTED_OTHER_REASON + && JGitText.get().transactionAborted.equals(cmd.getMessage())) { + aborted++; + } + } + + if (lockFailure + aborted == bru.getCommands().size()) { + throw new LockFailureException("Update aborted with one or more lock failures: " + bru); + } else if (failure > 0) { + throw new IOException("Update failed: " + bru); + } + } + + private RefUpdateUtil() {} +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoContext.java index 5009c50..9faf628 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoContext.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoContext.java
@@ -15,6 +15,7 @@ package com.google.gerrit.server.update; import java.io.IOException; +import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.transport.ReceiveCommand; @@ -31,11 +32,28 @@ /** * Add a command to the pending list of commands. * - * <p>Callers should use this method instead of writing directly to the repository returned by - * {@link #getRepository()}. + * <p>Adding commands to the {@code RepoContext} is the only way of updating refs in the + * repository from a {@link BatchUpdateOp}. * * @param cmd ref update command. * @throws IOException if an error occurred opening the repo. */ void addRefUpdate(ReceiveCommand cmd) throws IOException; + + /** + * Add a command to the pending list of commands. + * + * <p>Adding commands to the {@code RepoContext} is the only way of updating refs in the + * repository from a {@link BatchUpdateOp}. + * + * @param oldId the old object ID; must not be null. Use {@link ObjectId#zeroId()} for ref + * creation. + * @param newId the new object ID; must not be null. Use {@link ObjectId#zeroId()} for ref + * deletion. + * @param refName the ref name. + * @throws IOException if an error occurred opening the repo. + */ + default void addRefUpdate(ObjectId oldId, ObjectId newId, String refName) throws IOException { + addRefUpdate(new ReceiveCommand(oldId, newId, refName)); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoView.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoView.java new file mode 100644 index 0000000..8839dbe --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RepoView.java
@@ -0,0 +1,234 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.base.Preconditions.checkNotNull; + +import com.google.common.collect.Maps; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.git.GitRepositoryManager; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Restricted view of a {@link Repository} for use by {@link BatchUpdateOp} implementations. + * + * <p>This class serves two purposes in the context of {@link BatchUpdate}. First, the subset of + * normal Repository functionality is purely read-only, which prevents implementors from modifying + * the repository outside of {@link BatchUpdateOp#updateRepo}. Write operations can only be + * performed by calling methods on {@link RepoContext}. + * + * <p>Second, the read methods take into account any pending operations on the repository that + * implementations have staged using the write methods on {@link RepoContext}. Callers do not have + * to worry about whether operations have been performed yet, and the implementation details may + * differ between ReviewDb and NoteDb, but callers just don't need to care. + */ +public class RepoView { + private final Repository repo; + private final RevWalk rw; + private final ObjectInserter inserter; + private final ObjectInserter inserterWrapper; + private final ChainedReceiveCommands commands; + private final boolean closeRepo; + + RepoView(GitRepositoryManager repoManager, Project.NameKey project) throws IOException { + repo = repoManager.openRepository(project); + inserter = repo.newObjectInserter(); + inserterWrapper = new NonFlushingInserter(inserter); + rw = new RevWalk(inserter.newReader()); + commands = new ChainedReceiveCommands(repo); + closeRepo = true; + } + + RepoView(Repository repo, RevWalk rw, ObjectInserter inserter) { + checkArgument( + rw.getObjectReader().getCreatedFromInserter() == inserter, + "expected RevWalk %s to be created by ObjectInserter %s", + rw, + inserter); + this.repo = checkNotNull(repo); + this.rw = checkNotNull(rw); + this.inserter = checkNotNull(inserter); + inserterWrapper = new NonFlushingInserter(inserter); + commands = new ChainedReceiveCommands(repo); + closeRepo = false; + } + + /** + * Get this repo's configuration. + * + * <p>This is the storage-level config you would get with {@link Repository#getConfig()}, not, for + * example, the Gerrit-level project config. + * + * @return a defensive copy of the config; modifications have no effect on the underlying config. + */ + public Config getConfig() { + return new Config(repo.getConfig()); + } + + /** + * Get an open revwalk on the repo. + * + * <p>Guaranteed to be able to read back any objects inserted in the repository via {@link + * RepoContext#getInserter()}, even if objects have not been flushed to the underlying repo. In + * particular this includes any object returned by {@link #getRef(String)}, even taking into + * account not-yet-executed commands. + * + * @return revwalk. + */ + public RevWalk getRevWalk() { + return rw; + } + + /** + * Read a single ref from the repo. + * + * <p>Takes into account any ref update commands added during the course of the update using + * {@link RepoContext#addRefUpdate}, even if they have not yet been executed on the underlying + * repo. + * + * <p>The results of individual ref lookups are cached: calling this method multiple times with + * the same ref name will return the same result (unless a command was added in the meantime). The + * repo is not reread. + * + * @param name exact ref name. + * @return the value of the ref, if present. + * @throws IOException if an error occurred. + */ + public Optional<ObjectId> getRef(String name) throws IOException { + return getCommands().get(name); + } + + /** + * Look up refs by prefix. + * + * <p>Takes into account any ref update commands added during the course of the update using + * {@link RepoContext#addRefUpdate}, even if they have not yet been executed on the underlying + * repo. + * + * <p>For any ref that has previously been accessed with {@link #getRef(String)}, the value in the + * result map will be that same cached value. Any refs that have <em>not</em> been previously + * accessed are re-scanned from the repo on each call. + * + * @param prefix ref prefix; must end in '/' or else be empty. + * @return a map of ref suffixes to SHA-1s. The refs are all under {@code prefix} and have the + * prefix stripped; this matches the behavior of {@link + * org.eclipse.jgit.lib.RefDatabase#getRefs(String)}. + * @throws IOException if an error occurred. + */ + public Map<String, ObjectId> getRefs(String prefix) throws IOException { + Map<String, ObjectId> result = + new HashMap<>( + Maps.transformValues(repo.getRefDatabase().getRefs(prefix), Ref::getObjectId)); + + // First, overwrite any cached reads from the underlying RepoRefCache. If any of these differ, + // it's because a ref was updated after the RepoRefCache read it. It feels a little odd to + // prefer the *old* value in this case, but it would be weirder to be inconsistent with getRef. + // + // Mostly this doesn't matter. If the caller was intending to write to the ref, they lost a + // race, and they will get a lock failure. If they just want to read, well, the JGit interface + // doesn't currently guarantee that any snapshot of multiple refs is consistent, so they were + // probably out of luck anyway. + commands + .getRepoRefCache() + .getCachedRefs() + .forEach((k, v) -> updateRefIfPrefixMatches(result, prefix, k, v)); + + // Second, overwrite with any pending commands. + commands + .getCommands() + .values() + .forEach( + c -> + updateRefIfPrefixMatches(result, prefix, c.getRefName(), toOptional(c.getNewId()))); + + return result; + } + + private static Optional<ObjectId> toOptional(ObjectId id) { + return id.equals(ObjectId.zeroId()) ? Optional.empty() : Optional.of(id); + } + + private static void updateRefIfPrefixMatches( + Map<String, ObjectId> map, String prefix, String fullRefName, Optional<ObjectId> maybeId) { + if (!fullRefName.startsWith(prefix)) { + return; + } + String suffix = fullRefName.substring(prefix.length()); + if (maybeId.isPresent()) { + map.put(suffix, maybeId.get()); + } else { + map.remove(suffix); + } + } + + // Not AutoCloseable so callers can't improperly close it. Plus it's never managed with a try + // block anyway. + void close() { + if (closeRepo) { + inserter.close(); + rw.close(); + repo.close(); + } + } + + Repository getRepository() { + return repo; + } + + ObjectInserter getInserter() { + return inserter; + } + + ObjectInserter getInserterWrapper() { + return inserterWrapper; + } + + ChainedReceiveCommands getCommands() { + return commands; + } + + private static class NonFlushingInserter extends ObjectInserter.Filter { + private final ObjectInserter delegate; + + private NonFlushingInserter(ObjectInserter delegate) { + this.delegate = delegate; + } + + @Override + protected ObjectInserter delegate() { + return delegate; + } + + @Override + public void flush() { + // Do nothing. + } + + @Override + public void close() { + // Do nothing; the delegate is closed separately. + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java new file mode 100644 index 0000000..25a4e3c --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java
@@ -0,0 +1,112 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; + +import com.github.rholder.retry.RetryException; +import com.github.rholder.retry.RetryListener; +import com.github.rholder.retry.RetryerBuilder; +import com.github.rholder.retry.StopStrategies; +import com.github.rholder.retry.StopStrategy; +import com.github.rholder.retry.WaitStrategies; +import com.github.rholder.retry.WaitStrategy; +import com.google.common.base.Throwables; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.git.LockFailureException; +import com.google.gerrit.server.notedb.NotesMigration; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import java.util.concurrent.ExecutionException; +import org.eclipse.jgit.lib.Config; + +@Singleton +public class RetryHelper { + public interface Action<T> { + T call(BatchUpdate.Factory updateFactory) throws Exception; + } + + private final NotesMigration migration; + private final BatchUpdate.Factory updateFactory; + private final StopStrategy stopStrategy; + private final WaitStrategy waitStrategy; + + @Inject + RetryHelper( + @GerritServerConfig Config cfg, + NotesMigration migration, + ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory, + FusedNoteDbBatchUpdate.AssistedFactory fusedNoteDbBatchUpdateFactory, + UnfusedNoteDbBatchUpdate.AssistedFactory unfusedNoteDbBatchUpdateFactory) { + this.migration = migration; + this.updateFactory = + new BatchUpdate.Factory( + migration, + reviewDbBatchUpdateFactory, + fusedNoteDbBatchUpdateFactory, + unfusedNoteDbBatchUpdateFactory); + this.stopStrategy = + StopStrategies.stopAfterDelay( + cfg.getTimeUnit("noteDb", null, "retryTimeout", SECONDS.toMillis(5), MILLISECONDS), + MILLISECONDS); + this.waitStrategy = + WaitStrategies.join( + WaitStrategies.exponentialWait( + cfg.getTimeUnit("noteDb", null, "retryMaxWait", SECONDS.toMillis(20), MILLISECONDS), + MILLISECONDS), + WaitStrategies.randomWait(50, MILLISECONDS)); + } + + public <T> T execute(Action<T> action) throws RestApiException, UpdateException { + return execute(action, null); + } + + public <T> T execute(Action<T> action, @Nullable RetryListener listener) + throws RestApiException, UpdateException { + try { + RetryerBuilder<T> builder = RetryerBuilder.newBuilder(); + if (migration.disableChangeReviewDb() && migration.fuseUpdates()) { + builder + .withStopStrategy(stopStrategy) + .withWaitStrategy(waitStrategy) + .retryIfException(RetryHelper::isLockFailure); + if (listener != null) { + builder.withRetryListener(listener); + } + } else { + // Either we aren't full-NoteDb, or the underlying ref storage doesn't support atomic + // transactions. Either way, retrying a partially-failed operation is not idempotent, so + // don't do it automatically. Let the end user decide whether they want to retry. + } + return builder.build().call(() -> action.call(updateFactory)); + } catch (ExecutionException | RetryException e) { + if (e.getCause() != null) { + Throwables.throwIfInstanceOf(e.getCause(), UpdateException.class); + Throwables.throwIfInstanceOf(e.getCause(), RestApiException.class); + } + throw new UpdateException(e); + } + } + + private static boolean isLockFailure(Throwable t) { + if (t instanceof UpdateException) { + t = t.getCause(); + } + return t instanceof LockFailureException; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryingRestModifyView.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryingRestModifyView.java new file mode 100644 index 0000000..e2f4a02 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryingRestModifyView.java
@@ -0,0 +1,35 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.extensions.restapi.RestResource; + +public abstract class RetryingRestModifyView<R extends RestResource, I, O> + implements RestModifyView<R, I> { + private final RetryHelper retryHelper; + + protected RetryingRestModifyView(RetryHelper retryHelper) { + this.retryHelper = retryHelper; + } + + @Override + public final O apply(R resource, I input) throws Exception { + return retryHelper.execute((updateFactory) -> applyImpl(updateFactory, resource, input)); + } + + protected abstract O applyImpl(BatchUpdate.Factory updateFactory, R resource, I input) + throws Exception; +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java index 67d1e7e..1561fc4 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/ReviewDbBatchUpdate.java
@@ -14,23 +14,21 @@ package com.google.gerrit.server.update; -import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static java.util.Comparator.comparing; import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.stream.Collectors.toList; import com.google.common.base.Stopwatch; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; -import com.google.common.util.concurrent.CheckedFuture; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import com.google.gerrit.common.Nullable; 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.metrics.Description; import com.google.gerrit.metrics.Description.Units; @@ -59,17 +57,12 @@ import com.google.gerrit.server.notedb.NoteDbUpdateManager.MismatchedStateException; import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.project.ChangeControl; -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.util.RequestId; import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.SchemaFactory; 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.sql.Timestamp; import java.util.ArrayList; @@ -85,7 +78,6 @@ import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectInserter; -import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevWalk; @@ -118,19 +110,14 @@ } class ContextImpl implements Context { - private Repository repoWrapper; - @Override - public Repository getRepository() throws IOException { - if (repoWrapper == null) { - repoWrapper = new ReadOnlyRepository(ReviewDbBatchUpdate.this.getRepository()); - } - return repoWrapper; + public RepoView getRepoView() throws IOException { + return ReviewDbBatchUpdate.this.getRepoView(); } @Override public RevWalk getRevWalk() throws IOException { - return ReviewDbBatchUpdate.this.getRevWalk(); + return getRepoView().getRevWalk(); } @Override @@ -166,19 +153,14 @@ private class RepoContextImpl extends ContextImpl implements RepoContext { @Override - public Repository getRepository() throws IOException { - return ReviewDbBatchUpdate.this.getRepository(); - } - - @Override public ObjectInserter getInserter() throws IOException { - return ReviewDbBatchUpdate.this.getObjectInserter(); + return getRepoView().getInserterWrapper(); } @Override public void addRefUpdate(ReceiveCommand cmd) throws IOException { initRepository(); - commands.add(cmd); + repoView.getCommands().add(cmd); } } @@ -208,11 +190,6 @@ } @Override - public Repository getRepository() { - return threadLocalRepo; - } - - @Override public RevWalk getRevWalk() { return threadLocalRevWalk; } @@ -238,8 +215,8 @@ } @Override - public void bumpLastUpdatedOn(boolean bump) { - bumpLastUpdatedOn = bump; + public void dontBumpLastUpdatedOn() { + bumpLastUpdatedOn = false; } @Override @@ -273,18 +250,9 @@ if (updates.isEmpty()) { return; } - if (requestId != null) { - for (BatchUpdate u : updates) { - checkArgument( - u.requestId == null || u.requestId == requestId, - "refusing to overwrite RequestId %s in update with %s", - u.requestId, - requestId); - u.setRequestId(requestId); - } - } + setRequestIds(updates, requestId); try { - Order order = getOrder(updates); + Order order = getOrder(updates, listener); boolean updateChangesInParallel = getUpdateChangesInParallel(updates); switch (order) { case REPO_BEFORE_DB: @@ -305,59 +273,37 @@ for (ReviewDbBatchUpdate u : updates) { u.reindexChanges(u.executeChangeOps(updateChangesInParallel, dryrun)); } - listener.afterUpdateChanges(); for (ReviewDbBatchUpdate u : updates) { u.executeUpdateRepo(); } - listener.afterUpdateRepos(); for (ReviewDbBatchUpdate u : updates) { u.executeRefUpdates(dryrun); } - listener.afterUpdateRefs(); break; default: throw new IllegalStateException("invalid execution order: " + order); } - List<CheckedFuture<?, IOException>> indexFutures = new ArrayList<>(); - for (ReviewDbBatchUpdate u : updates) { - indexFutures.addAll(u.indexFutures); - } - ChangeIndexer.allAsList(indexFutures).get(); + ChangeIndexer.allAsList( + updates.stream().flatMap(u -> u.indexFutures.stream()).collect(toList())) + .get(); - for (ReviewDbBatchUpdate u : updates) { - if (u.batchRefUpdate != null) { - // 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. - u.gitRefUpdated.fire( - u.project, - u.batchRefUpdate, - u.getUser().isIdentifiedUser() ? u.getUser().asIdentifiedUser().getAccount() : null); - } - } + // 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 + .stream() + .filter(u -> u.batchRefUpdate != null) + .forEach( + u -> u.gitRefUpdated.fire(u.project, u.batchRefUpdate, u.getAccount().orElse(null))); + if (!dryrun) { for (ReviewDbBatchUpdate u : updates) { u.executePostOps(); } } - } catch (UpdateException | RestApiException e) { - // Propagate REST API exceptions thrown by operations; they commonly throw - // exceptions like ResourceConflictException to indicate an atomic update - // failure. - throw e; - - // Convert other common non-REST exception types with user-visible - // messages to corresponding REST exception types - } catch (InvalidChangeOperationException e) { - throw new ResourceConflictException(e.getMessage(), e); - } catch (NoSuchChangeException | NoSuchRefException | NoSuchProjectException e) { - throw new ResourceNotFoundException(e.getMessage(), e); - } catch (Exception e) { - Throwables.throwIfUnchecked(e); - throw new UpdateException(e); + wrapAndThrowException(e); } } @@ -374,9 +320,12 @@ private final ReviewDb db; private final SchemaFactory<ReviewDb> schemaFactory; private final long skewMs; - private final List<CheckedFuture<?, IOException>> indexFutures = new ArrayList<>(); - @AssistedInject + @SuppressWarnings("deprecation") + private final List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures = + new ArrayList<>(); + + @Inject ReviewDbBatchUpdate( @GerritServerConfig Config cfg, AllUsersName allUsers, @@ -413,11 +362,6 @@ } @Override - public void execute() throws UpdateException, RestApiException { - execute(BatchUpdateListener.NONE); - } - - @Override public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException { execute(ImmutableList.of(this), listener, requestId, false); } @@ -440,20 +384,18 @@ op.updateRepo(ctx); } - if (onSubmitValidators != null && commands != null && !commands.isEmpty()) { - try (ObjectReader reader = ctx.getInserter().newReader()) { - // Validation of refs has to take place here and not at the beginning - // executeRefUpdates. Otherwise failing validation in a second BatchUpdate object will - // happen *after* first object's executeRefUpdates has finished, hence after first repo's - // refs have been updated, which is too late. - onSubmitValidators.validate( - project, new ReadOnlyRepository(getRepository()), reader, commands.getCommands()); - } + if (onSubmitValidators != null && !getRefUpdates().isEmpty()) { + // Validation of refs has to take place here and not at the beginning of executeRefUpdates. + // Otherwise, failing validation in a second BatchUpdate object will happen *after* the + // first update's executeRefUpdates has finished, hence after first repo's refs have been + // updated, which is too late. + onSubmitValidators.validate( + project, ctx.getRevWalk().getObjectReader(), repoView.getCommands()); } - if (inserter != null) { + if (repoView != null) { logDebug("Flushing inserter"); - inserter.flush(); + repoView.getInserter().flush(); } else { logDebug("No objects to flush"); } @@ -464,24 +406,31 @@ } private void executeRefUpdates(boolean dryrun) throws IOException, RestApiException { - if (commands == null || commands.isEmpty()) { + if (getRefUpdates().isEmpty()) { logDebug("No ref updates to execute"); return; } // May not be opened if the caller added ref updates but no new objects. + // TODO(dborowitz): Really? initRepository(); - batchRefUpdate = repo.getRefDatabase().newBatchUpdate(); + batchRefUpdate = repoView.getRepository().getRefDatabase().newBatchUpdate(); + batchRefUpdate.setPushCertificate(pushCert); batchRefUpdate.setRefLogMessage(refLogMessage, true); + batchRefUpdate.setAllowNonFastForwards(true); + repoView.getCommands().addTo(batchRefUpdate); if (user.isIdentifiedUser()) { batchRefUpdate.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, tz)); } - commands.addTo(batchRefUpdate); logDebug("Executing batch of {} ref updates", batchRefUpdate.getCommands().size()); if (dryrun) { return; } - batchRefUpdate.execute(revWalk, NullProgressMonitor.INSTANCE); + // Force BatchRefUpdate to read newly referenced objects using a new RevWalk, rather than one + // that might have access to unflushed objects. + try (RevWalk updateRw = new RevWalk(repoView.getRepository())) { + batchRefUpdate.execute(updateRw, NullProgressMonitor.INSTANCE); + } boolean ok = true; for (ReceiveCommand cmd : batchRefUpdate.getCommands()) { if (cmd.getResult() != ReceiveCommand.Result.OK) { @@ -506,11 +455,11 @@ tasks = new ArrayList<>(ops.keySet().size()); try { - if (notesMigration.commitChangeWrites() && repo != null) { + if (notesMigration.commitChangeWrites() && repoView != null) { // A NoteDb change may have been rebuilt since the repo was originally // opened, so make sure we see that. logDebug("Preemptively scanning for repo changes"); - repo.scanForRepoChanges(); + repoView.getRepository().scanForRepoChanges(); } if (!ops.isEmpty() && notesMigration.failChangeWrites()) { // Fail fast before attempting any writes if changes are read-only, as @@ -578,9 +527,10 @@ // updates on the change repo first. logDebug("Executing NoteDb updates for {} changes", tasks.size()); try { - BatchRefUpdate changeRefUpdate = getRepository().getRefDatabase().newBatchUpdate(); + initRepository(); + BatchRefUpdate changeRefUpdate = repoView.getRepository().getRefDatabase().newBatchUpdate(); boolean hasAllUsersCommands = false; - try (ObjectInserter ins = getRepository().newObjectInserter()) { + try (ObjectInserter ins = repoView.getRepository().newObjectInserter()) { int objs = 0; for (ChangeTask task : tasks) { if (task.noteDbResult == null) { @@ -687,7 +637,8 @@ public Void call() throws Exception { taskId = id.toString() + "-" + Thread.currentThread().getId(); if (Thread.currentThread() == mainThread) { - Repository repo = getRepository(); + initRepository(); + Repository repo = repoView.getRepository(); try (RevWalk rw = new RevWalk(repo)) { call(ReviewDbBatchUpdate.this.db, repo, rw); } @@ -846,7 +797,10 @@ updateManagerFactory .create(ctx.getProject()) .setChangeRepo( - ctx.getRepository(), ctx.getRevWalk(), null, new ChainedReceiveCommands(repo)); + ctx.threadLocalRepo, + ctx.threadLocalRevWalk, + null, + new ChainedReceiveCommands(ctx.threadLocalRepo)); if (ctx.getUser().isIdentifiedUser()) { updateManager.setRefLogIdent( ctx.getUser().asIdentifiedUser().newRefLogIdent(ctx.getWhen(), tz));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java new file mode 100644 index 0000000..ce96c0e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java
@@ -0,0 +1,459 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.checkNotNull; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.extensions.restapi.RestApiException; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.PatchSet; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +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.index.change.ChangeIndexer; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.NoteDbUpdateManager; +import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.util.RequestId; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; +import java.util.TreeMap; +import org.eclipse.jgit.lib.NullProgressMonitor; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; + +/** + * {@link BatchUpdate} implementation that only supports NoteDb. + * + * <p>Used when {@code noteDb.changes.disableReviewDb=true}, at which point ReviewDb is not + * consulted during updates. + */ +class UnfusedNoteDbBatchUpdate extends BatchUpdate { + interface AssistedFactory { + UnfusedNoteDbBatchUpdate create( + ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when); + } + + static void execute( + ImmutableList<UnfusedNoteDbBatchUpdate> updates, + BatchUpdateListener listener, + @Nullable RequestId requestId, + boolean dryrun) + throws UpdateException, RestApiException { + if (updates.isEmpty()) { + return; + } + setRequestIds(updates, requestId); + + try { + Order order = getOrder(updates, listener); + // TODO(dborowitz): Fuse implementations to use a single BatchRefUpdate between phases. Note + // that we may still need to respect the order, since op implementations may make assumptions + // about the order in which their methods are called. + switch (order) { + case REPO_BEFORE_DB: + for (UnfusedNoteDbBatchUpdate u : updates) { + u.executeUpdateRepo(); + } + listener.afterUpdateRepos(); + for (UnfusedNoteDbBatchUpdate u : updates) { + u.executeRefUpdates(dryrun); + } + listener.afterUpdateRefs(); + for (UnfusedNoteDbBatchUpdate u : updates) { + u.reindexChanges(u.executeChangeOps(dryrun), dryrun); + } + listener.afterUpdateChanges(); + break; + case DB_BEFORE_REPO: + for (UnfusedNoteDbBatchUpdate u : updates) { + u.reindexChanges(u.executeChangeOps(dryrun), dryrun); + } + for (UnfusedNoteDbBatchUpdate u : updates) { + u.executeUpdateRepo(); + } + for (UnfusedNoteDbBatchUpdate u : updates) { + u.executeRefUpdates(dryrun); + } + break; + default: + throw new IllegalStateException("invalid execution order: " + order); + } + + ChangeIndexer.allAsList( + updates.stream().flatMap(u -> u.indexFutures.stream()).collect(toList())) + .get(); + + // 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 + .stream() + .filter(u -> u.batchRefUpdate != null) + .forEach( + u -> u.gitRefUpdated.fire(u.project, u.batchRefUpdate, u.getAccount().orElse(null))); + + if (!dryrun) { + for (UnfusedNoteDbBatchUpdate u : updates) { + u.executePostOps(); + } + } + } catch (Exception e) { + wrapAndThrowException(e); + } + } + + class ContextImpl implements Context { + @Override + public RepoView getRepoView() throws IOException { + return UnfusedNoteDbBatchUpdate.this.getRepoView(); + } + + @Override + public RevWalk getRevWalk() throws IOException { + return getRepoView().getRevWalk(); + } + + @Override + public Project.NameKey getProject() { + return project; + } + + @Override + public Timestamp getWhen() { + return when; + } + + @Override + public TimeZone getTimeZone() { + return tz; + } + + @Override + public ReviewDb getDb() { + return db; + } + + @Override + public CurrentUser getUser() { + return user; + } + + @Override + public Order getOrder() { + return order; + } + } + + private class RepoContextImpl extends ContextImpl implements RepoContext { + @Override + public ObjectInserter getInserter() throws IOException { + return getRepoView().getInserterWrapper(); + } + + @Override + public void addRefUpdate(ReceiveCommand cmd) throws IOException { + getRepoView().getCommands().add(cmd); + } + } + + private class ChangeContextImpl extends ContextImpl implements ChangeContext { + private final ChangeControl ctl; + private final Map<PatchSet.Id, ChangeUpdate> updates; + + private boolean deleted; + + protected ChangeContextImpl(ChangeControl ctl) { + this.ctl = checkNotNull(ctl); + updates = new TreeMap<>(comparing(PatchSet.Id::get)); + } + + @Override + public ChangeUpdate getUpdate(PatchSet.Id psId) { + ChangeUpdate u = updates.get(psId); + if (u == null) { + u = changeUpdateFactory.create(ctl, when); + if (newChanges.containsKey(ctl.getId())) { + u.setAllowWriteToNewRef(true); + } + u.setPatchSetId(psId); + updates.put(psId, u); + } + return u; + } + + @Override + public ChangeControl getControl() { + return ctl; + } + + @Override + public void dontBumpLastUpdatedOn() { + // Do nothing; NoteDb effectively updates timestamp if and only if a commit was written to the + // change meta ref. + } + + @Override + public void deleteChange() { + deleted = true; + } + } + + /** Per-change result status from {@link #executeChangeOps}. */ + private enum ChangeResult { + SKIPPED, + UPSERTED, + DELETED; + } + + private final ChangeNotes.Factory changeNotesFactory; + private final ChangeControl.GenericFactory changeControlFactory; + private final ChangeUpdate.Factory changeUpdateFactory; + private final NoteDbUpdateManager.Factory updateManagerFactory; + private final ChangeIndexer indexer; + private final GitReferenceUpdated gitRefUpdated; + private final ReviewDb db; + + @SuppressWarnings("deprecation") + private List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures; + + @Inject + UnfusedNoteDbBatchUpdate( + GitRepositoryManager repoManager, + @GerritPersonIdent PersonIdent serverIdent, + ChangeNotes.Factory changeNotesFactory, + ChangeControl.GenericFactory changeControlFactory, + ChangeUpdate.Factory changeUpdateFactory, + NoteDbUpdateManager.Factory updateManagerFactory, + ChangeIndexer indexer, + GitReferenceUpdated gitRefUpdated, + @Assisted ReviewDb db, + @Assisted Project.NameKey project, + @Assisted CurrentUser user, + @Assisted Timestamp when) { + super(repoManager, serverIdent, project, user, when); + this.changeNotesFactory = changeNotesFactory; + this.changeControlFactory = changeControlFactory; + this.changeUpdateFactory = changeUpdateFactory; + this.updateManagerFactory = updateManagerFactory; + this.indexer = indexer; + this.gitRefUpdated = gitRefUpdated; + this.db = db; + this.indexFutures = new ArrayList<>(); + } + + @Override + public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException { + execute(ImmutableList.of(this), listener, requestId, false); + } + + @Override + protected Context newContext() { + return new ContextImpl(); + } + + private void executeUpdateRepo() throws UpdateException, RestApiException { + try { + logDebug("Executing updateRepo on {} ops", ops.size()); + RepoContextImpl ctx = new RepoContextImpl(); + for (BatchUpdateOp op : ops.values()) { + op.updateRepo(ctx); + } + + logDebug("Executing updateRepo on {} RepoOnlyOps", repoOnlyOps.size()); + for (RepoOnlyOp op : repoOnlyOps) { + op.updateRepo(ctx); + } + + if (onSubmitValidators != null && !getRefUpdates().isEmpty()) { + // Validation of refs has to take place here and not at the beginning of executeRefUpdates. + // Otherwise, failing validation in a second BatchUpdate object will happen *after* the + // first update's executeRefUpdates has finished, hence after first repo's refs have been + // updated, which is too late. + onSubmitValidators.validate( + project, ctx.getRevWalk().getObjectReader(), repoView.getCommands()); + } + + // TODO(dborowitz): Don't flush when fusing phases. + if (repoView != null) { + logDebug("Flushing inserter"); + repoView.getInserter().flush(); + } else { + logDebug("No objects to flush"); + } + } catch (Exception e) { + Throwables.throwIfInstanceOf(e, RestApiException.class); + throw new UpdateException(e); + } + } + + // TODO(dborowitz): Don't execute non-change ref updates separately when fusing phases. + private void executeRefUpdates(boolean dryrun) throws IOException, RestApiException { + if (getRefUpdates().isEmpty()) { + logDebug("No ref updates to execute"); + return; + } + // May not be opened if the caller added ref updates but no new objects. + initRepository(); + batchRefUpdate = repoView.getRepository().getRefDatabase().newBatchUpdate(); + batchRefUpdate.setPushCertificate(pushCert); + batchRefUpdate.setRefLogMessage(refLogMessage, true); + batchRefUpdate.setAllowNonFastForwards(true); + repoView.getCommands().addTo(batchRefUpdate); + logDebug("Executing batch of {} ref updates", batchRefUpdate.getCommands().size()); + if (dryrun) { + return; + } + + // Force BatchRefUpdate to read newly referenced objects using a new RevWalk, rather than one + // that might have access to unflushed objects. + try (RevWalk updateRw = new RevWalk(repoView.getRepository())) { + batchRefUpdate.execute(updateRw, NullProgressMonitor.INSTANCE); + } + boolean ok = true; + for (ReceiveCommand cmd : batchRefUpdate.getCommands()) { + if (cmd.getResult() != ReceiveCommand.Result.OK) { + ok = false; + break; + } + } + if (!ok) { + throw new RestApiException("BatchRefUpdate failed: " + batchRefUpdate); + } + } + + private Map<Change.Id, ChangeResult> executeChangeOps(boolean dryrun) throws Exception { + logDebug("Executing change ops"); + Map<Change.Id, ChangeResult> result = + Maps.newLinkedHashMapWithExpectedSize(ops.keySet().size()); + initRepository(); + Repository repo = repoView.getRepository(); + // TODO(dborowitz): Teach NoteDbUpdateManager to allow reusing the same inserter and batch ref + // update as in executeUpdateRepo. + try (ObjectInserter ins = repo.newObjectInserter(); + ObjectReader reader = ins.newReader(); + RevWalk rw = new RevWalk(reader); + NoteDbUpdateManager updateManager = + updateManagerFactory + .create(project) + .setChangeRepo(repo, rw, ins, new ChainedReceiveCommands(repo))) { + if (user.isIdentifiedUser()) { + updateManager.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, tz)); + } + for (Map.Entry<Change.Id, Collection<BatchUpdateOp>> e : ops.asMap().entrySet()) { + Change.Id id = e.getKey(); + ChangeContextImpl ctx = newChangeContext(id); + boolean dirty = false; + logDebug("Applying {} ops for change {}", e.getValue().size(), id); + for (BatchUpdateOp op : e.getValue()) { + dirty |= op.updateChange(ctx); + } + if (!dirty) { + logDebug("No ops reported dirty, short-circuiting"); + result.put(id, ChangeResult.SKIPPED); + continue; + } + for (ChangeUpdate u : ctx.updates.values()) { + updateManager.add(u); + } + if (ctx.deleted) { + logDebug("Change {} was deleted", id); + updateManager.deleteChange(id); + result.put(id, ChangeResult.DELETED); + } else { + result.put(id, ChangeResult.UPSERTED); + } + } + + if (!dryrun) { + logDebug("Executing NoteDb updates"); + updateManager.execute(); + } + } + return result; + } + + private ChangeContextImpl newChangeContext(Change.Id id) throws OrmException { + logDebug("Opening change {} for update", id); + Change c = newChanges.get(id); + boolean isNew = c != null; + if (!isNew) { + // Pass a synthetic change into ChangeNotes.Factory, which will take care of checking for + // existence and populating columns from the parsed notes state. + // TODO(dborowitz): This dance made more sense when using Reviewdb; consider a nicer way. + c = ChangeNotes.Factory.newNoteDbOnlyChange(project, id); + } else { + logDebug("Change {} is new", id); + } + ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew); + ChangeControl ctl = changeControlFactory.controlFor(notes, user); + return new ChangeContextImpl(ctl); + } + + private void reindexChanges(Map<Change.Id, ChangeResult> updateResults, boolean dryrun) { + if (dryrun) { + return; + } + logDebug("Reindexing {} changes", updateResults.size()); + for (Map.Entry<Change.Id, ChangeResult> e : updateResults.entrySet()) { + Change.Id id = e.getKey(); + switch (e.getValue()) { + case UPSERTED: + indexFutures.add(indexer.indexAsync(project, id)); + break; + case DELETED: + indexFutures.add(indexer.deleteAsync(id)); + break; + case SKIPPED: + break; + default: + throw new IllegalStateException("unexpected result: " + e.getValue()); + } + } + } + + private void executePostOps() throws Exception { + ContextImpl ctx = new ContextImpl(); + for (BatchUpdateOp op : ops.values()) { + op.postUpdate(ctx); + } + + for (RepoOnlyOp op : repoOnlyOps) { + op.postUpdate(ctx); + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/CommitMessageUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/CommitMessageUtil.java new file mode 100644 index 0000000..fa55597 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/CommitMessageUtil.java
@@ -0,0 +1,40 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.util; + +import com.google.common.base.Strings; +import com.google.gerrit.extensions.restapi.BadRequestException; + +/** Utility functions to manipulate commit messages. */ +public class CommitMessageUtil { + + private CommitMessageUtil() {} + + /** + * Checks for null or empty commit messages and appends a newline character to the commit message. + * + * @throws BadRequestException if the commit message is null or empty + * @returns the trimmed message with a trailing newline character + */ + public static String checkAndSanitizeCommitMessage(String commitMessage) + throws BadRequestException { + String wellFormedMessage = Strings.nullToEmpty(commitMessage).trim(); + if (wellFormedMessage.isEmpty()) { + throw new BadRequestException("Commit message cannot be null or empty"); + } + wellFormedMessage = wellFormedMessage + "\n"; + return wellFormedMessage; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/IdGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/IdGenerator.java index e4d2890..a9a22d9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/IdGenerator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/IdGenerator.java
@@ -52,7 +52,7 @@ } /** A very simple bit permutation to mask a simple incrementer. */ - public static int mix(final int salt, final int in) { + public static int mix(int salt, int in) { short v0 = hi16(in); short v1 = lo16(in); v0 += ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1; @@ -61,7 +61,7 @@ } /* For testing only. */ - static int unmix(final int in) { + static int unmix(int in) { short v0 = hi16(in); short v1 = lo16(in); v1 -= ((v0 << 2) + 2 ^ v0) + (salt ^ (v0 >>> 3)) + 3; @@ -69,7 +69,7 @@ return result(v0, v1); } - private static short hi16(final int in) { + private static short hi16(int in) { return (short) ( // ((in >>> 24 & 0xff)) @@ -78,7 +78,7 @@ ); } - private static short lo16(final int in) { + private static short lo16(int in) { return (short) ( // ((in >>> 8 & 0xff)) @@ -87,7 +87,7 @@ ); } - private static int result(final short v0, final short v1) { + private static int result(short v0, short v1) { return ((v0 & 0xff) << 24) | // (((v0 >>> 8) & 0xff) << 16)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java index 4019851..f243726 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java
@@ -52,7 +52,7 @@ return compare(a.getName(), b.getName()); } - public int compare(final String pattern1, final String pattern2) { + public int compare(String pattern1, String pattern2) { int cmp = distance(pattern1) - distance(pattern2); if (cmp == 0) { boolean p1_finite = finite(pattern1);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java index dc7dd3d..8e8db12 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestId.java
@@ -47,7 +47,7 @@ private final String str; private RequestId(String resourceId) { - Hasher h = Hashing.sha1().newHasher(); + Hasher h = Hashing.murmur3_128().newHasher(); h.putLong(Thread.currentThread().getId()).putUnencodedChars(MACHINE_ID); str = "["
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java index 4d66809e..9c83549 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -29,6 +29,7 @@ import com.google.inject.servlet.ServletScopes; import java.util.concurrent.Callable; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledThreadPoolExecutor; /** * Base class for propagating request-scoped data between threads. @@ -64,10 +65,9 @@ * request state when the returned Callable is invoked. The method must be called in a request * scope and the returned Callable may only be invoked in a thread that is not already in a * request scope or is in the same request scope. The returned Callable will inherit toString() - * from the passed in Callable. A {@link com.google.gerrit.server.git.WorkQueue.Executor} does not - * accept a Callable, so there is no ProjectCallable implementation. Implementations of this - * method must be consistent with Guice's {@link ServletScopes#continueRequest(Callable, - * java.util.Map)}. + * from the passed in Callable. A {@link ScheduledThreadPoolExecutor} does not accept a Callable, + * so there is no ProjectCallable implementation. Implementations of this method must be + * consistent with Guice's {@link ServletScopes#continueRequest(Callable, java.util.Map)}. * * <p>There are some limitations: * @@ -82,7 +82,7 @@ * @return a new Callable which will execute in the current request scope. */ @SuppressWarnings("javadoc") // See GuiceRequestScopePropagator#wrapImpl - public final <T> Callable<T> wrap(final Callable<T> callable) { + public final <T> Callable<T> wrap(Callable<T> callable) { final RequestContext callerContext = checkNotNull(local.getContext()); final Callable<T> wrapped = wrapImpl(context(callerContext, cleanup(callable))); return new Callable<T>() { @@ -114,7 +114,7 @@ * @param runnable the Runnable to wrap. * @return a new Runnable which will execute in the current request scope. */ - public final Runnable wrap(final Runnable runnable) { + public final Runnable wrap(Runnable runnable) { final Callable<Object> wrapped = wrap(Executors.callable(runnable)); if (runnable instanceof ProjectRunnable) { @@ -172,52 +172,46 @@ /** @see #wrap(Callable) */ protected abstract <T> Callable<T> wrapImpl(Callable<T> callable); - protected <T> Callable<T> context(final RequestContext context, final Callable<T> callable) { - return new Callable<T>() { - @Override - public T call() throws Exception { - RequestContext old = - local.setContext( - new RequestContext() { - @Override - public CurrentUser getUser() { - return context.getUser(); - } + protected <T> Callable<T> context(RequestContext context, Callable<T> callable) { + return () -> { + RequestContext old = + local.setContext( + new RequestContext() { + @Override + public CurrentUser getUser() { + return context.getUser(); + } - @Override - public Provider<ReviewDb> getReviewDbProvider() { - return dbProviderProvider.get(); - } - }); - try { - return callable.call(); - } finally { - local.setContext(old); - } + @Override + public Provider<ReviewDb> getReviewDbProvider() { + return dbProviderProvider.get(); + } + }); + try { + return callable.call(); + } finally { + local.setContext(old); } }; } - protected <T> Callable<T> cleanup(final Callable<T> callable) { - return new Callable<T>() { - @Override - public T call() throws Exception { - RequestCleanup cleanup = - scope - .scope( - Key.get(RequestCleanup.class), - new Provider<RequestCleanup>() { - @Override - public RequestCleanup get() { - return new RequestCleanup(); - } - }) - .get(); - try { - return callable.call(); - } finally { - cleanup.run(); - } + protected <T> Callable<T> cleanup(Callable<T> callable) { + return () -> { + RequestCleanup cleanup = + scope + .scope( + Key.get(RequestCleanup.class), + new Provider<RequestCleanup>() { + @Override + public RequestCleanup get() { + return new RequestCleanup(); + } + }) + .get(); + try { + return callable.call(); + } finally { + cleanup.run(); } }; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/SocketUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/SocketUtil.java index 5b22f73..afa2aee 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/SocketUtil.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/SocketUtil.java
@@ -22,12 +22,12 @@ public final class SocketUtil { /** True if this InetAddress is a raw IPv6 in dotted quad notation. */ - public static boolean isIPv6(final InetAddress ip) { + public static boolean isIPv6(InetAddress ip) { return ip instanceof Inet6Address && ip.getHostName().equals(ip.getHostAddress()); } /** Get the name or IP address, or {@code *} if this address is a wildcard IP. */ - public static String hostname(final InetSocketAddress addr) { + public static String hostname(InetSocketAddress addr) { if (addr.getAddress() != null) { if (addr.getAddress().isAnyLocalAddress()) { return "*"; @@ -38,7 +38,7 @@ } /** Format an address string into {@code host:port} or {@code *:port} syntax. */ - public static String format(final SocketAddress s, final int defaultPort) { + public static String format(SocketAddress s, int defaultPort) { if (s instanceof InetSocketAddress) { final InetSocketAddress addr = (InetSocketAddress) s; if (addr.getPort() == defaultPort) { @@ -62,7 +62,7 @@ } /** Parse an address string such as {@code host:port} or {@code *:port}. */ - public static InetSocketAddress parse(final String desc, final int defaultPort) { + public static InetSocketAddress parse(String desc, int defaultPort) { String hostStr; String portStr; @@ -109,7 +109,7 @@ } /** Parse and resolve an address string, looking up the IP address. */ - public static InetSocketAddress resolve(final String desc, final int defaultPort) { + public static InetSocketAddress resolve(String desc, int defaultPort) { final InetSocketAddress addr = parse(desc, defaultPort); if (addr.getAddress() != null && addr.getAddress().isAnyLocalAddress()) { return addr;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java index 61c863b..6de2fef 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
@@ -56,7 +56,7 @@ public Set<SubmoduleSubscription> parseAllSections() { Set<SubmoduleSubscription> parsedSubscriptions = new HashSet<>(); - for (final String id : bbc.getSubsections("submodule")) { + for (String id : bbc.getSubsections("submodule")) { final SubmoduleSubscription subscription = parse(id); if (subscription != null) { parsedSubscriptions.add(subscription); @@ -65,7 +65,7 @@ return parsedSubscriptions; } - private SubmoduleSubscription parse(final String id) { + private SubmoduleSubscription parse(String id) { final String url = bbc.getString("submodule", id, "url"); final String path = bbc.getString("submodule", id, "path"); String branch = bbc.getString("submodule", id, "branch");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java index 65fbfd6..81a2df2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/SystemLog.java
@@ -46,7 +46,7 @@ private final Config config; @Inject - public SystemLog(final SitePaths site, @GerritServerConfig Config config) { + public SystemLog(SitePaths site, @GerritServerConfig Config config) { this.site = site; this.config = config; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java index 4b27208..90fb994 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
@@ -41,21 +41,18 @@ /** @see RequestScopePropagator#wrap(Callable) */ @Override - protected final <T> Callable<T> wrapImpl(final Callable<T> callable) { - final C ctx = continuingContext(requireContext()); - return new Callable<T>() { - @Override - public T call() throws Exception { - C old = threadLocal.get(); - threadLocal.set(ctx); - try { - return callable.call(); - } finally { - if (old != null) { - threadLocal.set(old); - } else { - threadLocal.remove(); - } + protected final <T> Callable<T> wrapImpl(Callable<T> callable) { + C ctx = continuingContext(requireContext()); + return () -> { + C old = threadLocal.get(); + threadLocal.set(ctx); + try { + return callable.call(); + } finally { + if (old != null) { + threadLocal.set(old); + } else { + threadLocal.remove(); } } };
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java index 8d511f3..49d4a55 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java
@@ -36,11 +36,11 @@ private final PrintWriter stdout; private String currentTabSeparator = " "; - public TreeFormatter(final PrintWriter stdout) { + public TreeFormatter(PrintWriter stdout) { this.stdout = stdout; } - public void printTree(final SortedSet<? extends TreeNode> rootNodes) { + public void printTree(SortedSet<? extends TreeNode> rootNodes) { if (rootNodes.isEmpty()) { return; } @@ -50,7 +50,7 @@ currentTabSeparator = DEFAULT_TAB_SEPARATOR; int i = 0; final int size = rootNodes.size(); - for (final TreeNode rootNode : rootNodes) { + for (TreeNode rootNode : rootNodes) { final boolean isLastRoot = ++i == size; if (isLastRoot) { currentTabSeparator = " "; @@ -60,28 +60,28 @@ } } - public void printTree(final TreeNode rootNode) { + public void printTree(TreeNode rootNode) { printTree(rootNode, 0, true); } - private void printTree(final TreeNode node, final int level, final boolean isLast) { + private void printTree(TreeNode node, int level, boolean isLast) { printNode(node, level, isLast); final SortedSet<? extends TreeNode> childNodes = node.getChildren(); int i = 0; final int size = childNodes.size(); - for (final TreeNode childNode : childNodes) { + for (TreeNode childNode : childNodes) { final boolean isLastChild = ++i == size; printTree(childNode, level + 1, isLastChild); } } - private void printIndention(final int level) { + private void printIndention(int level) { if (level > 0) { stdout.print(String.format("%-" + 4 * level + "s", currentTabSeparator)); } } - private void printNode(final TreeNode node, final int level, final boolean isLast) { + private void printNode(TreeNode node, int level, boolean isLast) { printIndention(level); stdout.print(isLast ? LAST_NODE_PREFIX : NODE_PREFIX); if (node.isVisible()) {
diff --git a/gerrit-server/src/main/java/gerrit/PRED__check_user_label_3.java b/gerrit-server/src/main/java/gerrit/PRED__check_user_label_3.java new file mode 100644 index 0000000..e84b3ac --- /dev/null +++ b/gerrit-server/src/main/java/gerrit/PRED__check_user_label_3.java
@@ -0,0 +1,107 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 gerrit; + +import com.google.gerrit.common.data.LabelType; +import com.google.gerrit.extensions.restapi.AuthException; +import com.google.gerrit.rules.StoredValues; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.permissions.LabelPermission; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gwtorm.server.OrmException; +import com.googlecode.prolog_cafe.exceptions.IllegalTypeException; +import com.googlecode.prolog_cafe.exceptions.JavaException; +import com.googlecode.prolog_cafe.exceptions.PInstantiationException; +import com.googlecode.prolog_cafe.exceptions.PrologException; +import com.googlecode.prolog_cafe.exceptions.SystemException; +import com.googlecode.prolog_cafe.lang.IntegerTerm; +import com.googlecode.prolog_cafe.lang.JavaObjectTerm; +import com.googlecode.prolog_cafe.lang.Operation; +import com.googlecode.prolog_cafe.lang.Predicate; +import com.googlecode.prolog_cafe.lang.Prolog; +import com.googlecode.prolog_cafe.lang.SymbolTerm; +import com.googlecode.prolog_cafe.lang.Term; +import com.googlecode.prolog_cafe.lang.VariableTerm; + +/** + * Checks user can set label to val. + * + * <pre> + * '_check_user_label'(+Label, +CurrentUser, +Val) + * </pre> + */ +class PRED__check_user_label_3 extends Predicate.P3 { + PRED__check_user_label_3(Term a1, Term a2, Term a3, Operation n) { + arg1 = a1; + arg2 = a2; + arg3 = a3; + cont = n; + } + + @Override + public Operation exec(Prolog engine) throws PrologException { + engine.setB0(); + Term a1 = arg1.dereference(); + Term a2 = arg2.dereference(); + Term a3 = arg3.dereference(); + + if (a1 instanceof VariableTerm) { + throw new PInstantiationException(this, 1); + } + if (!(a1 instanceof SymbolTerm)) { + throw new IllegalTypeException(this, 1, "atom", a1); + } + String label = a1.name(); + + if (a2 instanceof VariableTerm) { + throw new PInstantiationException(this, 2); + } + if (!(a2 instanceof JavaObjectTerm) || !a2.convertible(CurrentUser.class)) { + throw new IllegalTypeException(this, 2, "CurrentUser)", a2); + } + CurrentUser user = (CurrentUser) ((JavaObjectTerm) a2).object(); + + if (a3 instanceof VariableTerm) { + throw new PInstantiationException(this, 3); + } + if (!(a3 instanceof IntegerTerm)) { + throw new IllegalTypeException(this, 3, "integer", a3); + } + short val = (short) ((IntegerTerm) a3).intValue(); + + try { + ChangeData cd = StoredValues.CHANGE_DATA.get(engine); + LabelType type = cd.getLabelTypes().byLabel(label); + if (type == null) { + return engine.fail(); + } + StoredValues.PERMISSION_BACKEND + .get(engine) + .user(user) + .change(cd) + .check(new LabelPermission.WithValue(type, val)); + return cont; + } catch (OrmException err) { + throw new JavaException(this, 1, err); + } catch (AuthException err) { + return engine.fail(); + } catch (PermissionBackendException err) { + SystemException se = new SystemException(err.getMessage()); + se.initCause(err); + throw se; + } + } +}
diff --git a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java index 8b5a33d..5a3d656 100644 --- a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java +++ b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
@@ -38,7 +38,7 @@ Term listHead = Prolog.Nil; try { ChangeData cd = StoredValues.CHANGE_DATA.get(engine); - LabelTypes types = StoredValues.CHANGE_CONTROL.get(engine).getLabelTypes(); + LabelTypes types = cd.getLabelTypes(); for (PatchSetApproval a : cd.currentApprovals()) { LabelType t = types.byLabel(a.getLabelId());
diff --git a/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java index d06664e..f7f39da 100644 --- a/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java +++ b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
@@ -14,14 +14,18 @@ package gerrit; -import com.google.gerrit.common.data.Permission; -import com.google.gerrit.common.data.PermissionRange; +import com.google.gerrit.common.data.LabelType; import com.google.gerrit.rules.StoredValues; import com.google.gerrit.server.CurrentUser; -import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.permissions.LabelPermission; +import com.google.gerrit.server.permissions.PermissionBackendException; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gwtorm.server.OrmException; import com.googlecode.prolog_cafe.exceptions.IllegalTypeException; +import com.googlecode.prolog_cafe.exceptions.JavaException; import com.googlecode.prolog_cafe.exceptions.PInstantiationException; import com.googlecode.prolog_cafe.exceptions.PrologException; +import com.googlecode.prolog_cafe.exceptions.SystemException; import com.googlecode.prolog_cafe.lang.IntegerTerm; import com.googlecode.prolog_cafe.lang.JavaObjectTerm; import com.googlecode.prolog_cafe.lang.Operation; @@ -30,12 +34,13 @@ import com.googlecode.prolog_cafe.lang.SymbolTerm; import com.googlecode.prolog_cafe.lang.Term; import com.googlecode.prolog_cafe.lang.VariableTerm; +import java.util.Set; /** * Resolves the valid range for a label on a CurrentUser. * * <pre> - * '$user_label_range'(+Label, +CurrentUser, -Min, -Max) + * '_user_label_range'(+Label, +CurrentUser, -Min, -Max) * </pre> */ class PRED__user_label_range_4 extends Predicate.P4 { @@ -71,20 +76,34 @@ } CurrentUser user = (CurrentUser) ((JavaObjectTerm) a2).object(); - ChangeControl ctl = StoredValues.CHANGE_CONTROL.get(engine).forUser(user); - PermissionRange range = ctl.getRange(Permission.LABEL + label); - if (range == null) { + Set<LabelPermission.WithValue> can; + try { + ChangeData cd = StoredValues.CHANGE_DATA.get(engine); + LabelType type = cd.getLabelTypes().byLabel(label); + if (type == null) { + return engine.fail(); + } + can = StoredValues.PERMISSION_BACKEND.get(engine).user(user).change(cd).test(type); + } catch (OrmException err) { + throw new JavaException(this, 1, err); + } catch (PermissionBackendException err) { + SystemException se = new SystemException(err.getMessage()); + se.initCause(err); + throw se; + } + + int min = 0; + int max = 0; + for (LabelPermission.WithValue v : can) { + min = Math.min(min, v.value()); + max = Math.max(max, v.value()); + } + + if (!a3.unify(new IntegerTerm(min), engine.trail)) { return engine.fail(); } - IntegerTerm min = new IntegerTerm(range.getMin()); - IntegerTerm max = new IntegerTerm(range.getMax()); - - if (!a3.unify(min, engine.trail)) { - return engine.fail(); - } - - if (!a4.unify(max, engine.trail)) { + if (!a4.unify(new IntegerTerm(max), engine.trail)) { return engine.fail(); }
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java index 95be5cb..95c4aaef 100644 --- a/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java +++ b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
@@ -141,7 +141,7 @@ return Pattern.compile(term.name(), Pattern.MULTILINE); } - private Text load(final ObjectId tree, final String path, final ObjectReader reader) + private Text load(ObjectId tree, String path, ObjectReader reader) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { if (path == null) {
diff --git a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java index ea3fb17..9bfcc61 100644 --- a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java +++ b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
@@ -17,6 +17,8 @@ import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelValue; import com.google.gerrit.rules.StoredValues; +import com.google.gwtorm.server.OrmException; +import com.googlecode.prolog_cafe.exceptions.JavaException; import com.googlecode.prolog_cafe.exceptions.PrologException; import com.googlecode.prolog_cafe.lang.IntegerTerm; import com.googlecode.prolog_cafe.lang.ListTerm; @@ -51,7 +53,12 @@ public Operation exec(Prolog engine) throws PrologException { engine.setB0(); Term a1 = arg1.dereference(); - List<LabelType> list = StoredValues.CHANGE_CONTROL.get(engine).getLabelTypes().getLabelTypes(); + List<LabelType> list; + try { + list = StoredValues.CHANGE_DATA.get(engine).getLabelTypes().getLabelTypes(); + } catch (OrmException err) { + throw new JavaException(this, 1, err); + } Term head = Prolog.Nil; for (int idx = list.size() - 1; 0 <= idx; idx--) { head = new ListTerm(export(list.get(idx)), head);
diff --git a/gerrit-server/src/main/prolog/BUILD b/gerrit-server/src/main/prolog/BUILD deleted file mode 100644 index 603a0bf..0000000 --- a/gerrit-server/src/main/prolog/BUILD +++ /dev/null
@@ -1,8 +0,0 @@ -load("//lib/prolog:prolog.bzl", "prolog_cafe_library") - -prolog_cafe_library( - name = "common", - srcs = ["gerrit_common.pl"], - visibility = ["//visibility:public"], - deps = ["//gerrit-server:server"], -)
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl index 59c926f..4671e0d 100644 --- a/gerrit-server/src/main/prolog/gerrit_common.pl +++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -92,6 +92,27 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% +%% check_user_label/3: +%% +%% Check Who can set Label to Val. +%% +check_user_label(Label, Who, Val) :- + hash_get(commit_labels, '$fast_range', true), !, + atom(Label), + assume_range_from_label(Label, Who, Min, Max), + Min @=< Val, Val @=< Max. +check_user_label(Label, Who, Val) :- + Who = user(_), !, + atom(Label), + current_user(Who, User), + '_check_user_label'(Label, User, Val). +check_user_label(Label, test_user(Name), Val) :- + clause(user:test_grant(Label, test_user(Name), range(Min, Max)), _), + Min @=< Val, Val @=< Max + . + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%% %% user_label_range/4: %% %% Lookup the range allowed to be used. @@ -319,8 +340,7 @@ %% check_label_range_permission(Label, ExpValue, ok(Who)) :- commit_label(label(Label, ExpValue), Who), - user_label_range(Label, Who, Min, Max), - Min @=< ExpValue, ExpValue @=< Max + check_user_label(Label, Who, ExpValue) . %TODO Uncomment this clause when group suggesting is possible. %check_label_range_permission(Label, ExpValue, ask(Group)) :-
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties index f34c992..b2bcde3 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties
@@ -1,9 +1,13 @@ # Changes to this file should also be made in # gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}. + +reviewerCantSeeChange = {0} does not have permission to see this change +reviewerInactive = {0} identifies an inactive account +reviewerInvalid = {0} is not a valid user identifier reviewerNotFoundUser = {0} does not identify a registered user reviewerNotFoundUserOrGroup = {0} does not identify a registered user or group -groupIsNotAllowed = The group {0} cannot be added as reviewer. +groupIsNotAllowed = The group {0} cannot be added as reviewer. groupHasTooManyMembers = The group {0} has too many members to add them all as reviewers. groupManyMembersConfirmation = The group {0} has {1} members. Do you want to add them all as reviewers?
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy index 59790dc..9adff05 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentHtml.soy
@@ -38,7 +38,17 @@ {let $ulStyle kind="css"} list-style: none; - padding-left: 20px; + padding: 0; + {/let} + + {let $fileLiStyle kind="css"} + margin: 0; + padding: 0; + {/let} + + {let $commentLiStyle kind="css"} + margin: 0; + padding: 0 0 0 16px; {/let} {let $voteStyle kind="css"} @@ -104,14 +114,14 @@ <ul style="{$ulStyle}"> {foreach $group in $commentFiles} - <li> + <li style="{$fileLiStyle}"> <p> <a href="{$group.link}">{$group.title}:</a> </p> <ul style="{$ulStyle}"> {foreach $comment in $group.comments} - <li> + <li style="{$commentLiStyle}"> {if $comment.isRobotComment} <p style="{$commentHeaderStyle}"> Robot Comment from{sp}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy index fa2b44d..927601b 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergedHtml.soy
@@ -17,6 +17,7 @@ {namespace com.google.gerrit.server.mail.template} /** + * @param diffLines * @param email * @param fromName */ @@ -36,6 +37,6 @@ {call .Pre}{param content: $email.changeDetail /}{/call} {if $email.includeDiff} - {call .Pre}{param content: $email.unifiedDiff /}{/call} + {call .UnifiedDiff}{param diffLines: $diffLines /}{/call} {/if} -{/template} \ No newline at end of file +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy index 559bb26..8026666 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChangeHtml.soy
@@ -17,6 +17,7 @@ {namespace com.google.gerrit.server.mail.template} /** + * @param diffLines * @param email * @param fromName * @param ownerName @@ -55,6 +56,6 @@ {/if} {if $email.includeDiff} - {call .Pre}{param content: $email.unifiedDiff /}{/call} + {call .UnifiedDiff}{param diffLines: $diffLines /}{/call} {/if} {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy index 93353d7..b26535b 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Private.soy
@@ -24,7 +24,7 @@ * Private template to generate "View Change" buttons. * @param email */ -{template .ViewChangeButton private="true" autoescape="strict" kind="html"} +{template .ViewChangeButton autoescape="strict" kind="html"} <a href="{$email.changeUrl}">View Change</a> {/template} @@ -32,7 +32,7 @@ * Private template to render PRE block with consistent font-sizing. * @param content */ -{template .Pre private="true" autoescape="strict" kind="html"} +{template .Pre autoescape="strict" kind="html"} {let $preStyle kind="css"} font-family: monospace,monospace; // Use this to avoid browsers scaling down // monospace text. @@ -56,7 +56,7 @@ * * @param content */ -{template .WikiFormat private="true" autoescape="strict" kind="html"} +{template .WikiFormat autoescape="strict" kind="html"} {let $blockquoteStyle kind="css"} border-left: 1px solid #aaa; margin: 10px 0; @@ -86,3 +86,36 @@ {/if} {/foreach} {/template} + +/** + * @param diffLines + */ +{template .UnifiedDiff autoescape="strict" kind="html"} + {let $addStyle kind="css"} + color: hsl(120, 100%, 40%); + {/let} + + {let $removeStyle kind="css"} + color: hsl(0, 100%, 40%); + {/let} + + {let $preStyle kind="css"} + font-family: monospace,monospace; // Use this to avoid browsers scaling down + // monospace text. + white-space: pre-wrap; + {/let} + + <pre style="{$preStyle}"> + {foreach $line in $diffLines} + {if $line.type == 'add'} + <span style="{$addStyle}"> + {elseif $line.type == 'remove'} + <span style="{$removeStyle}"> + {else} + <span> + {/if} + {$line.text} + </span><br> + {/foreach} + </pre> +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy index bbf16d6..31cfbd6 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/SetAssigneeHtml.soy
@@ -17,6 +17,7 @@ {namespace com.google.gerrit.server.mail.template} /** + * @param diffLines * @param email * @param fromName * @param patchSet @@ -44,6 +45,6 @@ {/if} {if $email.includeDiff} - {call .Pre}{param content: $email.unifiedDiff /}{/call} + {call .UnifiedDiff}{param diffLines: $diffLines /}{/call} {/if} {/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties index 5a937b6..996e8a4 100644 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -26,6 +26,7 @@ cob = text/x-cobol coffee = text/x-coffeescript conf = text/plain +config = text/x-ini cpy = text/x-cobol cr = text/x-crystal cs = text/x-csharp @@ -156,7 +157,6 @@ pm = text/x-perl pp = text/x-puppet pro = text/x-idl -project.config = text/x-ini properties = text/x-ini proto = text/x-protobuf protobuf = text/x-protobuf
diff --git a/gerrit-server/src/test/java/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java b/gerrit-server/src/test/java/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java index 9974bc6..91b01f6 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
@@ -126,19 +126,16 @@ @Test public void callbackMetric0() { - final CallbackMetric0<Long> cntr = + CallbackMetric0<Long> cntr = metrics.newCallbackMetric( "test/count", Long.class, new Description("simple test").setCumulative()); - final AtomicInteger invocations = new AtomicInteger(0); + AtomicInteger invocations = new AtomicInteger(0); metrics.newTrigger( cntr, - new Runnable() { - @Override - public void run() { - invocations.getAndIncrement(); - cntr.set(42L); - } + () -> { + invocations.getAndIncrement(); + cntr.set(42L); }); // Triggers run immediately with DropWizard binding.
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java index fa4a951..40596e8 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
@@ -17,8 +17,8 @@ import static org.easymock.EasyMock.expect; import com.google.gerrit.common.data.LabelTypes; -import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.Util; +import com.google.gerrit.server.query.change.ChangeData; import com.google.inject.AbstractModule; import com.googlecode.prolog_cafe.exceptions.CompileException; import com.googlecode.prolog_cafe.exceptions.ReductionLimitException; @@ -47,27 +47,28 @@ cfg.setInt("rules", null, "reductionLimit", 1300); cfg.setInt("rules", null, "compileReductionLimit", (int) 1e6); bind(PrologEnvironment.Args.class) - .toInstance(new PrologEnvironment.Args(null, null, null, null, null, null, cfg)); + .toInstance( + new PrologEnvironment.Args(null, null, null, null, null, null, null, cfg)); } }); } @Override - protected void setUpEnvironment(PrologEnvironment env) { + protected void setUpEnvironment(PrologEnvironment env) throws Exception { LabelTypes labelTypes = new LabelTypes(Arrays.asList(Util.codeReview(), Util.verified())); - ChangeControl ctl = EasyMock.createMock(ChangeControl.class); - expect(ctl.getLabelTypes()).andStubReturn(labelTypes); - EasyMock.replay(ctl); - env.set(StoredValues.CHANGE_CONTROL, ctl); + ChangeData cd = EasyMock.createMock(ChangeData.class); + expect(cd.getLabelTypes()).andStubReturn(labelTypes); + EasyMock.replay(cd); + env.set(StoredValues.CHANGE_DATA, cd); } @Test - public void gerritCommon() { + public void gerritCommon() throws Exception { runPrologBasedTests(); } @Test - public void reductionLimit() throws CompileException { + public void reductionLimit() throws Exception { PrologEnvironment env = envFactory.create(machine); setUpEnvironment(env);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java index 6f6d189..7b2b388 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java +++ b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
@@ -84,7 +84,7 @@ * * @param env Prolog environment. */ - protected void setUpEnvironment(PrologEnvironment env) {} + protected void setUpEnvironment(PrologEnvironment env) throws Exception {} private PrologMachineCopy newMachine() { BufferingPrologControl ctl = new BufferingPrologControl(); @@ -115,7 +115,7 @@ return env.execute(Prolog.BUILTIN, "clause", head, new VariableTerm()); } - public void runPrologBasedTests() { + public void runPrologBasedTests() throws Exception { int errors = 0; long start = TimeUtil.nowMs();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java index 4689688..b32bdc6 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/IdentifiedUserTest.java
@@ -20,7 +20,6 @@ import com.google.gerrit.common.TimeUtil; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.account.AccountCache; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.FakeRealm; import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.Realm; @@ -36,7 +35,6 @@ import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; -import com.google.inject.util.Providers; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -93,8 +91,6 @@ .toInstance("http://localhost:8080/"); bind(AccountCache.class).toInstance(accountCache); bind(GroupBackend.class).to(SystemGroupBackend.class).in(SINGLETON); - bind(CapabilityControl.Factory.class) - .toProvider(Providers.<CapabilityControl.Factory>of(null)); bind(Realm.class).toInstance(mockRealm); } };
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java b/gerrit-server/src/test/java/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java new file mode 100644 index 0000000..801b2b0 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/edit/tree/ChangeFileContentModificationSubject.java
@@ -0,0 +1,69 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.edit.tree; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.io.CharStreams; +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.StringSubject; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; +import com.google.gerrit.extensions.restapi.RawInput; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +public class ChangeFileContentModificationSubject + extends Subject<ChangeFileContentModificationSubject, ChangeFileContentModification> { + + private static final SubjectFactory< + ChangeFileContentModificationSubject, ChangeFileContentModification> + MODIFICATION_SUBJECT_FACTORY = + new SubjectFactory< + ChangeFileContentModificationSubject, ChangeFileContentModification>() { + @Override + public ChangeFileContentModificationSubject getSubject( + FailureStrategy failureStrategy, ChangeFileContentModification modification) { + return new ChangeFileContentModificationSubject(failureStrategy, modification); + } + }; + + public static ChangeFileContentModificationSubject assertThat( + ChangeFileContentModification modification) { + return assertAbout(MODIFICATION_SUBJECT_FACTORY).that(modification); + } + + private ChangeFileContentModificationSubject( + FailureStrategy failureStrategy, ChangeFileContentModification modification) { + super(failureStrategy, modification); + } + + public StringSubject filePath() { + isNotNull(); + return Truth.assertThat(actual().getFilePath()).named("filePath"); + } + + public StringSubject newContent() throws IOException { + isNotNull(); + RawInput newContent = actual().getNewContent(); + Truth.assertThat(newContent).named("newContent").isNotNull(); + String contentString = + CharStreams.toString( + new InputStreamReader(newContent.getInputStream(), StandardCharsets.UTF_8)); + return Truth.assertThat(contentString).named("newContent"); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/edit/tree/TreeModificationSubject.java b/gerrit-server/src/test/java/com/google/gerrit/server/edit/tree/TreeModificationSubject.java new file mode 100644 index 0000000..ac4ebb8 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/edit/tree/TreeModificationSubject.java
@@ -0,0 +1,57 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.edit.tree; + +import static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.gerrit.truth.ListSubject; +import java.util.List; + +public class TreeModificationSubject extends Subject<TreeModificationSubject, TreeModification> { + + private static final SubjectFactory<TreeModificationSubject, TreeModification> + TREE_MODIFICATION_SUBJECT_FACTORY = + new SubjectFactory<TreeModificationSubject, TreeModification>() { + @Override + public TreeModificationSubject getSubject( + FailureStrategy failureStrategy, TreeModification treeModification) { + return new TreeModificationSubject(failureStrategy, treeModification); + } + }; + + public static TreeModificationSubject assertThat(TreeModification treeModification) { + return assertAbout(TREE_MODIFICATION_SUBJECT_FACTORY).that(treeModification); + } + + public static ListSubject<TreeModificationSubject, TreeModification> assertThatList( + List<TreeModification> treeModifications) { + return ListSubject.assertThat(treeModifications, TreeModificationSubject::assertThat) + .named("treeModifications"); + } + + private TreeModificationSubject( + FailureStrategy failureStrategy, TreeModification treeModification) { + super(failureStrategy, treeModification); + } + + public ChangeFileContentModificationSubject asChangeFileContentModification() { + isInstanceOf(ChangeFileContentModification.class); + return ChangeFileContentModificationSubject.assertThat( + (ChangeFileContentModification) actual()); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/events/EventDeserializerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/events/EventDeserializerTest.java index 355f775..997fda9 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/events/EventDeserializerTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/events/EventDeserializerTest.java
@@ -57,7 +57,7 @@ assertThat(e.submitter.get().email).isEqualTo(accountAttribute.email); } - private <T> Supplier<T> createSupplier(final T value) { + private <T> Supplier<T> createSupplier(T value) { return Suppliers.memoize( new Supplier<T>() { @Override
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java new file mode 100644 index 0000000..c1a65bb --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/fixes/FixReplacementInterpreterTest.java
@@ -0,0 +1,333 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.fixes; + +import static com.google.gerrit.server.edit.tree.TreeModificationSubject.assertThatList; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.replay; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.extensions.restapi.BinaryResult; +import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.reviewdb.client.Comment.Range; +import com.google.gerrit.reviewdb.client.FixReplacement; +import com.google.gerrit.server.change.FileContentUtil; +import com.google.gerrit.server.edit.tree.TreeModification; +import com.google.gerrit.server.project.ProjectState; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import org.easymock.EasyMock; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class FixReplacementInterpreterTest { + + @Rule public ExpectedException expectedException = ExpectedException.none(); + + private final FileContentUtil fileContentUtil = createMock(FileContentUtil.class); + private final Repository repository = createMock(Repository.class); + private final ProjectState projectState = createMock(ProjectState.class); + private final ObjectId patchSetCommitId = createMock(ObjectId.class); + private final String filePath1 = "an/arbitrary/file.txt"; + private final String filePath2 = "another/arbitrary/file.txt"; + + private FixReplacementInterpreter fixReplacementInterpreter; + + @Before + public void setUp() { + fixReplacementInterpreter = new FixReplacementInterpreter(fileContentUtil); + } + + @Test + public void noReplacementsResultInNoTreeModifications() throws Exception { + List<TreeModification> treeModifications = toTreeModifications(); + assertThatList(treeModifications).isEmpty(); + } + + @Test + public void treeModificationsTargetCorrectFiles() throws Exception { + FixReplacement fixReplacement = + new FixReplacement(filePath1, new Range(1, 6, 3, 2), "Modified content"); + FixReplacement fixReplacement2 = + new FixReplacement(filePath1, new Range(3, 5, 3, 5), "Second modification"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + FixReplacement fixReplacement3 = + new FixReplacement(filePath2, new Range(2, 0, 3, 0), "Another modified content"); + mockFileContent(filePath2, "1st line\n2nd line\n3rd line\n"); + + replay(fileContentUtil); + List<TreeModification> treeModifications = + toTreeModifications(fixReplacement, fixReplacement3, fixReplacement2); + List<TreeModification> sortedTreeModifications = getSortedCopy(treeModifications); + assertThatList(sortedTreeModifications) + .element(0) + .asChangeFileContentModification() + .filePath() + .isEqualTo(filePath1); + assertThatList(sortedTreeModifications) + .element(0) + .asChangeFileContentModification() + .newContent() + .startsWith("First"); + assertThatList(sortedTreeModifications) + .element(1) + .asChangeFileContentModification() + .filePath() + .isEqualTo(filePath2); + assertThatList(sortedTreeModifications) + .element(1) + .asChangeFileContentModification() + .newContent() + .startsWith("1st"); + } + + @Test + public void replacementsCanDeleteALine() throws Exception { + FixReplacement fixReplacement = new FixReplacement(filePath1, new Range(2, 0, 3, 0), ""); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + + replay(fileContentUtil); + List<TreeModification> treeModifications = toTreeModifications(fixReplacement); + assertThatList(treeModifications) + .onlyElement() + .asChangeFileContentModification() + .newContent() + .isEqualTo("First line\nThird line\n"); + } + + @Test + public void replacementsCanAddALine() throws Exception { + FixReplacement fixReplacement = + new FixReplacement(filePath1, new Range(2, 0, 2, 0), "A new line\n"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + + replay(fileContentUtil); + List<TreeModification> treeModifications = toTreeModifications(fixReplacement); + assertThatList(treeModifications) + .onlyElement() + .asChangeFileContentModification() + .newContent() + .isEqualTo("First line\nA new line\nSecond line\nThird line\n"); + } + + @Test + public void replacementsMaySpanMultipleLines() throws Exception { + FixReplacement fixReplacement = new FixReplacement(filePath1, new Range(1, 6, 3, 1), "and t"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + + replay(fileContentUtil); + List<TreeModification> treeModifications = toTreeModifications(fixReplacement); + assertThatList(treeModifications) + .onlyElement() + .asChangeFileContentModification() + .newContent() + .isEqualTo("First and third line\n"); + } + + @Test + public void replacementsMayOccurOnSameLine() throws Exception { + FixReplacement fixReplacement1 = new FixReplacement(filePath1, new Range(2, 0, 2, 6), "A"); + FixReplacement fixReplacement2 = + new FixReplacement(filePath1, new Range(2, 7, 2, 11), "modification"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + + replay(fileContentUtil); + List<TreeModification> treeModifications = + toTreeModifications(fixReplacement1, fixReplacement2); + assertThatList(treeModifications) + .onlyElement() + .asChangeFileContentModification() + .newContent() + .isEqualTo("First line\nA modification\nThird line\n"); + } + + @Test + public void replacementsMayTouch() throws Exception { + FixReplacement fixReplacement1 = + new FixReplacement(filePath1, new Range(1, 6, 2, 7), "modified "); + FixReplacement fixReplacement2 = + new FixReplacement(filePath1, new Range(2, 7, 3, 5), "content"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + + replay(fileContentUtil); + List<TreeModification> treeModifications = + toTreeModifications(fixReplacement1, fixReplacement2); + assertThatList(treeModifications) + .onlyElement() + .asChangeFileContentModification() + .newContent() + .isEqualTo("First modified content line\n"); + } + + @Test + public void replacementsCanAddContentAtEndOfFile() throws Exception { + FixReplacement fixReplacement = + new FixReplacement(filePath1, new Range(4, 0, 4, 0), "New content"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + + replay(fileContentUtil); + List<TreeModification> treeModifications = toTreeModifications(fixReplacement); + assertThatList(treeModifications) + .onlyElement() + .asChangeFileContentModification() + .newContent() + .isEqualTo("First line\nSecond line\nThird line\nNew content"); + } + + @Test + public void replacementsCanModifySeveralFilesInAnyOrder() throws Exception { + FixReplacement fixReplacement1 = + new FixReplacement(filePath1, new Range(1, 1, 3, 2), "Modified content"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + FixReplacement fixReplacement2 = + new FixReplacement(filePath2, new Range(2, 0, 3, 0), "First modification\n"); + FixReplacement fixReplacement3 = + new FixReplacement(filePath2, new Range(3, 0, 4, 0), "Second modification\n"); + mockFileContent(filePath2, "1st line\n2nd line\n3rd line\n"); + + replay(fileContentUtil); + List<TreeModification> treeModifications = + toTreeModifications(fixReplacement3, fixReplacement1, fixReplacement2); + List<TreeModification> sortedTreeModifications = getSortedCopy(treeModifications); + assertThatList(sortedTreeModifications) + .element(0) + .asChangeFileContentModification() + .newContent() + .isEqualTo("FModified contentird line\n"); + assertThatList(sortedTreeModifications) + .element(1) + .asChangeFileContentModification() + .newContent() + .isEqualTo("1st line\nFirst modification\nSecond modification\n"); + } + + @Test + public void lineSeparatorCanBeChanged() throws Exception { + FixReplacement fixReplacement = new FixReplacement(filePath1, new Range(2, 11, 3, 0), "\r"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + + replay(fileContentUtil); + List<TreeModification> treeModifications = toTreeModifications(fixReplacement); + assertThatList(treeModifications) + .onlyElement() + .asChangeFileContentModification() + .newContent() + .isEqualTo("First line\nSecond line\rThird line\n"); + } + + @Test + public void replacementsDoNotNeedToBeOrderedAccordingToRange() throws Exception { + FixReplacement fixReplacement1 = + new FixReplacement(filePath1, new Range(1, 0, 2, 0), "1st modification\n"); + FixReplacement fixReplacement2 = + new FixReplacement(filePath1, new Range(3, 0, 4, 0), "2nd modification\n"); + FixReplacement fixReplacement3 = + new FixReplacement(filePath1, new Range(4, 0, 5, 0), "3rd modification\n"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\nFourth line\nFifth line\n"); + + replay(fileContentUtil); + List<TreeModification> treeModifications = + toTreeModifications(fixReplacement2, fixReplacement1, fixReplacement3); + assertThatList(treeModifications) + .onlyElement() + .asChangeFileContentModification() + .newContent() + .isEqualTo( + "1st modification\nSecond line\n2nd modification\n3rd modification\nFifth line\n"); + } + + @Test + public void replacementsMustNotReferToNotExistingLine() throws Exception { + FixReplacement fixReplacement = + new FixReplacement(filePath1, new Range(5, 0, 5, 0), "A new line\n"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + + replay(fileContentUtil); + + expectedException.expect(ResourceConflictException.class); + toTreeModifications(fixReplacement); + } + + @Test + public void replacementsMustNotReferToZeroLine() throws Exception { + FixReplacement fixReplacement = + new FixReplacement(filePath1, new Range(0, 0, 0, 0), "A new line\n"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + + replay(fileContentUtil); + + expectedException.expect(ResourceConflictException.class); + toTreeModifications(fixReplacement); + } + + @Test + public void replacementsMustNotReferToNotExistingOffsetOfIntermediateLine() throws Exception { + FixReplacement fixReplacement = + new FixReplacement(filePath1, new Range(1, 0, 1, 11), "modified"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + + replay(fileContentUtil); + + expectedException.expect(ResourceConflictException.class); + toTreeModifications(fixReplacement); + } + + @Test + public void replacementsMustNotReferToNotExistingOffsetOfLastLine() throws Exception { + FixReplacement fixReplacement = + new FixReplacement(filePath1, new Range(3, 0, 3, 11), "modified"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + + replay(fileContentUtil); + + expectedException.expect(ResourceConflictException.class); + toTreeModifications(fixReplacement); + } + + @Test + public void replacementsMustNotReferToNegativeOffset() throws Exception { + FixReplacement fixReplacement = + new FixReplacement(filePath1, new Range(1, -1, 1, 5), "modified"); + mockFileContent(filePath1, "First line\nSecond line\nThird line\n"); + + replay(fileContentUtil); + + expectedException.expect(ResourceConflictException.class); + toTreeModifications(fixReplacement); + } + + private void mockFileContent(String filePath, String fileContent) throws Exception { + EasyMock.expect( + fileContentUtil.getContent(repository, projectState, patchSetCommitId, filePath)) + .andReturn(BinaryResult.create(fileContent)); + } + + private List<TreeModification> toTreeModifications(FixReplacement... fixReplacements) + throws Exception { + return fixReplacementInterpreter.toTreeModifications( + repository, projectState, patchSetCommitId, ImmutableList.copyOf(fixReplacements)); + } + + private static List<TreeModification> getSortedCopy(List<TreeModification> treeModifications) { + List<TreeModification> sortedTreeModifications = new ArrayList<>(treeModifications); + sortedTreeModifications.sort(Comparator.comparing(TreeModification::getFilePath)); + return sortedTreeModifications; + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/fixes/LineIdentifierTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/fixes/LineIdentifierTest.java new file mode 100644 index 0000000..f638346 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/fixes/LineIdentifierTest.java
@@ -0,0 +1,257 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.fixes; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class LineIdentifierTest { + + @Rule public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void lineNumberMustBePositive() { + LineIdentifier lineIdentifier = new LineIdentifier("First line\nSecond line"); + expectedException.expect(StringIndexOutOfBoundsException.class); + expectedException.expectMessage("positive"); + lineIdentifier.getStartIndexOfLine(0); + } + + @Test + public void lineNumberMustIndicateAnAvailableLine() { + LineIdentifier lineIdentifier = new LineIdentifier("First line\nSecond line"); + expectedException.expect(StringIndexOutOfBoundsException.class); + expectedException.expectMessage("Line 3 isn't available"); + lineIdentifier.getStartIndexOfLine(3); + } + + @Test + public void startIndexOfFirstLineIsRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567"); + int startIndex = lineIdentifier.getStartIndexOfLine(1); + assertThat(startIndex).isEqualTo(0); + } + + @Test + public void lengthOfFirstLineIsCorrect() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567"); + int lineLength = lineIdentifier.getLengthOfLine(1); + assertThat(lineLength).isEqualTo(8); + } + + @Test + public void startIndexOfSecondLineIsRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567"); + int startIndex = lineIdentifier.getStartIndexOfLine(2); + assertThat(startIndex).isEqualTo(9); + } + + @Test + public void lengthOfSecondLineIsCorrect() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567"); + int lineLength = lineIdentifier.getLengthOfLine(2); + assertThat(lineLength).isEqualTo(3); + } + + @Test + public void startIndexOfLastLineIsRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567"); + int startIndex = lineIdentifier.getStartIndexOfLine(3); + assertThat(startIndex).isEqualTo(13); + } + + @Test + public void lengthOfLastLineIsCorrect() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567"); + int lineLength = lineIdentifier.getLengthOfLine(3); + assertThat(lineLength).isEqualTo(7); + } + + @Test + public void emptyFirstLineIsRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier("\n123\n1234567"); + int startIndex = lineIdentifier.getStartIndexOfLine(1); + assertThat(startIndex).isEqualTo(0); + } + + @Test + public void lengthOfEmptyFirstLineIsCorrect() { + LineIdentifier lineIdentifier = new LineIdentifier("\n123\n1234567"); + int lineLength = lineIdentifier.getLengthOfLine(1); + assertThat(lineLength).isEqualTo(0); + } + + @Test + public void emptyIntermediaryLineIsRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n\n1234567"); + int startIndex = lineIdentifier.getStartIndexOfLine(2); + assertThat(startIndex).isEqualTo(9); + } + + @Test + public void lengthOfEmptyIntermediaryLineIsCorrect() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n\n1234567"); + int lineLength = lineIdentifier.getLengthOfLine(2); + assertThat(lineLength).isEqualTo(0); + } + + @Test + public void lineAfterIntermediaryLineIsRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n\n1234567"); + int startIndex = lineIdentifier.getStartIndexOfLine(3); + assertThat(startIndex).isEqualTo(10); + } + + @Test + public void emptyLastLineIsRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n"); + int startIndex = lineIdentifier.getStartIndexOfLine(3); + assertThat(startIndex).isEqualTo(13); + } + + @Test + public void lengthOfEmptyLastLineIsCorrect() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n"); + int lineLength = lineIdentifier.getLengthOfLine(3); + assertThat(lineLength).isEqualTo(0); + } + + @Test + public void startIndexOfSingleLineIsRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678"); + int startIndex = lineIdentifier.getStartIndexOfLine(1); + assertThat(startIndex).isEqualTo(0); + } + + @Test + public void lengthOfSingleLineIsCorrect() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678"); + int lineLength = lineIdentifier.getLengthOfLine(1); + assertThat(lineLength).isEqualTo(8); + } + + @Test + public void startIndexOfSingleEmptyLineIsRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier(""); + int startIndex = lineIdentifier.getStartIndexOfLine(1); + assertThat(startIndex).isEqualTo(0); + } + + @Test + public void lengthOfSingleEmptyLineIsCorrect() { + LineIdentifier lineIdentifier = new LineIdentifier(""); + int lineLength = lineIdentifier.getLengthOfLine(1); + assertThat(lineLength).isEqualTo(0); + } + + @Test + public void lookingUpSubsequentLinesIsPossible() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567\n12"); + + int firstLineStartIndex = lineIdentifier.getStartIndexOfLine(1); + assertThat(firstLineStartIndex).isEqualTo(0); + + int secondLineStartIndex = lineIdentifier.getStartIndexOfLine(2); + assertThat(secondLineStartIndex).isEqualTo(9); + } + + @Test + public void lookingUpNotSubsequentLinesInAscendingOrderIsPossible() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567\n12"); + + int firstLineStartIndex = lineIdentifier.getStartIndexOfLine(1); + assertThat(firstLineStartIndex).isEqualTo(0); + + int fourthLineStartIndex = lineIdentifier.getStartIndexOfLine(4); + assertThat(fourthLineStartIndex).isEqualTo(21); + } + + @Test + public void lookingUpNotSubsequentLinesInDescendingOrderIsPossible() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\n123\n1234567\n12"); + + int fourthLineStartIndex = lineIdentifier.getStartIndexOfLine(4); + assertThat(fourthLineStartIndex).isEqualTo(21); + + int secondLineStartIndex = lineIdentifier.getStartIndexOfLine(2); + assertThat(secondLineStartIndex).isEqualTo(9); + } + + @Test + public void linesSeparatedByOnlyCarriageReturnAreRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\r123\r12"); + int startIndex = lineIdentifier.getStartIndexOfLine(2); + assertThat(startIndex).isEqualTo(9); + } + + @Test + public void lengthOfLinesSeparatedByOnlyCarriageReturnIsCorrect() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\r123\r12"); + int lineLength = lineIdentifier.getLengthOfLine(2); + assertThat(lineLength).isEqualTo(3); + } + + @Test + public void linesSeparatedByLineFeedAndCarriageReturnAreRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\r\n123\r\n12"); + int startIndex = lineIdentifier.getStartIndexOfLine(2); + assertThat(startIndex).isEqualTo(10); + } + + @Test + public void lengthOfLinesSeparatedByLineFeedAndCarriageReturnIsCorrect() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\r\n123\r\n12"); + int lineLength = lineIdentifier.getLengthOfLine(2); + assertThat(lineLength).isEqualTo(3); + } + + @Test + public void linesSeparatedByMixtureOfCarriageReturnAndLineFeedAreRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\r123\r\n12\n123456\r\n1234"); + int startIndex = lineIdentifier.getStartIndexOfLine(5); + assertThat(startIndex).isEqualTo(25); + } + + @Test + public void linesSeparatedBySomeUnicodeLinebreakCharacterAreRecognized() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\u2029123\u202912"); + int startIndex = lineIdentifier.getStartIndexOfLine(2); + assertThat(startIndex).isEqualTo(9); + } + + @Test + public void lengthOfLinesSeparatedBySomeUnicodeLinebreakCharacterIsCorrect() { + LineIdentifier lineIdentifier = new LineIdentifier("12345678\u2029123\u202912"); + int lineLength = lineIdentifier.getLengthOfLine(2); + assertThat(lineLength).isEqualTo(3); + } + + @Test + public void blanksAreNotInterpretedAsLineSeparators() { + LineIdentifier lineIdentifier = new LineIdentifier("1 2345678\n123\n12"); + int startIndex = lineIdentifier.getStartIndexOfLine(2); + assertThat(startIndex).isEqualTo(10); + } + + @Test + public void tabsAreNotInterpretedAsLineSeparators() { + LineIdentifier lineIdentifier = new LineIdentifier("123\t45678\n123\n12"); + int startIndex = lineIdentifier.getStartIndexOfLine(2); + assertThat(startIndex).isEqualTo(10); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/fixes/StringModifierTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/fixes/StringModifierTest.java new file mode 100644 index 0000000..d23e928 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/fixes/StringModifierTest.java
@@ -0,0 +1,106 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.fixes; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class StringModifierTest { + + @Rule public ExpectedException expectedException = ExpectedException.none(); + + private final String originalString = "This is the original, unmodified string."; + private StringModifier stringModifier; + + @Before + public void setUp() { + stringModifier = new StringModifier(originalString); + } + + @Test + public void singlePartIsReplaced() { + stringModifier.replace(0, 11, "An"); + String modifiedString = stringModifier.getResult(); + assertThat(modifiedString).isEqualTo("An original, unmodified string."); + } + + @Test + public void twoPartsCanBeReplacedWithInsertionFirst() { + stringModifier.replace(5, 5, "string "); + stringModifier.replace(8, 39, "a modified version"); + String modifiedString = stringModifier.getResult(); + assertThat(modifiedString).isEqualTo("This string is a modified version."); + } + + @Test + public void twoPartsCanBeReplacedWithDeletionFirst() { + stringModifier.replace(0, 8, ""); + stringModifier.replace(12, 32, "modified"); + String modifiedString = stringModifier.getResult(); + assertThat(modifiedString).isEqualTo("the modified string."); + } + + @Test + public void replacedPartsMayTouch() { + stringModifier.replace(0, 8, ""); + stringModifier.replace(8, 32, "The modified"); + String modifiedString = stringModifier.getResult(); + assertThat(modifiedString).isEqualTo("The modified string."); + } + + @Test + public void replacedPartsMustNotOverlap() { + stringModifier.replace(0, 9, ""); + expectedException.expect(StringIndexOutOfBoundsException.class); + stringModifier.replace(8, 32, "The modified"); + } + + @Test + public void startIndexMustNotBeGreaterThanEndIndex() { + expectedException.expect(StringIndexOutOfBoundsException.class); + stringModifier.replace(10, 9, "something"); + } + + @Test + public void startIndexMustNotBeNegative() { + expectedException.expect(StringIndexOutOfBoundsException.class); + stringModifier.replace(-1, 9, "something"); + } + + @Test + public void newContentCanBeInsertedAtEndOfString() { + stringModifier.replace( + originalString.length(), originalString.length(), " And this an addition."); + String modifiedString = stringModifier.getResult(); + assertThat(modifiedString) + .isEqualTo("This is the original, unmodified string. And this an addition."); + } + + @Test + public void startIndexMustNotBeGreaterThanLengthOfString() { + expectedException.expect(StringIndexOutOfBoundsException.class); + stringModifier.replace(originalString.length() + 1, originalString.length() + 1, "something"); + } + + @Test + public void endIndexMustNotBeGreaterThanLengthOfString() { + expectedException.expect(StringIndexOutOfBoundsException.class); + stringModifier.replace(8, originalString.length() + 1, "something"); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java index ab68c10..fed0be4 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -27,8 +27,12 @@ import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.config.PluginConfig; import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gwtorm.client.KeyUtil; +import com.google.gwtorm.server.StandardKeyEncoder; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.Map; import org.eclipse.jgit.errors.ConfigInvalidException; @@ -44,6 +48,7 @@ import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.util.RawParseUtils; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; public class ProjectConfigTest extends LocalDiskRepositoryTestCase { @@ -74,6 +79,11 @@ private Repository db; private TestRepository<Repository> util; + @BeforeClass + public static void setUpOnce() { + KeyUtil.setEncoderImpl(new StandardKeyEncoder()); + } + @Override @Before public void setUp() throws Exception { @@ -325,6 +335,155 @@ + " read = group Developers\n"); } + @Test + public void readExistingPluginConfig() throws Exception { + RevCommit rev = + util.commit( + util.tree( // + util.file( + "project.config", + util.blob( + "" // + + "[plugin \"somePlugin\"]\n" // + + " key1 = value1\n" // + + " key2 = value2a\n" + + " key2 = value2b\n")) // + )); + update(rev); + + ProjectConfig cfg = read(rev); + PluginConfig pluginCfg = cfg.getPluginConfig("somePlugin"); + assertThat(pluginCfg.getNames().size()).isEqualTo(2); + assertThat(pluginCfg.getString("key1")).isEqualTo("value1"); + assertThat(pluginCfg.getStringList(("key2"))).isEqualTo(new String[] {"value2a", "value2b"}); + } + + @Test + public void readUnexistingPluginConfig() throws Exception { + ProjectConfig cfg = new ProjectConfig(new Project.NameKey("test")); + cfg.load(db); + PluginConfig pluginCfg = cfg.getPluginConfig("somePlugin"); + assertThat(pluginCfg.getNames()).isEmpty(); + } + + @Test + public void editPluginConfig() throws Exception { + RevCommit rev = + util.commit( + util.tree( // + util.file( + "project.config", + util.blob( + "" // + + "[plugin \"somePlugin\"]\n" // + + " key1 = value1\n" // + + " key2 = value2a\n" // + + " key2 = value2b\n")) // + )); + update(rev); + + ProjectConfig cfg = read(rev); + PluginConfig pluginCfg = cfg.getPluginConfig("somePlugin"); + pluginCfg.setString("key1", "updatedValue1"); + pluginCfg.setStringList("key2", Arrays.asList("updatedValue2a", "updatedValue2b")); + rev = commit(cfg); + assertThat(text(rev, "project.config")) + .isEqualTo( + "" // + + "[plugin \"somePlugin\"]\n" // + + "\tkey1 = updatedValue1\n" // + + "\tkey2 = updatedValue2a\n" // + + "\tkey2 = updatedValue2b\n"); + } + + @Test + public void readPluginConfigGroupReference() throws Exception { + RevCommit rev = + util.commit( + util.tree( // + util.file("groups", util.blob(group(developers))), // + util.file( + "project.config", + util.blob( + "" // + + "[plugin \"somePlugin\"]\n" // + + "key1 = " + + developers.toConfigValue() + + "\n")) // + )); + update(rev); + + ProjectConfig cfg = read(rev); + PluginConfig pluginCfg = cfg.getPluginConfig("somePlugin"); + assertThat(pluginCfg.getNames().size()).isEqualTo(1); + assertThat(pluginCfg.getGroupReference("key1")).isEqualTo(developers); + } + + @Test + public void readPluginConfigGroupReferenceNotInGroupsFile() throws Exception { + RevCommit rev = + util.commit( + util.tree( // + util.file("groups", util.blob(group(developers))), // + util.file( + "project.config", + util.blob( + "" // + + "[plugin \"somePlugin\"]\n" // + + "key1 = " + + staff.toConfigValue() + + "\n")) // + )); + update(rev); + + ProjectConfig cfg = read(rev); + assertThat(cfg.getValidationErrors()).hasSize(1); + assertThat(Iterables.getOnlyElement(cfg.getValidationErrors()).getMessage()) + .isEqualTo( + "project.config: group \"" + staff.getName() + "\" not in " + GroupList.FILE_NAME); + } + + @Test + public void editPluginConfigGroupReference() throws Exception { + RevCommit rev = + util.commit( + util.tree( // + util.file("groups", util.blob(group(developers))), // + util.file( + "project.config", + util.blob( + "" // + + "[plugin \"somePlugin\"]\n" // + + "key1 = " + + developers.toConfigValue() + + "\n")) // + )); + update(rev); + + ProjectConfig cfg = read(rev); + PluginConfig pluginCfg = cfg.getPluginConfig("somePlugin"); + assertThat(pluginCfg.getNames().size()).isEqualTo(1); + assertThat(pluginCfg.getGroupReference("key1")).isEqualTo(developers); + + pluginCfg.setGroupReference("key1", staff); + rev = commit(cfg); + assertThat(text(rev, "project.config")) + .isEqualTo( + "" // + + "[plugin \"somePlugin\"]\n" // + + "\tkey1 = " + + staff.toConfigValue() + + "\n"); + assertThat(text(rev, "groups")) + .isEqualTo( + "# UUID\tGroup Name\n" // + + "#\n" // + + staff.getUUID().get() + + " \t" + + staff.getName() + + "\n"); + } + private ProjectConfig read(RevCommit rev) throws IOException, ConfigInvalidException { ProjectConfig cfg = new ProjectConfig(new Project.NameKey("test")); cfg.load(db, rev);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java index 1724c51..b80e31e 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -29,7 +29,6 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.index.IndexConfig; import com.google.gerrit.server.index.QueryOptions; -import com.google.gerrit.server.query.AndPredicate; import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.QueryParseException; import com.google.gerrit.server.query.change.AndChangeSource; @@ -58,7 +57,7 @@ indexes = new ChangeIndexCollection(); indexes.setSearchIndex(index); queryBuilder = new FakeQueryBuilder(indexes); - rewrite = new ChangeIndexRewriter(indexes, IndexConfig.create(0, 0, 3)); + rewrite = new ChangeIndexRewriter(indexes, IndexConfig.builder().maxTerms(3).build()); } @Test @@ -196,9 +195,10 @@ assertThat(rewrite(in)).isEqualTo(query(in)); indexes.setSearchIndex(new FakeChangeIndex(FakeChangeIndex.V1)); - Predicate<ChangeData> out = rewrite(in); - assertThat(out).isInstanceOf(AndPredicate.class); - assertThat(out.getChildren()).containsExactly(query(in.getChild(0)), in.getChild(1)).inOrder(); + + exception.expect(QueryParseException.class); + exception.expectMessage("Unsupported index predicate: file:a"); + rewrite(in); } @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java index 4eef629..0af642d 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/StalenessCheckerTest.java
@@ -321,11 +321,13 @@ Change indexChange = newChange(P1, new Account.Id(1)); indexChange.setNoteDbState(SHA1); - assertThat(StalenessChecker.reviewDbChangeIsStale(indexChange, null)).isFalse(); + // Change is missing from ReviewDb but present in index. + assertThat(StalenessChecker.reviewDbChangeIsStale(indexChange, null)).isTrue(); + // Change differs only in primary storage. Change noteDbPrimary = clone(indexChange); noteDbPrimary.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); - assertThat(StalenessChecker.reviewDbChangeIsStale(indexChange, noteDbPrimary)).isFalse(); + assertThat(StalenessChecker.reviewDbChangeIsStale(indexChange, noteDbPrimary)).isTrue(); assertThat(StalenessChecker.reviewDbChangeIsStale(indexChange, clone(indexChange))).isFalse();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java index 800413b..fae8559 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/BasicSerializationTest.java
@@ -129,14 +129,14 @@ assertOutput(b(7, 'c', 'o', 'f', 'f', 'e', 'e', '4'), out); } - private static void assertOutput(final byte[] expect, final ByteArrayOutputStream out) { + private static void assertOutput(byte[] expect, ByteArrayOutputStream out) { final byte[] buf = out.toByteArray(); for (int i = 0; i < expect.length; i++) { assertEquals(expect[i], buf[i]); } } - private static InputStream r(final byte[] buf) { + private static InputStream r(byte[] buf) { return new ByteArrayInputStream(buf); }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java index 2909df7..42e8a8e 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/AddressTest.java
@@ -96,7 +96,7 @@ assertInvalid("a <@a>"); } - private void assertInvalid(final String in) { + private void assertInvalid(String in) { try { Address.parse(in); fail("Expected IllegalArgumentException for " + in); @@ -151,7 +151,7 @@ assertThat(format(null, "a,b@a")).isEqualTo("<a,b@a>"); } - private static String format(final String name, final String email) { + private static String format(String name, String email) { return new Address(name, email).toHeaderString(); } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/HtmlParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/HtmlParserTest.java index 62bc580..6c60db7 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/HtmlParserTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/HtmlParserTest.java
@@ -105,6 +105,23 @@ assertInlineComment("Also have a comment here.", parsedComments.get(1), comments.get(3)); } + @Test + public void commentsSpanningMultipleBlocks() { + String htmlMessage = + "This is a very long test comment. <div><br></div><div>Now this is a new paragraph yay.</div>"; + String txtMessage = "This is a very long test comment.\n\nNow this is a new paragraph yay."; + MailMessage.Builder b = newMailMessageBuilder(); + b.htmlContent(newHtmlBody(htmlMessage, null, null, htmlMessage, htmlMessage, null, null)); + + List<Comment> comments = defaultComments(); + List<MailComment> parsedComments = HtmlParser.parse(b.build(), comments, CHANGE_URL); + + assertThat(parsedComments).hasSize(3); + assertChangeMessage(txtMessage, parsedComments.get(0)); + assertFileComment(txtMessage, parsedComments.get(1), comments.get(1).key.filename); + assertInlineComment(txtMessage, parsedComments.get(2), comments.get(3)); + } + /** * Create an html message body with the specified comments. *
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java index a7b37a8..aaf723a 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -51,7 +51,7 @@ return new FromAddressGeneratorProvider(config, "Anonymous Coward", ident, accountCache).get(); } - private void setFrom(final String newFrom) { + private void setFrom(String newFrom) { config.setString("sendemail", null, "from", newFrom); } @@ -366,18 +366,18 @@ verify(accountCache); } - private Account.Id user(final String name, final String email) { + private Account.Id user(String name, String email) { final AccountState s = makeUser(name, email); expect(accountCache.get(eq(s.getAccount().getId()))).andReturn(s); return s.getAccount().getId(); } - private Account.Id userNoLookup(final String name, final String email) { + private Account.Id userNoLookup(String name, String email) { final AccountState s = makeUser(name, email); return s.getAccount().getId(); } - private AccountState makeUser(final String name, final String email) { + private AccountState makeUser(String name, String email) { final Account.Id userId = new Account.Id(42); final Account account = new Account(userId, TimeUtil.nowTs()); account.setFullName(name);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/ValidatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/ValidatorTest.java deleted file mode 100644 index 8cf1097..0000000 --- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/send/ValidatorTest.java +++ /dev/null
@@ -1,62 +0,0 @@ -// Copyright (C) 2016 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assert_; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import org.junit.Test; - -public class ValidatorTest { - private static final String UNSUPPORTED_PREFIX = "#! "; - - @Test - public void validateLocalDomain() throws Exception { - assertThat(OutgoingEmailValidator.isValid("foo@bar.local")).isTrue(); - } - - @Test - public void validateTopLevelDomains() throws Exception { - try (InputStream in = this.getClass().getResourceAsStream("tlds-alpha-by-domain.txt")) { - if (in == null) { - throw new Exception("TLD list not found"); - } - BufferedReader r = new BufferedReader(new InputStreamReader(in)); - String tld; - while ((tld = r.readLine()) != null) { - if (tld.startsWith("# ") || tld.startsWith("XN--")) { - // Ignore comments and non-latin domains - continue; - } - if (tld.startsWith(UNSUPPORTED_PREFIX)) { - String test = "test@example." + tld.toLowerCase().substring(UNSUPPORTED_PREFIX.length()); - assert_() - .withFailureMessage("expected invalid TLD \"" + test + "\"") - .that(OutgoingEmailValidator.isValid(test)) - .isFalse(); - } else { - String test = "test@example." + tld.toLowerCase(); - assert_() - .withFailureMessage("failed to validate TLD \"" + test + "\"") - .that(OutgoingEmailValidator.isValid(test)) - .isTrue(); - } - } - } - } -}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java index be153c9..f59063a 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -35,7 +35,6 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.InternalUser; import com.google.gerrit.server.account.AccountCache; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.FakeRealm; import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.Realm; @@ -60,9 +59,11 @@ import com.google.gerrit.testutil.TestNotesMigration; import com.google.gerrit.testutil.TestTimeUtil; import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; +import com.google.inject.TypeLiteral; import com.google.inject.util.Providers; import java.sql.Timestamp; import java.util.TimeZone; @@ -156,8 +157,6 @@ bind(NotesMigration.class).toInstance(MIGRATION); bind(GitRepositoryManager.class).toInstance(repoManager); bind(ProjectCache.class).toProvider(Providers.<ProjectCache>of(null)); - bind(CapabilityControl.Factory.class) - .toProvider(Providers.<CapabilityControl.Factory>of(null)); bind(Config.class).annotatedWith(GerritServerConfig.class).toInstance(testConfig); bind(String.class) .annotatedWith(AnonymousCowardName.class) @@ -177,6 +176,18 @@ bind(GitReferenceUpdated.class).toInstance(GitReferenceUpdated.DISABLED); bind(MetricMaker.class).to(DisabledMetricMaker.class); bind(ReviewDb.class).toProvider(Providers.<ReviewDb>of(null)); + + // Tests don't support ReviewDb at all, but bindings are required via NoteDbModule. + bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}) + .toInstance( + () -> { + throw new UnsupportedOperationException(); + }); + bind(ChangeBundleReader.class) + .toInstance( + (db, id) -> { + throw new UnsupportedOperationException(); + }); } }); @@ -185,7 +196,7 @@ changeOwner = userFactory.create(co.getId()); otherUser = userFactory.create(ou.getId()); otherUserId = otherUser.getAccountId(); - internalUser = new InternalUser(null); + internalUser = new InternalUser(); } private void setTimeForTesting() { @@ -199,15 +210,24 @@ System.setProperty("user.timezone", systemTimeZone); } - protected Change newChange() throws Exception { + protected Change newChange(boolean workInProgress) throws Exception { Change c = TestChanges.newChange(project, changeOwner.getAccountId()); ChangeUpdate u = newUpdate(c, changeOwner); u.setChangeId(c.getKey().get()); u.setBranch(c.getDest().get()); + u.setWorkInProgress(workInProgress); u.commit(); return c; } + protected Change newWorkInProgressChange() throws Exception { + return newChange(true); + } + + protected Change newChange() throws Exception { + return newChange(false); + } + protected ChangeUpdate newUpdate(Change c, CurrentUser user) throws Exception { ChangeUpdate update = TestChanges.newUpdate(injector, c, user); update.setPatchSetId(c.currentPatchSetId());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java index 39c4c08..a04b755 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.notedb; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import com.google.gerrit.common.TimeUtil; @@ -437,6 +438,45 @@ } @Test + public void parseWorkInProgress() throws Exception { + // Change created in WIP remains in WIP. + RevCommit commit = writeCommit("Update WIP change\n" + "\n" + "Patch-set: 1\n", true); + ChangeNotesState state = newParser(commit).parseAll(); + assertThat(state.hasReviewStarted()).isFalse(); + + // Moving change out of WIP starts review. + commit = + writeCommit("New ready change\n" + "\n" + "Patch-set: 1\n" + "Work-in-progress: false\n"); + state = newParser(commit).parseAll(); + assertThat(state.hasReviewStarted()).isTrue(); + + // Change created not in WIP has always been in review started state. + state = assertParseSucceeds("New change that doesn't declare WIP\n" + "\n" + "Patch-set: 1\n"); + assertThat(state.hasReviewStarted()).isTrue(); + } + + @Test + public void pendingReviewers() throws Exception { + // Change created in WIP. + RevCommit commit = writeCommit("Update WIP change\n" + "\n" + "Patch-set: 1\n", true); + ChangeNotesState state = newParser(commit).parseAll(); + assertThat(state.pendingReviewers().all()).isEmpty(); + assertThat(state.pendingReviewersByEmail().all()).isEmpty(); + + // Reviewers added while in WIP. + commit = + writeCommit( + "Add reviewers\n" + + "\n" + + "Patch-set: 1\n" + + "Reviewer: Change Owner " + + "<1@gerrit>\n", + true); + state = newParser(commit).parseAll(); + assertThat(state.pendingReviewers().byState(ReviewerStateInternal.REVIEWER)).isNotEmpty(); + } + + @Test public void caseInsensitiveFooters() throws Exception { assertParseSucceeds( "Update change\n" @@ -460,11 +500,26 @@ return writeCommit( body, noteUtil.newIdent( - changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent, "Anonymous Coward")); + changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent, "Anonymous Coward"), + false); } private RevCommit writeCommit(String body, PersonIdent author) throws Exception { - Change change = newChange(); + return writeCommit(body, author, false); + } + + private RevCommit writeCommit(String body, boolean initWorkInProgress) throws Exception { + ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class); + return writeCommit( + body, + noteUtil.newIdent( + changeOwner.getAccount(), TimeUtil.nowTs(), serverIdent, "Anonymous Coward"), + initWorkInProgress); + } + + private RevCommit writeCommit(String body, PersonIdent author, boolean initWorkInProgress) + throws Exception { + Change change = newChange(initWorkInProgress); ChangeNotes notes = newNotes(change).load(); try (ObjectInserter ins = testRepo.getRepository().newObjectInserter()) { CommitBuilder cb = new CommitBuilder(); @@ -481,12 +536,12 @@ } } - private void assertParseSucceeds(String body) throws Exception { - assertParseSucceeds(writeCommit(body)); + private ChangeNotesState assertParseSucceeds(String body) throws Exception { + return assertParseSucceeds(writeCommit(body)); } - private void assertParseSucceeds(RevCommit commit) throws Exception { - newParser(commit).parseAll(); + private ChangeNotesState assertParseSucceeds(RevCommit commit) throws Exception { + return newParser(commit).parseAll(); } private void assertParseFails(String body) throws Exception {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java index 9d6cb60..3d12680 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -19,6 +19,7 @@ import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef; import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments; import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC; +import static com.google.gerrit.server.notedb.ReviewerStateInternal.REMOVED; import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER; import static java.nio.charset.StandardCharsets.UTF_8; import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; @@ -49,6 +50,7 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.config.GerritServerId; +import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk; import com.google.gerrit.server.util.RequestId; import com.google.gerrit.testutil.TestChanges; @@ -115,7 +117,7 @@ } @Test - public void tagInlineCommenrts() throws Exception { + public void tagInlineComments() throws Exception { String tag = "jenkins"; Change c = newChange(); RevCommit commit = tr.commit().message("PS2").create(); @@ -751,7 +753,7 @@ try (RevWalk walk = new RevWalk(repo)) { RevCommit commit = walk.parseCommit(update.getResult()); walk.parseBody(commit); - assertThat(commit.getFullMessage()).endsWith("Hashtags: tag1,tag2\n"); + assertThat(commit.getFullMessage()).contains("Hashtags: tag1,tag2\n"); } } @@ -3265,6 +3267,233 @@ assertThat(notes.getReadOnlyUntil()).isEqualTo(new Timestamp(0)); } + @Test + public void privateDefault() throws Exception { + Change c = newChange(); + ChangeNotes notes = newNotes(c); + assertThat(notes.isPrivate()).isFalse(); + } + + @Test + public void privateSetPrivate() throws Exception { + Change c = newChange(); + ChangeUpdate update = newUpdate(c, changeOwner); + update.setPrivate(true); + update.commit(); + + ChangeNotes notes = newNotes(c); + assertThat(notes.isPrivate()).isTrue(); + } + + @Test + public void privateSetPrivateMultipleTimes() throws Exception { + Change c = newChange(); + ChangeUpdate update = newUpdate(c, changeOwner); + update.setPrivate(true); + update.commit(); + + update = newUpdate(c, changeOwner); + update.setPrivate(false); + update.commit(); + + ChangeNotes notes = newNotes(c); + assertThat(notes.isPrivate()).isFalse(); + } + + @Test + public void defaultReviewersByEmailIsEmpty() throws Exception { + Change c = newChange(); + ChangeNotes notes = newNotes(c); + assertThat(notes.getReviewersByEmail().all()).isEmpty(); + } + + @Test + public void putReviewerByEmail() throws Exception { + Address adr = new Address("Foo Bar", "foo.bar@gerritcodereview.com"); + + Change c = newChange(); + ChangeUpdate update = newUpdate(c, changeOwner); + update.putReviewerByEmail(adr, ReviewerStateInternal.REVIEWER); + update.commit(); + + ChangeNotes notes = newNotes(c); + assertThat(notes.getReviewersByEmail().all()).containsExactly(adr); + } + + @Test + public void putAndRemoveReviewerByEmail() throws Exception { + Address adr = new Address("Foo Bar", "foo.bar@gerritcodereview.com"); + + Change c = newChange(); + ChangeUpdate update = newUpdate(c, changeOwner); + update.putReviewerByEmail(adr, ReviewerStateInternal.REVIEWER); + update.commit(); + + update = newUpdate(c, changeOwner); + update.removeReviewerByEmail(adr); + update.commit(); + + ChangeNotes notes = newNotes(c); + assertThat(notes.getReviewersByEmail().all()).isEmpty(); + } + + @Test + public void putRemoveAndAddBackReviewerByEmail() throws Exception { + Address adr = new Address("Foo Bar", "foo.bar@gerritcodereview.com"); + + Change c = newChange(); + ChangeUpdate update = newUpdate(c, changeOwner); + update.putReviewerByEmail(adr, ReviewerStateInternal.REVIEWER); + update.commit(); + + update = newUpdate(c, changeOwner); + update.removeReviewerByEmail(adr); + update.commit(); + + update = newUpdate(c, changeOwner); + update.putReviewerByEmail(adr, ReviewerStateInternal.CC); + update.commit(); + + ChangeNotes notes = newNotes(c); + assertThat(notes.getReviewersByEmail().all()).containsExactly(adr); + } + + @Test + public void putReviewerByEmailAndCcByEmail() throws Exception { + Address adrReviewer = new Address("Foo Bar", "foo.bar@gerritcodereview.com"); + Address adrCc = new Address("Foo Bor", "foo.bar.2@gerritcodereview.com"); + + Change c = newChange(); + ChangeUpdate update = newUpdate(c, changeOwner); + update.putReviewerByEmail(adrReviewer, ReviewerStateInternal.REVIEWER); + update.commit(); + + update = newUpdate(c, changeOwner); + update.putReviewerByEmail(adrCc, ReviewerStateInternal.CC); + update.commit(); + + ChangeNotes notes = newNotes(c); + assertThat(notes.getReviewersByEmail().byState(ReviewerStateInternal.REVIEWER)) + .containsExactly(adrReviewer); + assertThat(notes.getReviewersByEmail().byState(ReviewerStateInternal.CC)) + .containsExactly(adrCc); + assertThat(notes.getReviewersByEmail().all()).containsExactly(adrReviewer, adrCc); + } + + @Test + public void putReviewerByEmailAndChangeToCc() throws Exception { + Address adr = new Address("Foo Bar", "foo.bar@gerritcodereview.com"); + + Change c = newChange(); + ChangeUpdate update = newUpdate(c, changeOwner); + update.putReviewerByEmail(adr, ReviewerStateInternal.REVIEWER); + update.commit(); + + update = newUpdate(c, changeOwner); + update.putReviewerByEmail(adr, ReviewerStateInternal.CC); + update.commit(); + + ChangeNotes notes = newNotes(c); + assertThat(notes.getReviewersByEmail().byState(ReviewerStateInternal.REVIEWER)).isEmpty(); + assertThat(notes.getReviewersByEmail().byState(ReviewerStateInternal.CC)).containsExactly(adr); + assertThat(notes.getReviewersByEmail().all()).containsExactly(adr); + } + + @Test + public void hasReviewStarted() throws Exception { + ChangeNotes notes = newNotes(newChange()); + assertThat(notes.hasReviewStarted()).isTrue(); + + notes = newNotes(newWorkInProgressChange()); + assertThat(notes.hasReviewStarted()).isFalse(); + + Change c = newWorkInProgressChange(); + ChangeUpdate update = newUpdate(c, changeOwner); + update.commit(); + notes = newNotes(c); + assertThat(notes.hasReviewStarted()).isFalse(); + + update = newUpdate(c, changeOwner); + update.setWorkInProgress(true); + update.commit(); + notes = newNotes(c); + assertThat(notes.hasReviewStarted()).isFalse(); + + update = newUpdate(c, changeOwner); + update.setWorkInProgress(false); + update.commit(); + notes = newNotes(c); + assertThat(notes.hasReviewStarted()).isTrue(); + + // Once review is started, setting WIP should have no impact. + c = newChange(); + notes = newNotes(c); + assertThat(notes.hasReviewStarted()).isTrue(); + update = newUpdate(c, changeOwner); + update.setWorkInProgress(true); + update.commit(); + notes = newNotes(c); + assertThat(notes.hasReviewStarted()).isTrue(); + } + + @Test + public void pendingReviewers() throws Exception { + Address adr1 = new Address("Foo Bar1", "foo.bar1@gerritcodereview.com"); + Address adr2 = new Address("Foo Bar2", "foo.bar2@gerritcodereview.com"); + Account.Id ownerId = changeOwner.getAccount().getId(); + Account.Id otherUserId = otherUser.getAccount().getId(); + + ChangeNotes notes = newNotes(newChange()); + assertThat(notes.getPendingReviewers().asTable()).isEmpty(); + assertThat(notes.getPendingReviewersByEmail().asTable()).isEmpty(); + + Change c = newWorkInProgressChange(); + notes = newNotes(c); + assertThat(notes.getPendingReviewers().asTable()).isEmpty(); + assertThat(notes.getPendingReviewersByEmail().asTable()).isEmpty(); + + ChangeUpdate update = newUpdate(c, changeOwner); + update.putReviewer(ownerId, REVIEWER); + update.putReviewer(otherUserId, CC); + update.putReviewerByEmail(adr1, REVIEWER); + update.putReviewerByEmail(adr2, CC); + update.commit(); + notes = newNotes(c); + assertThat(notes.getPendingReviewers().byState(REVIEWER)).containsExactly(ownerId); + assertThat(notes.getPendingReviewers().byState(CC)).containsExactly(otherUserId); + assertThat(notes.getPendingReviewers().byState(REMOVED)).isEmpty(); + assertThat(notes.getPendingReviewersByEmail().byState(REVIEWER)).containsExactly(adr1); + assertThat(notes.getPendingReviewersByEmail().byState(CC)).containsExactly(adr2); + assertThat(notes.getPendingReviewersByEmail().byState(REMOVED)).isEmpty(); + + update = newUpdate(c, changeOwner); + update.removeReviewer(ownerId); + update.removeReviewerByEmail(adr1); + update.commit(); + notes = newNotes(c); + assertThat(notes.getPendingReviewers().byState(REVIEWER)).isEmpty(); + assertThat(notes.getPendingReviewers().byState(CC)).containsExactly(otherUserId); + assertThat(notes.getPendingReviewers().byState(REMOVED)).containsExactly(ownerId); + assertThat(notes.getPendingReviewersByEmail().byState(REVIEWER)).isEmpty(); + assertThat(notes.getPendingReviewersByEmail().byState(CC)).containsExactly(adr2); + assertThat(notes.getPendingReviewersByEmail().byState(REMOVED)).containsExactly(adr1); + + update = newUpdate(c, changeOwner); + update.setWorkInProgress(false); + update.commit(); + notes = newNotes(c); + assertThat(notes.getPendingReviewers().asTable()).isEmpty(); + assertThat(notes.getPendingReviewersByEmail().asTable()).isEmpty(); + + update = newUpdate(c, changeOwner); + update.putReviewer(ownerId, REVIEWER); + update.putReviewerByEmail(adr1, REVIEWER); + update.commit(); + notes = newNotes(c); + assertThat(notes.getPendingReviewers().asTable()).isEmpty(); + assertThat(notes.getPendingReviewersByEmail().asTable()).isEmpty(); + } + private boolean testJson() { return noteUtil.getWriteJson(); }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java index 25b5168..83dcf61 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -23,6 +23,7 @@ import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.util.RequestId; import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.TestChanges; @@ -382,6 +383,32 @@ assertBodyEquals("Update patch set 1\n\nPatch-set: 1\nCurrent: true\n", update.getResult()); } + @Test + public void reviewerByEmail() throws Exception { + Change c = newChange(); + ChangeUpdate update = newUpdate(c, changeOwner); + update.putReviewerByEmail( + new Address("John Doe", "j.doe@gerritcodereview.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.getResult()); + } + + @Test + public void ccByEmail() throws Exception { + Change c = newChange(); + ChangeUpdate update = newUpdate(c, changeOwner); + update.putReviewerByEmail(new Address("j.doe@gerritcodereview.com"), ReviewerStateInternal.CC); + update.commit(); + + assertBodyEquals( + "Update patch set 1\n\nPatch-set: 1\nCC-email: j.doe@gerritcodereview.com\n", + update.getResult()); + } + private RevCommit parseCommit(ObjectId id) throws Exception { if (id instanceof RevCommit) { return (RevCommit) id;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java index 0553dc5..67ad65c 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
@@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; import static com.google.gerrit.common.TimeUtil.nowTs; import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.NOTE_DB; import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; @@ -63,7 +64,7 @@ assertThat(state.getChangeId()).isEqualTo(new Change.Id(1)); assertThat(state.getChangeMetaId()).isEqualTo(SHA1); assertThat(state.getDraftIds()).isEmpty(); - assertThat(state.getReadOnlyUntil().isPresent()).isFalse(); + assertThat(state.getReadOnlyUntil()).isEmpty(); assertThat(state.toString()).isEqualTo(SHA1.name()); state = parse(new Change.Id(1), "R," + SHA1.name()); @@ -71,7 +72,7 @@ assertThat(state.getChangeId()).isEqualTo(new Change.Id(1)); assertThat(state.getChangeMetaId()).isEqualTo(SHA1); assertThat(state.getDraftIds()).isEmpty(); - assertThat(state.getReadOnlyUntil().isPresent()).isFalse(); + assertThat(state.getReadOnlyUntil()).isEmpty(); assertThat(state.toString()).isEqualTo(SHA1.name()); } @@ -87,7 +88,7 @@ .containsExactly( new Account.Id(1001), SHA3, new Account.Id(2003), SHA2); - assertThat(state.getReadOnlyUntil().isPresent()).isFalse(); + assertThat(state.getReadOnlyUntil()).isEmpty(); assertThat(state.toString()).isEqualTo(expected); state = parse(new Change.Id(1), "R," + str); @@ -98,7 +99,7 @@ .containsExactly( new Account.Id(1001), SHA3, new Account.Id(2003), SHA2); - assertThat(state.getReadOnlyUntil().isPresent()).isFalse(); + assertThat(state.getReadOnlyUntil()).isEmpty(); assertThat(state.toString()).isEqualTo(expected); } @@ -117,7 +118,7 @@ state = parse(new Change.Id(1), str); assertThat(state.getPrimaryStorage()).isEqualTo(NOTE_DB); assertThat(state.getChangeId()).isEqualTo(new Change.Id(1)); - assertThat(state.getRefState().isPresent()).isFalse(); + assertThat(state.getRefState()).isEmpty(); assertThat(state.getReadOnlyUntil().get()).isEqualTo(ts); assertThat(state.toString()).isEqualTo(str); } @@ -194,8 +195,8 @@ public void parseNoteDbPrimary() { NoteDbChangeState state = parse(new Change.Id(1), "N"); assertThat(state.getPrimaryStorage()).isEqualTo(NOTE_DB); - assertThat(state.getRefState().isPresent()).isFalse(); - assertThat(state.getReadOnlyUntil().isPresent()).isFalse(); + assertThat(state.getRefState()).isEmpty(); + assertThat(state.getReadOnlyUntil()).isEmpty(); } @Test(expected = IllegalArgumentException.class)
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java index df3e405..66ccdad 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/RepoSequenceTest.java
@@ -19,7 +19,6 @@ import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; import static org.junit.Assert.fail; -import com.github.rholder.retry.BlockStrategy; import com.github.rholder.retry.Retryer; import com.github.rholder.retry.RetryerBuilder; import com.github.rholder.retry.StopStrategies; @@ -45,16 +44,9 @@ import org.junit.rules.ExpectedException; public class RepoSequenceTest { + // Don't sleep in tests. private static final Retryer<RefUpdate.Result> RETRYER = - RepoSequence.retryerBuilder() - .withBlockStrategy( - new BlockStrategy() { - @Override - public void block(long sleepTime) { - // Don't sleep in tests. - } - }) - .build(); + RepoSequence.retryerBuilder().withBlockStrategy(t -> {}).build(); @Rule public ExpectedException exception = ExpectedException.none(); @@ -159,14 +151,11 @@ // Seed existing ref value. writeBlob("id", "1"); - final AtomicBoolean doneBgUpdate = new AtomicBoolean(false); + AtomicBoolean doneBgUpdate = new AtomicBoolean(false); Runnable bgUpdate = - new Runnable() { - @Override - public void run() { - if (!doneBgUpdate.getAndSet(true)) { - writeBlob("id", "1234"); - } + () -> { + if (!doneBgUpdate.getAndSet(true)) { + writeBlob("id", "1234"); } }; @@ -203,20 +192,13 @@ @Test public void failAfterRetryerGivesUp() throws Exception { - final AtomicInteger bgCounter = new AtomicInteger(1234); - Runnable bgUpdate = - new Runnable() { - @Override - public void run() { - writeBlob("id", Integer.toString(bgCounter.getAndAdd(1000))); - } - }; + AtomicInteger bgCounter = new AtomicInteger(1234); RepoSequence s = newSequence( "id", 1, 10, - bgUpdate, + () -> writeBlob("id", Integer.toString(bgCounter.getAndAdd(1000))), RetryerBuilder.<RefUpdate.Result>newBuilder() .withStopStrategy(StopStrategies.stopAfterAttempt(3)) .build()); @@ -282,18 +264,7 @@ Runnable afterReadRef, Retryer<RefUpdate.Result> retryer) { return new RepoSequence( - repoManager, - project, - name, - new RepoSequence.Seed() { - @Override - public int get() { - return start; - } - }, - batchSize, - afterReadRef, - retryer); + repoManager, project, name, () -> start, batchSize, afterReadRef, retryer); } private ObjectId writeBlob(String sequenceName, String value) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/patch/IntraLineLoaderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/patch/IntraLineLoaderTest.java index 0859bf7..eb31abd 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/patch/IntraLineLoaderTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/patch/IntraLineLoaderTest.java
@@ -17,6 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.collect.ImmutableList; import java.util.List; import org.eclipse.jgit.diff.Edit; import org.eclipse.jgit.diff.EditList; @@ -148,8 +149,7 @@ Text aText = new Text(a.getBytes(UTF_8)); Text bText = new Text(b.getBytes(UTF_8)); - IntraLineDiff diff; - diff = IntraLineLoader.compute(aText, bText, EditList.singleton(lines)); + IntraLineDiff diff = IntraLineLoader.compute(aText, bText, ImmutableList.of(lines)); assertThat(diff.getStatus()).isEqualTo(IntraLineDiff.Status.EDIT_LIST); List<Edit> actualEdits = diff.getEdits();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java index 6390e1f6..9b9cfff 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -48,7 +48,6 @@ import com.google.gerrit.rules.RulesCache; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.CapabilityCollection; -import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.GroupMembership; import com.google.gerrit.server.account.ListGroupMembership; import com.google.gerrit.server.config.AllProjectsName; @@ -58,6 +57,9 @@ import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.index.SingleVersionModule.SingleVersionListener; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.ProjectPermission; +import com.google.gerrit.server.permissions.RefPermission; import com.google.gerrit.server.query.change.InternalChangeQuery; import com.google.gerrit.server.schema.SchemaCreator; import com.google.gerrit.server.util.RequestContext; @@ -109,12 +111,14 @@ assertThat(u.controlForRef(ref).isOwner()).named("NOT OWN " + ref).isFalse(); } - private void assertCanRead(ProjectControl u) { - assertThat(u.isVisible()).named("can read").isTrue(); + private void assertCanAccess(ProjectControl u) { + boolean access = u.asForProject().testOrFalse(ProjectPermission.ACCESS); + assertThat(access).named("can access").isTrue(); } - private void assertCannotRead(ProjectControl u) { - assertThat(u.isVisible()).named("cannot read").isFalse(); + private void assertAccessDenied(ProjectControl u) { + boolean access = u.asForProject().testOrFalse(ProjectPermission.ACCESS); + assertThat(access).named("cannot access").isFalse(); } private void assertCanRead(String ref, ProjectControl u) { @@ -137,16 +141,18 @@ assertThat(u.canPushToAtLeastOneRef()).named("can upload").isEqualTo(Capable.OK); } - private void assertCanUpload(String ref, ProjectControl u) { - assertThat(u.controlForRef(ref).canUpload()).named("can upload " + ref).isTrue(); + private void assertCreateChange(String ref, ProjectControl u) { + boolean create = u.asForProject().ref(ref).testOrFalse(RefPermission.CREATE_CHANGE); + assertThat(create).named("can create change " + ref).isTrue(); } private void assertCannotUpload(ProjectControl u) { assertThat(u.canPushToAtLeastOneRef()).named("cannot upload").isNotEqualTo(Capable.OK); } - private void assertCannotUpload(String ref, ProjectControl u) { - assertThat(u.controlForRef(ref).canUpload()).named("cannot upload " + ref).isFalse(); + private void assertCannotCreateChange(String ref, ProjectControl u) { + boolean create = u.asForProject().ref(ref).testOrFalse(RefPermission.CREATE_CHANGE); + assertThat(create).named("cannot create change " + ref).isFalse(); } private void assertBlocked(String p, String ref, ProjectControl u) { @@ -158,19 +164,23 @@ } private void assertCanUpdate(String ref, ProjectControl u) { - assertThat(u.controlForRef(ref).canUpdate()).named("can update " + ref).isTrue(); + boolean update = u.asForProject().ref(ref).testOrFalse(RefPermission.UPDATE); + assertThat(update).named("can update " + ref).isTrue(); } private void assertCannotUpdate(String ref, ProjectControl u) { - assertThat(u.controlForRef(ref).canUpdate()).named("cannot update " + ref).isFalse(); + boolean update = u.asForProject().ref(ref).testOrFalse(RefPermission.UPDATE); + assertThat(update).named("cannot update " + ref).isFalse(); } private void assertCanForceUpdate(String ref, ProjectControl u) { - assertThat(u.controlForRef(ref).canForceUpdate()).named("can force push " + ref).isTrue(); + boolean update = u.asForProject().ref(ref).testOrFalse(RefPermission.FORCE_UPDATE); + assertThat(update).named("can force push " + ref).isTrue(); } private void assertCannotForceUpdate(String ref, ProjectControl u) { - assertThat(u.controlForRef(ref).canForceUpdate()).named("cannot force push " + ref).isFalse(); + boolean update = u.asForProject().ref(ref).testOrFalse(RefPermission.FORCE_UPDATE); + assertThat(update).named("cannot force push " + ref).isFalse(); } private void assertCanVote(int score, PermissionRange range) { @@ -196,8 +206,8 @@ private ChangeControl.Factory changeControlFactory; private ReviewDb db; + @Inject private PermissionBackend permissionBackend; @Inject private CapabilityCollection.Factory capabilityCollectionFactory; - @Inject private CapabilityControl.Factory capabilityControlFactory; @Inject private SchemaCreator schemaCreator; @Inject private SingleVersionListener singleVersionListener; @Inject private InMemoryDatabase schemaFactory; @@ -397,8 +407,8 @@ ProjectControl u = user(local); assertCanUpload(u); - assertCanUpload("refs/heads/master", u); - assertCannotUpload("refs/heads/foobar", u); + assertCreateChange("refs/heads/master", u); + assertCannotCreateChange("refs/heads/foobar", u); } @Test @@ -407,7 +417,7 @@ block(parent, PUSH, ANONYMOUS_USERS, "refs/drafts/*"); ProjectControl u = user(local); - assertCanUpload("refs/heads/master", u); + assertCreateChange("refs/heads/master", u); assertBlocked(PUSH, "refs/drafts/refs/heads/master", u); } @@ -430,21 +440,21 @@ ProjectControl u = user(local); assertCanUpload(u); - assertCanUpload("refs/heads/master", u); - assertCanUpload("refs/heads/foobar", u); + assertCreateChange("refs/heads/master", u); + assertCreateChange("refs/heads/foobar", u); } @Test public void inheritDuplicateSections() throws Exception { allow(parent, READ, ADMIN, "refs/*"); allow(local, READ, DEVS, "refs/heads/*"); - assertCanRead(user(local, "a", ADMIN)); + assertCanAccess(user(local, "a", ADMIN)); local = new ProjectConfig(localKey); local.load(newRepository(localKey)); local.getProject().setParentName(parentKey); allow(local, READ, DEVS, "refs/*"); - assertCanRead(user(local, "d", DEVS)); + assertCanAccess(user(local, "d", DEVS)); } @Test @@ -452,7 +462,7 @@ allow(parent, READ, REGISTERED_USERS, "refs/*"); deny(local, READ, REGISTERED_USERS, "refs/*"); - assertCannotRead(user(local)); + assertAccessDenied(user(local)); } @Test @@ -461,7 +471,7 @@ deny(local, READ, REGISTERED_USERS, "refs/heads/*"); ProjectControl u = user(local); - assertCanRead(u); + assertCanAccess(u); assertCanRead("refs/master", u); assertCanRead("refs/tags/foobar", u); assertCanRead("refs/heads/master", u); @@ -474,7 +484,7 @@ allow(local, READ, REGISTERED_USERS, "refs/heads/*"); ProjectControl u = user(local); - assertCanRead(u); + assertCanAccess(u); assertCannotRead("refs/foobar", u); assertCannotRead("refs/tags/foobar", u); assertCanRead("refs/heads/foobar", u); @@ -500,7 +510,7 @@ ProjectControl u = user(local); assertCannotUpload(u); - assertCannotUpload("refs/heads/master", u); + assertCannotCreateChange("refs/heads/master", u); } @Test @@ -896,12 +906,11 @@ Collections.<AccountGroup.UUID>emptySet(), projectCache, sectionSorter, - null, changeControlFactory, - null, + null, // refFilter queryProvider, - null, canonicalWebUrl, + permissionBackend, new MockUser(name, memberOf), newProjectState(local), metrics); @@ -917,7 +926,6 @@ private final GroupMembership groups; MockUser(String name, AccountGroup.UUID[] groupId) { - super(capabilityControlFactory); username = name; ArrayList<AccountGroup.UUID> groupIds = Lists.newArrayList(groupId); groupIds.add(REGISTERED_USERS);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java index ea22c00..6a72fce 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/FieldPredicateTest.java
@@ -30,6 +30,7 @@ assertEquals("owner:\"A U Thor\"", f("owner", "A U Thor").toString()); } + @SuppressWarnings("unlikely-arg-type") @Test public void testEquals() { assertTrue(f("author", "bob").equals(f("author", "bob")));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java index 05df241..f70b8fc 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/NotPredicateTest.java
@@ -73,6 +73,7 @@ assertEquals("-author:bob", not(f("author", "bob")).toString()); } + @SuppressWarnings("unlikely-arg-type") @Test public void testEquals() { assertTrue(not(f("author", "bob")).equals(not(f("author", "bob"))));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/QueryParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/QueryParserTest.java index 4c0bcc0..3a38a50 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/QueryParserTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/QueryParserTest.java
@@ -31,7 +31,7 @@ assertSingleWord("project", "tools/*", r); } - private static void assertSingleWord(final String name, final String value, final Tree r) { + private static void assertSingleWord(String name, String value, Tree r) { assertEquals(QueryParser.FIELD_NAME, r.getType()); assertEquals(name, r.getText()); assertEquals(1, r.getChildCount()); @@ -41,7 +41,7 @@ assertEquals(0, c.getChildCount()); } - private static Tree parse(final String str) throws QueryParseException { + private static Tree parse(String str) throws QueryParseException { return QueryParser.parse(str); } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java index 3d13536..8323051 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -18,7 +18,7 @@ import static java.util.stream.Collectors.toList; import static org.junit.Assert.fail; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.gerrit.extensions.api.GerritApi; import com.google.gerrit.extensions.api.accounts.Accounts.QueryRequest; @@ -33,12 +33,20 @@ import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.AccountConfig; import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.account.Accounts; +import com.google.gerrit.server.account.AccountsUpdate; import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.config.AllProjectsName; +import com.google.gerrit.server.config.AllUsersName; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.schema.SchemaCreator; import com.google.gerrit.server.util.ManualRequestContext; import com.google.gerrit.server.util.OneOffRequestContext; @@ -53,16 +61,15 @@ import com.google.inject.util.Providers; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Iterator; import java.util.List; import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; import org.junit.After; import org.junit.Before; import org.junit.Ignore; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.TestName; @Ignore public abstract class AbstractQueryAccountsTest extends GerritServerTests { @@ -73,7 +80,9 @@ return cfg; } - @Rule public final TestName testName = new TestName(); + @Inject protected Accounts accounts; + + @Inject protected AccountsUpdate.Server accountsUpdate; @Inject protected AccountCache accountCache; @@ -81,6 +90,8 @@ @Inject protected GerritApi gApi; + @Inject @GerritPersonIdent Provider<PersonIdent> serverIdent; + @Inject protected IdentifiedUser.GenericFactory userFactory; @Inject private Provider<AnonymousUser> anonymousUser; @@ -97,7 +108,12 @@ @Inject protected AllProjectsName allProjects; + @Inject protected AllUsersName allUsers; + + @Inject protected GitRepositoryManager repoManager; + protected LifecycleManager lifecycle; + protected Injector injector; protected ReviewDb db; protected AccountInfo currentUserInfo; protected CurrentUser user; @@ -107,11 +123,14 @@ @Before public void setUpInjector() throws Exception { lifecycle = new LifecycleManager(); - Injector injector = createInjector(); + injector = createInjector(); lifecycle.add(injector); injector.injectMembers(this); lifecycle.start(); + setUpDatabase(); + } + protected void setUpDatabase() throws Exception { db = schemaFactory.open(); schemaCreator.create(db); @@ -255,6 +274,7 @@ assertQuery("Jo Do", user1); assertQuery("jo do", user1); assertQuery("self", currentUserInfo, user3); + assertQuery("me", currentUserInfo); assertQuery("name:John", user1); assertQuery("name:john", user1); assertQuery("name:Doe", user1); @@ -377,11 +397,24 @@ public void reindex() throws Exception { AccountInfo user1 = newAccountWithFullName("tester", "Test Usre"); - // update account in the database so that account index is stale + // update account in ReviewDb without reindex so that account index is stale String newName = "Test User"; - Account account = db.accounts().get(new Account.Id(user1._accountId)); + Account.Id accountId = new Account.Id(user1._accountId); + Account account = accounts.get(db, accountId); account.setFullName(newName); - db.accounts().update(Collections.singleton(account)); + db.accounts().update(ImmutableSet.of(account)); + + // update account in NoteDb without reindex so that account index is stale + try (Repository repo = repoManager.openRepository(allUsers)) { + MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsers, repo); + PersonIdent ident = serverIdent.get(); + md.getCommitBuilder().setAuthor(ident); + md.getCommitBuilder().setCommitter(ident); + AccountConfig accountConfig = new AccountConfig(null, accountId); + accountConfig.load(repo); + accountConfig.getAccount().setFullName(newName); + accountConfig.commit(md); + } assertQuery("name:" + quote(user1.name), user1); assertQuery("name:" + quote(newName)); @@ -448,7 +481,8 @@ if (name == null) { return null; } - String suffix = testName.getMethodName().toLowerCase(); + + String suffix = getSanitizedMethodName(); if (name.contains("@")) { return name + "." + suffix; } @@ -462,12 +496,16 @@ if (email != null) { accountManager.link(id, AuthRequest.forEmail(email)); } - Account a = db.accounts().get(id); - a.setFullName(fullName); - a.setPreferredEmail(email); - a.setActive(active); - db.accounts().update(ImmutableList.of(a)); - accountCache.evict(id); + accountsUpdate + .create() + .update( + db, + id, + a -> { + a.setFullName(fullName); + a.setPreferredEmail(email); + a.setActive(active); + }); return id; } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java index 978283a..fa130ca 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/LuceneQueryAccountsTest.java
@@ -14,12 +14,26 @@ package com.google.gerrit.server.query.account; +import com.google.gerrit.server.index.account.AccountSchemaDefinitions; +import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.InMemoryModule; +import com.google.gerrit.testutil.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; 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 = + IndexVersions.getWithoutLatest(AccountSchemaDefinitions.INSTANCE); + return IndexVersions.asConfigMap( + AccountSchemaDefinitions.INSTANCE, schemaVersions, "againstIndexVersion", defaultConfig()); + } + @Override protected Injector createInjector() { Config luceneConfig = new Config(config);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java index d0c8df3..2b8536b 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -22,6 +22,7 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.stream.Collectors.toList; +import static org.junit.Assert.fail; import com.google.common.base.MoreObjects; import com.google.common.collect.FluentIterable; @@ -44,6 +45,8 @@ import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput; import com.google.gerrit.extensions.api.changes.StarsInput; import com.google.gerrit.extensions.api.groups.GroupInput; +import com.google.gerrit.extensions.api.projects.ConfigInput; +import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeMessageInfo; @@ -65,15 +68,22 @@ import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.Sequences; import com.google.gerrit.server.StarredChangesUtil; +import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountManager; +import com.google.gerrit.server.account.Accounts; +import com.google.gerrit.server.account.AccountsUpdate; import com.google.gerrit.server.account.AuthRequest; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIdsUpdate; import com.google.gerrit.server.change.ChangeInserter; import com.google.gerrit.server.change.ChangeTriplet; import com.google.gerrit.server.change.PatchSetInserter; import com.google.gerrit.server.config.AllUsersName; -import com.google.gerrit.server.git.validators.CommitValidators; +import com.google.gerrit.server.git.MetaDataUpdate; +import com.google.gerrit.server.index.FieldDef; import com.google.gerrit.server.index.IndexConfig; import com.google.gerrit.server.index.QueryOptions; +import com.google.gerrit.server.index.Schema; import com.google.gerrit.server.index.change.ChangeField; import com.google.gerrit.server.index.change.ChangeIndexCollection; import com.google.gerrit.server.index.change.ChangeIndexer; @@ -83,8 +93,11 @@ import com.google.gerrit.server.notedb.NoteDbChangeState; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.schema.SchemaCreator; import com.google.gerrit.server.update.BatchUpdate; +import com.google.gerrit.server.util.ManualRequestContext; +import com.google.gerrit.server.util.OneOffRequestContext; import com.google.gerrit.server.util.RequestContext; import com.google.gerrit.server.util.ThreadLocalRequestContext; import com.google.gerrit.testutil.ConfigSuite; @@ -94,6 +107,7 @@ import com.google.gerrit.testutil.InMemoryRepositoryManager; import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo; import com.google.gerrit.testutil.TestTimeUtil; +import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Provider; @@ -101,6 +115,7 @@ import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; @@ -111,10 +126,14 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jgit.junit.TestRepository; 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.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.util.SystemReader; import org.junit.After; import org.junit.Before; @@ -130,6 +149,9 @@ return cfg; } + @Inject protected Accounts accounts; + @Inject protected AccountCache accountCache; + @Inject protected AccountsUpdate.Server accountsUpdate; @Inject protected AccountManager accountManager; @Inject protected AllUsersName allUsersName; @Inject protected BatchUpdate.Factory updateFactory; @@ -140,17 +162,24 @@ @Inject protected ChangeIndexCollection indexes; @Inject protected ChangeIndexer indexer; @Inject protected IndexConfig indexConfig; - @Inject protected InMemoryDatabase schemaFactory; @Inject protected InMemoryRepositoryManager repoManager; @Inject protected InternalChangeQuery internalChangeQuery; @Inject protected ChangeNotes.Factory notesFactory; + @Inject protected OneOffRequestContext oneOffRequestContext; @Inject protected PatchSetInserter.Factory patchSetFactory; @Inject protected PatchSetUtil psUtil; @Inject protected ChangeControl.GenericFactory changeControlFactory; @Inject protected ChangeQueryProcessor queryProcessor; @Inject protected SchemaCreator schemaCreator; + @Inject protected SchemaFactory<ReviewDb> schemaFactory; @Inject protected Sequences seq; @Inject protected ThreadLocalRequestContext requestContext; + @Inject protected ProjectCache projectCache; + @Inject protected MetaDataUpdate.Server metaDataUpdateFactory; + @Inject protected ExternalIdsUpdate.Server externalIdsUpdate; + + // Only for use in setting up/tearing down injector; other users should use schemaFactory. + @Inject private InMemoryDatabase inMemoryDatabase; protected Injector injector; protected LifecycleManager lifecycle; @@ -173,15 +202,17 @@ } protected void setUpDatabase() throws Exception { + try (ReviewDb underlyingDb = inMemoryDatabase.getDatabase().open()) { + schemaCreator.create(underlyingDb); + } db = schemaFactory.open(); - schemaCreator.create(db); userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId(); - Account userAccount = db.accounts().get(userId); - userAccount.setPreferredEmail("user@example.com"); - db.accounts().update(ImmutableList.of(userAccount)); + String email = "user@example.com"; + externalIdsUpdate.create().insert(ExternalId.createEmail(userId, email)); + accountsUpdate.create().update(db, userId, a -> a.setPreferredEmail(email)); user = userFactory.create(userId); - requestContext.setContext(newRequestContext(userAccount.getId())); + requestContext.setContext(newRequestContext(userId)); } protected RequestContext newRequestContext(Account.Id requestUserId) { @@ -208,7 +239,7 @@ if (db != null) { db.close(); } - InMemoryDatabase.drop(schemaFactory); + InMemoryDatabase.drop(inMemoryDatabase); } @Before @@ -373,11 +404,124 @@ } @Test + public void byPrivate() throws Exception { + if (getSchemaVersion() < 40) { + assertMissingField(ChangeField.PRIVATE); + assertFailingQuery( + "is:private", "'is:private' operator is not supported by change index version"); + return; + } + + TestRepository<Repo> repo = createProject("repo"); + Change change1 = insert(repo, newChange(repo), userId); + Account.Id user2 = + accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId(); + Change change2 = insert(repo, newChange(repo), user2); + + // No private changes. + assertQuery("is:open", change2, change1); + assertQuery("is:private"); + + gApi.changes().id(change1.getChangeId()).setPrivate(true, null); + + // Change1 is not private, but should be still visible to its owner. + assertQuery("is:open", change1, change2); + assertQuery("is:private", change1); + + // Switch request context to user2. + requestContext.setContext(newRequestContext(user2)); + assertQuery("is:open", change2); + assertQuery("is:private"); + } + + @Test + public void byWip() throws Exception { + if (getSchemaVersion() < 42) { + assertMissingField(ChangeField.WIP); + assertFailingQuery("is:wip", "'is:wip' operator is not supported by change index version"); + return; + } + + TestRepository<Repo> repo = createProject("repo"); + Change change1 = insert(repo, newChange(repo), userId); + + assertQuery("is:open", change1); + assertQuery("is:wip"); + + gApi.changes().id(change1.getChangeId()).setWorkInProgress(); + + assertQuery("is:wip", change1); + + gApi.changes().id(change1.getChangeId()).setReadyForReview(); + + assertQuery("is:wip"); + } + + @Test + public void excludeWipChangeFromReviewersDashboardsBeforeSchema42() throws Exception { + assume().that(getSchemaVersion()).isLessThan(42); + + assertMissingField(ChangeField.WIP); + assertFailingQuery("is:wip", "'is:wip' operator is not supported by change index version"); + + Account.Id user1 = createAccount("user1"); + TestRepository<Repo> repo = createProject("repo"); + Change change1 = insert(repo, newChangeWorkInProgress(repo), userId); + assertQuery("reviewer:" + user1, change1); + gApi.changes().id(change1.getChangeId()).setWorkInProgress(); + assertQuery("reviewer:" + user1, change1); + } + + @Test + public void excludeWipChangeFromReviewersDashboards() throws Exception { + assume().that(getSchemaVersion()).isAtLeast(42); + + Account.Id user1 = createAccount("user1"); + TestRepository<Repo> repo = createProject("repo"); + Change change1 = insert(repo, newChangeWorkInProgress(repo), userId); + + assertQuery("is:wip", change1); + assertQuery("reviewer:" + user1); + + gApi.changes().id(change1.getChangeId()).setReadyForReview(); + assertQuery("is:wip"); + assertQuery("reviewer:" + user1); + + gApi.changes().id(change1.getChangeId()).setWorkInProgress(); + assertQuery("is:wip", change1); + assertQuery("reviewer:" + user1); + } + + @Test + public void byStartedBeforeSchema44() throws Exception { + assume().that(getSchemaVersion()).isLessThan(44); + assertMissingField(ChangeField.STARTED); + assertFailingQuery( + "is:started", "'is:started' operator is not supported by change index version"); + } + + @Test + public void byStarted() throws Exception { + assume().that(getSchemaVersion()).isAtLeast(44); + + TestRepository<Repo> repo = createProject("repo"); + Change change1 = insert(repo, newChangeWorkInProgress(repo)); + + assertQuery("is:started"); + + gApi.changes().id(change1.getChangeId()).setReadyForReview(); + assertQuery("is:started", change1); + + gApi.changes().id(change1.getChangeId()).setWorkInProgress(); + assertQuery("is:started", change1); + } + + @Test public void byCommit() throws Exception { TestRepository<Repo> repo = createProject("repo"); ChangeInserter ins = newChange(repo); insert(repo, ins); - String sha = ins.getCommit().name(); + String sha = ins.getCommitId().name(); assertQuery("0000000000000000000000000000000000000000"); for (int i = 0; i <= 36; i++) { @@ -402,57 +546,84 @@ } @Test - public void byAuthor() throws Exception { - TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo), userId); - - // By exact email address - assertQuery("author:jauthor@example.com", change1); - - // By email address part - assertQuery("author:jauthor", change1); - assertQuery("author:example", change1); - assertQuery("author:example.com", change1); - - // By name part - assertQuery("author:Author", change1); - - // Case insensitive - assertQuery("author:jAuThOr", change1); - assertQuery("author:ExAmPlE", change1); - - // By non-existing email address / name / part - assertQuery("author:jcommitter@example.com"); - assertQuery("author:somewhere.com"); - assertQuery("author:jcommitter"); - assertQuery("author:Committer"); + public void byAuthorExact() throws Exception { + assume().that(getSchema().hasField(ChangeField.EXACT_AUTHOR)).isTrue(); + byAuthorOrCommitterExact("author:"); } @Test - public void byCommitter() throws Exception { + public void byAuthorFullText() throws Exception { + byAuthorOrCommitterFullText("author:"); + } + + @Test + public void byCommitterExact() throws Exception { + assume().that(getSchema().hasField(ChangeField.EXACT_COMMITTER)).isTrue(); + byAuthorOrCommitterExact("committer:"); + } + + @Test + public void byCommitterFullText() throws Exception { + byAuthorOrCommitterFullText("committer:"); + } + + private void byAuthorOrCommitterExact(String searchOperator) throws Exception { TestRepository<Repo> repo = createProject("repo"); - Change change1 = insert(repo, newChange(repo), userId); + 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); - // By exact email address - assertQuery("committer:jcommitter@example.com", change1); + // Only email address. + assertQuery(searchOperator + "john.doe@example.com", change1); + assertQuery(searchOperator + "john@example.com", change2); + assertQuery(searchOperator + "Doe_SmIth@example.com", change3); // Case insensitive. - // By email address part - assertQuery("committer:jcommitter", change1); - assertQuery("committer:example", change1); - assertQuery("committer:example.com", change1); + // Right combination of email address and name. + assertQuery(searchOperator + "\"John Doe <john.doe@example.com>\"", change1); + assertQuery(searchOperator + "\" John <john@example.com> \"", change2); + assertQuery(searchOperator + "\"doE SMITH <doe_smitH@example.com>\"", change3); - // By name part - assertQuery("committer:Committer", change1); + // Wrong combination of email address and name. + assertQuery(searchOperator + "\"John <john.doe@example.com>\""); + assertQuery(searchOperator + "\"Doe John <john@example.com>\""); + assertQuery(searchOperator + "\"Doe John <doe_smith@example.com>\""); + } - // Case insensitive - assertQuery("committer:jCoMmItTeR", change1); - assertQuery("committer:ExAmPlE", change1); + private void byAuthorOrCommitterFullText(String searchOperator) throws Exception { + TestRepository<Repo> repo = createProject("repo"); + 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); - // By non-existing email address / name / part - assertQuery("committer:jauthor@example.com"); - assertQuery("committer:somewhere.com"); - assertQuery("committer:jauthor"); - assertQuery("committer:Author"); + // By exact name. + assertQuery(searchOperator + "\"John Doe\"", change1); + assertQuery(searchOperator + "\"john\"", change2, change1); + assertQuery(searchOperator + "\"Doe smith\"", change3); + + // By name part. + assertQuery(searchOperator + "Doe", change3, change1); + assertQuery(searchOperator + "smith", change3); + + // By wrong combination. + assertQuery(searchOperator + "\"John Smith\""); + + // By invalid query. + exception.expect(BadRequestException.class); + exception.expectMessage("invalid value"); + // SchemaUtil.getNameParts will return an empty set for query only containing these characters. + assertQuery(searchOperator + "@.- /_"); + } + + private Change createChange(TestRepository<Repo> repo, PersonIdent person) throws Exception { + RevCommit commit = + repo.parseBody(repo.commit().message("message").author(person).committer(person).create()); + return insert(repo, newChangeForCommit(repo, commit), null); } @Test @@ -536,8 +707,23 @@ assertQuery("intopic:fixup", change4); assertQuery("topic:\"\"", change5); assertQuery("intopic:\"\"", change5); - assertQuery("intopic:^feature2.*", change4, change2); - assertQuery("intopic:{^.*feature2$}", change3, change2); + } + + @Test + public void byTopicRegex() throws Exception { + TestRepository<Repo> repo = createProject("repo"); + + ChangeInserter ins1 = newChangeWithTopic(repo, "feature1"); + Change change1 = insert(repo, ins1); + + ChangeInserter ins2 = newChangeWithTopic(repo, "Cherrypick-feature1"); + Change change2 = insert(repo, ins2); + + ChangeInserter ins3 = newChangeWithTopic(repo, "feature1-fixup"); + Change change3 = insert(repo, ins3); + + assertQuery("intopic:^feature1.*", change3, change1); + assertQuery("intopic:{^.*feature1$}", change2, change1); } @Test @@ -570,11 +756,11 @@ public void byLabel() throws Exception { accountManager.authenticate(AuthRequest.forUser("anotheruser")); TestRepository<Repo> repo = createProject("repo"); - ChangeInserter ins = newChange(repo, null, null, null, null); - ChangeInserter ins2 = newChange(repo, null, null, null, null); - ChangeInserter ins3 = newChange(repo, null, null, null, null); - ChangeInserter ins4 = newChange(repo, null, null, null, null); - ChangeInserter ins5 = newChange(repo, null, null, null, null); + ChangeInserter ins = newChange(repo, null, null, null, null, false); + ChangeInserter ins2 = newChange(repo, null, null, null, null, false); + ChangeInserter ins3 = newChange(repo, null, null, null, null, false); + ChangeInserter ins4 = newChange(repo, null, null, null, null, false); + ChangeInserter ins5 = newChange(repo, null, null, null, null, false); Change reviewMinus2Change = insert(repo, ins); gApi.changes().id(reviewMinus2Change.getId().get()).current().review(ReviewInput.reject()); @@ -654,7 +840,7 @@ @Test public void byLabelNotOwner() throws Exception { TestRepository<Repo> repo = createProject("repo"); - ChangeInserter ins = newChange(repo, null, null, null, null); + ChangeInserter ins = newChange(repo, null, null, null, null, false); Account.Id user1 = createAccount("user1"); Change reviewPlus1Change = insert(repo, ins); @@ -822,38 +1008,14 @@ } @Test - public void updatedOrderWithMinuteResolution() throws Exception { - resetTimeWithClockStep(2, MINUTES); - TestRepository<Repo> repo = createProject("repo"); - ChangeInserter ins1 = newChange(repo); - Change change1 = insert(repo, ins1); - Change change2 = insert(repo, newChange(repo)); - - assertThat(lastUpdatedMs(change1)).isLessThan(lastUpdatedMs(change2)); - assertQuery("status:new", change2, change1); - - gApi.changes().id(change1.getId().get()).topic("new-topic"); - change1 = notesFactory.create(db, change1.getProject(), change1.getId()).getChange(); - - assertThat(lastUpdatedMs(change1)).isGreaterThan(lastUpdatedMs(change2)); - assertThat(lastUpdatedMs(change1) - lastUpdatedMs(change2)) - .isGreaterThan(MILLISECONDS.convert(1, MINUTES)); - - // change1 moved to the top. - assertQuery("status:new", change1, change2); - } - - @Test - public void updatedOrderWithSubMinuteResolution() throws Exception { + public void updatedOrder() throws Exception { resetTimeWithClockStep(1, SECONDS); - TestRepository<Repo> repo = createProject("repo"); ChangeInserter ins1 = newChange(repo); Change change1 = insert(repo, ins1); Change change2 = insert(repo, newChange(repo)); assertThat(lastUpdatedMs(change1)).isLessThan(lastUpdatedMs(change2)); - assertQuery("status:new", change2, change1); gApi.changes().id(change1.getId().get()).topic("new-topic"); @@ -861,7 +1023,7 @@ assertThat(lastUpdatedMs(change1)).isGreaterThan(lastUpdatedMs(change2)); assertThat(lastUpdatedMs(change1) - lastUpdatedMs(change2)) - .isLessThan(MILLISECONDS.convert(1, MINUTES)); + .isAtLeast(MILLISECONDS.convert(1, SECONDS)); // change1 moved to the top. assertQuery("status:new", change1, change2); @@ -1343,7 +1505,8 @@ allUsers.update(draftsRef.getName(), draftsRef.getObjectId()); assertThat(allUsers.getRepository().exactRef(draftsRef.getName())).isNotNull(); - if (PrimaryStorage.of(change) == PrimaryStorage.REVIEW_DB) { + if (PrimaryStorage.of(change) == PrimaryStorage.REVIEW_DB + && !notesMigration.disableChangeReviewDb()) { // Record draft ref in noteDbState as well. ReviewDb db = ReviewDbUtil.unwrapDb(this.db); change = db.changes().get(id); @@ -1380,7 +1543,8 @@ TestRepository<Repo> repo = createProject("repo"); Change change1 = insert(repo, newChange(repo)); Change change2 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED)); - insert(repo, newChangeWithStatus(repo, Change.Status.MERGED)); + Change change3 = insert(repo, newChangeWithStatus(repo, Change.Status.MERGED)); + Change change4 = insert(repo, newChange(repo)); gApi.accounts() .self() @@ -1394,16 +1558,42 @@ new StarsInput( new HashSet<>(Arrays.asList(StarredChangesUtil.DEFAULT_LABEL, "green", "blue")))); + gApi.accounts() + .self() + .setStars( + change4.getId().toString(), new StarsInput(new HashSet<>(Arrays.asList("ignore")))); + // check labeled stars assertQuery("star:red", change1); assertQuery("star:blue", change2, change1); - assertQuery("has:stars", change2, change1); + assertQuery("has:stars", change4, change2, change1); // check default star assertQuery("has:star", change2); assertQuery("is:starred", change2); assertQuery("starredby:self", change2); assertQuery("star:" + StarredChangesUtil.DEFAULT_LABEL, change2); + + // check ignored + assertQuery("is:ignored", change4); + assertQuery("-is:ignored", change3, change2, change1); + } + + @Test + public void byIgnore() throws Exception { + TestRepository<Repo> repo = createProject("repo"); + Account.Id user2 = + accountManager.authenticate(AuthRequest.forUser("anotheruser")).getAccountId(); + Change change1 = insert(repo, newChange(repo), user2); + Change change2 = insert(repo, newChange(repo), user2); + + gApi.changes().id(change1.getId().toString()).ignore(true); + assertQuery("is:ignored", change1); + assertQuery("-is:ignored", change2); + + gApi.changes().id(change1.getId().toString()).ignore(false); + assertQuery("is:ignored"); + assertQuery("-is:ignored", change2, change1); } @Test @@ -1523,6 +1713,93 @@ } @Test + public void reviewerAndCcByEmail() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + Project.NameKey project = new Project.NameKey("repo"); + TestRepository<Repo> repo = createProject(project.get()); + ConfigInput conf = new ConfigInput(); + conf.enableReviewerByEmail = InheritableBoolean.TRUE; + gApi.projects().name(project.get()).config(conf); + + 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)); + + AddReviewerInput rin = new AddReviewerInput(); + rin.reviewer = userByEmailWithName; + rin.state = ReviewerState.REVIEWER; + gApi.changes().id(change1.getId().get()).addReviewer(rin); + + rin = new AddReviewerInput(); + rin.reviewer = userByEmailWithName; + rin.state = ReviewerState.CC; + gApi.changes().id(change2.getId().get()).addReviewer(rin); + + if (getSchemaVersion() >= 41) { + assertQuery("reviewer:\"" + userByEmailWithName + "\"", change1); + assertQuery("cc:\"" + userByEmailWithName + "\"", change2); + + // Omitting the name: + assertQuery("reviewer:\"" + userByEmail + "\"", change1); + assertQuery("cc:\"" + userByEmail + "\"", change2); + } else { + assertMissingField(ChangeField.REVIEWER_BY_EMAIL); + + assertFailingQuery( + "reviewer:\"" + userByEmailWithName + "\"", "User " + userByEmailWithName + " not found"); + assertFailingQuery( + "cc:\"" + userByEmailWithName + "\"", "User " + userByEmailWithName + " not found"); + + // Omitting the name: + assertFailingQuery("reviewer:\"" + userByEmail + "\"", "User " + userByEmail + " not found"); + assertFailingQuery("cc:\"" + userByEmail + "\"", "User " + userByEmail + " not found"); + } + } + + @Test + public void reviewerAndCcByEmailWithQueryForDifferentUser() throws Exception { + assume().that(notesMigration.enabled()).isTrue(); + + Project.NameKey project = new Project.NameKey("repo"); + TestRepository<Repo> repo = createProject(project.get()); + 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)); + + AddReviewerInput rin = new AddReviewerInput(); + rin.reviewer = userByEmail; + rin.state = ReviewerState.REVIEWER; + gApi.changes().id(change1.getId().get()).addReviewer(rin); + + rin = new AddReviewerInput(); + rin.reviewer = userByEmail; + rin.state = ReviewerState.CC; + gApi.changes().id(change2.getId().get()).addReviewer(rin); + + if (getSchemaVersion() >= 41) { + assertQuery("reviewer:\"someone@example.com\""); + assertQuery("cc:\"someone@example.com\""); + } else { + assertMissingField(ChangeField.REVIEWER_BY_EMAIL); + + String someoneEmail = "someone@example.com"; + assertFailingQuery( + "reviewer:\"" + someoneEmail + "\"", "User " + someoneEmail + " not found"); + assertFailingQuery("cc:\"" + someoneEmail + "\"", "User " + someoneEmail + " not found"); + } + } + + @Test public void submitRecords() throws Exception { Account.Id user1 = createAccount("user1"); TestRepository<Repo> repo = createProject("repo"); @@ -1610,9 +1887,28 @@ @Test public void byCommitsOnBranchNotMerged() throws Exception { + TestRepository<Repo> tr = createProject("repo"); + testByCommitsOnBranchNotMerged(tr, ImmutableSet.of()); + } + + @Test + public void byCommitsOnBranchNotMergedSkipsMissingChanges() throws Exception { TestRepository<Repo> repo = createProject("repo"); + ObjectId missing = + repo.branch(new PatchSet.Id(new Change.Id(987654), 1).toRefName()) + .commit() + .message("No change for this commit") + .insertChangeId() + .create() + .copy(); + testByCommitsOnBranchNotMerged(repo, ImmutableSet.of(missing)); + } + + private void testByCommitsOnBranchNotMerged(TestRepository<Repo> repo, Collection<ObjectId> extra) + throws Exception { int n = 10; - List<String> shas = new ArrayList<>(n); + List<String> shas = new ArrayList<>(n + extra.size()); + extra.forEach(i -> shas.add(i.name())); List<Integer> expectedIds = new ArrayList<>(n); Branch.NameKey dest = null; for (int i = 0; i < n; i++) { @@ -1621,7 +1917,7 @@ if (dest == null) { dest = ins.getChange().getDest(); } - shas.add(ins.getCommit().name()); + shas.add(ins.getCommitId().name()); expectedIds.add(ins.getChange().getId().get()); } @@ -1804,28 +2100,53 @@ .containsExactlyElementsIn(expectedPatterns); } + @Test + public void selfAndMe() throws Exception { + TestRepository<Repo> repo = createProject("repo"); + Change change1 = insert(repo, newChange(repo)); + Change change2 = insert(repo, newChange(repo), userId); + insert(repo, newChange(repo)); + gApi.accounts().self().starChange(change1.getId().toString()); + gApi.accounts().self().starChange(change2.getId().toString()); + + assertQuery("starredby:self", change2, change1); + assertQuery("starredby:me", change2, change1); + } + + @Test + public void defaultFieldWithManyUsers() throws Exception { + for (int i = 0; i < ChangeQueryBuilder.MAX_ACCOUNTS_PER_DEFAULT_FIELD * 2; i++) { + createAccount("user" + i, "User " + i, "user" + i + "@example.com", true); + } + assertQuery("us"); + } + protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception { - return newChange(repo, null, null, null, null); + return newChange(repo, null, null, null, null, false); } protected ChangeInserter newChangeForCommit(TestRepository<Repo> repo, RevCommit commit) throws Exception { - return newChange(repo, commit, null, null, null); + return newChange(repo, commit, null, null, null, false); } protected ChangeInserter newChangeForBranch(TestRepository<Repo> repo, String branch) throws Exception { - return newChange(repo, null, branch, null, null); + return newChange(repo, null, branch, null, null, false); } protected ChangeInserter newChangeWithStatus(TestRepository<Repo> repo, Change.Status status) throws Exception { - return newChange(repo, null, null, status, null); + return newChange(repo, null, null, status, null, false); } protected ChangeInserter newChangeWithTopic(TestRepository<Repo> repo, String topic) throws Exception { - return newChange(repo, null, null, null, topic); + return newChange(repo, null, null, null, topic, false); + } + + protected ChangeInserter newChangeWorkInProgress(TestRepository<Repo> repo) throws Exception { + return newChange(repo, null, null, null, null, true); } protected ChangeInserter newChange( @@ -1833,7 +2154,8 @@ @Nullable RevCommit commit, @Nullable String branch, @Nullable Change.Status status, - @Nullable String topic) + @Nullable String topic, + boolean workInProgress) throws Exception { if (commit == null) { commit = repo.parseBody(repo.commit().message("message").create()); @@ -1848,9 +2170,10 @@ ChangeInserter ins = changeFactory .create(id, commit, branch) - .setValidatePolicy(CommitValidators.Policy.NONE) + .setValidate(false) .setStatus(status) - .setTopic(topic); + .setTopic(topic) + .setWorkInProgress(workInProgress); return ins; } @@ -1893,10 +2216,12 @@ .create(ctl, new PatchSet.Id(c.getId(), n), commit) .setNotify(NotifyHandling.NONE) .setFireRevisionCreated(false) - .setValidatePolicy(CommitValidators.Policy.NONE); + .setValidate(false); try (BatchUpdate bu = updateFactory.create(db, c.getProject(), user, TimeUtil.nowTs()); - ObjectInserter oi = repo.getRepository().newObjectInserter()) { - bu.setRepository(repo.getRepository(), repo.getRevWalk(), oi); + ObjectInserter oi = repo.getRepository().newObjectInserter(); + ObjectReader reader = oi.newReader(); + RevWalk rw = new RevWalk(reader)) { + bu.setRepository(repo.getRepository(), rw, oi); bu.addOp(c.getId(), inserter); bu.execute(); } @@ -2005,4 +2330,48 @@ Patch.COMMIT_MSG, ImmutableList.<ReviewInput.CommentInput>of(comment)); gApi.changes().id(changeId).current().review(input); } + + private Account.Id createAccount(String username, String fullName, String email, boolean active) + throws Exception { + try (ManualRequestContext ctx = oneOffRequestContext.open()) { + Account.Id id = accountManager.authenticate(AuthRequest.forUser(username)).getAccountId(); + if (email != null) { + accountManager.link(id, AuthRequest.forEmail(email)); + } + accountsUpdate + .create() + .update( + db, + id, + a -> { + a.setFullName(fullName); + a.setPreferredEmail(email); + a.setActive(active); + }); + return id; + } + } + + protected void assertMissingField(FieldDef<ChangeData, ?> field) { + assertThat(getSchema().hasField(field)) + .named("schema %s has field %s", getSchemaVersion(), field.getName()) + .isFalse(); + } + + protected void assertFailingQuery(String query, String expectedMessage) throws Exception { + try { + assertQuery(query); + fail("expected BadRequestException for query '" + query + "'"); + } catch (BadRequestException e) { + assertThat(e.getMessage()).isEqualTo(expectedMessage); + } + } + + protected int getSchemaVersion() { + return getSchema().getVersion(); + } + + protected Schema<ChangeData> getSchema() { + return indexes.getSearchIndex().getSchema(); + } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java index d3ecc29..c1f5c8a 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/LuceneQueryChangesTest.java
@@ -16,16 +16,29 @@ import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.server.index.change.ChangeSchemaDefinitions; +import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.InMemoryModule; import com.google.gerrit.testutil.InMemoryRepositoryManager.Repo; +import com.google.gerrit.testutil.IndexVersions; import com.google.inject.Guice; import com.google.inject.Injector; +import java.util.List; +import java.util.Map; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Test; public class LuceneQueryChangesTest extends AbstractQueryChangesTest { + @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(ChangeSchemaDefinitions.INSTANCE); + return IndexVersions.asConfigMap( + ChangeSchemaDefinitions.INSTANCE, schemaVersions, "againstIndexVersion", defaultConfig()); + } + @Override protected Injector createInjector() { Config luceneConfig = new Config(config);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java index a0e5ee0..0de5fad 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -17,7 +17,7 @@ import static com.google.common.truth.Truth.assertThat; import static java.util.stream.Collectors.toList; -import com.google.common.collect.ImmutableList; +import com.google.common.base.CharMatcher; import com.google.gerrit.extensions.api.GerritApi; import com.google.gerrit.extensions.api.groups.GroupInput; import com.google.gerrit.extensions.api.groups.Groups.QueryRequest; @@ -33,6 +33,8 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountManager; +import com.google.gerrit.server.account.Accounts; +import com.google.gerrit.server.account.AccountsUpdate; import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.config.AllProjectsName; @@ -58,9 +60,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Ignore; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.TestName; @Ignore public abstract class AbstractQueryGroupsTest extends GerritServerTests { @@ -71,7 +71,9 @@ return cfg; } - @Rule public final TestName testName = new TestName(); + @Inject protected Accounts accounts; + + @Inject protected AccountsUpdate.Server accountsUpdate; @Inject protected AccountCache accountCache; @@ -98,6 +100,7 @@ @Inject protected GroupCache groupCache; protected LifecycleManager lifecycle; + protected Injector injector; protected ReviewDb db; protected AccountInfo currentUserInfo; protected CurrentUser user; @@ -107,11 +110,14 @@ @Before public void setUpInjector() throws Exception { lifecycle = new LifecycleManager(); - Injector injector = createInjector(); + injector = createInjector(); lifecycle.add(injector); injector.injectMembers(this); lifecycle.start(); + setUpDatabase(); + } + protected void setUpDatabase() throws Exception { db = schemaFactory.open(); schemaCreator.create(db); @@ -191,7 +197,9 @@ @Test public void byInname() throws Exception { - String namePart = testName.getMethodName(); + String namePart = getSanitizedMethodName(); + namePart = CharMatcher.is('_').removeFrom(namePart); + GroupInfo group1 = createGroup("group-" + namePart); GroupInfo group2 = createGroup("group-" + namePart + "-2"); GroupInfo group3 = createGroup("group-" + namePart + "3"); @@ -311,12 +319,16 @@ if (email != null) { accountManager.link(id, AuthRequest.forEmail(email)); } - Account a = db.accounts().get(id); - a.setFullName(fullName); - a.setPreferredEmail(email); - a.setActive(active); - db.accounts().update(ImmutableList.of(a)); - accountCache.evict(id); + accountsUpdate + .create() + .update( + db, + id, + a -> { + a.setFullName(fullName); + a.setPreferredEmail(email); + a.setActive(active); + }); return id; } } @@ -437,6 +449,7 @@ if (name == null) { return null; } - return name + "_" + testName.getMethodName().toLowerCase(); + + return name + "_" + getSanitizedMethodName(); } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java index 0551e92..001a897 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/LuceneQueryGroupsTest.java
@@ -14,12 +14,25 @@ package com.google.gerrit.server.query.group; +import com.google.gerrit.server.index.group.GroupSchemaDefinitions; +import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.InMemoryModule; +import com.google.gerrit.testutil.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; 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); + return IndexVersions.asConfigMap( + GroupSchemaDefinitions.INSTANCE, schemaVersions, "againstIndexVersion", defaultConfig()); + } + @Override protected Injector createInjector() { Config luceneConfig = new Config(config);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/HANATest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/HANATest.java index 76bee6f..ac58134 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/HANATest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/HANATest.java
@@ -33,11 +33,8 @@ @Test public void getUrl() throws Exception { - config.setString("database", null, "instance", "3"); - assertThat(hana.getUrl()).isEqualTo("jdbc:sap://my.host:30315"); - - config.setString("database", null, "instance", "77"); - assertThat(hana.getUrl()).isEqualTo("jdbc:sap://my.host:37715"); + config.setString("database", null, "port", "4242"); + assertThat(hana.getUrl()).isEqualTo("jdbc:sap://my.host:4242"); } @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java index 9a32365..74eb1d2 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -38,6 +38,7 @@ import com.google.gwtorm.server.SchemaFactory; import com.google.gwtorm.server.StatementExecutor; import com.google.inject.Guice; +import com.google.inject.Key; import com.google.inject.ProvisionException; import com.google.inject.TypeLiteral; import java.io.FileNotFoundException; @@ -82,7 +83,10 @@ new FactoryModule() { @Override protected void configure() { - bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).toInstance(db); + TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory = + new TypeLiteral<SchemaFactory<ReviewDb>>() {}; + bind(schemaFactory).to(NotesMigrationSchemaFactory.class); + bind(Key.get(schemaFactory, ReviewDbFactory.class)).toInstance(db); bind(SitePaths.class).toInstance(paths); Config cfg = new Config();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/Schema_150_to_151_Test.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/Schema_150_to_151_Test.java new file mode 100644 index 0000000..52acd9f --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/Schema_150_to_151_Test.java
@@ -0,0 +1,177 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.schema; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.api.groups.GroupInput; +import com.google.gerrit.extensions.common.GroupInfo; +import com.google.gerrit.extensions.restapi.TopLevelResource; +import com.google.gerrit.lifecycle.LifecycleManager; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.AccountGroup; +import com.google.gerrit.reviewdb.client.AccountGroup.Id; +import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.AccountManager; +import com.google.gerrit.server.account.AuthRequest; +import com.google.gerrit.server.group.CreateGroup; +import com.google.gerrit.server.util.RequestContext; +import com.google.gerrit.server.util.ThreadLocalRequestContext; +import com.google.gerrit.testutil.InMemoryDatabase; +import com.google.gerrit.testutil.InMemoryModule; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.ResultSet; +import com.google.gwtorm.server.SchemaFactory; +import com.google.gwtorm.server.StatementExecutor; +import com.google.inject.Guice; +import com.google.inject.Inject; +import com.google.inject.Injector; +import com.google.inject.Provider; +import com.google.inject.util.Providers; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.Month; +import java.time.ZoneOffset; +import java.util.List; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class Schema_150_to_151_Test { + @Inject private AccountManager accountManager; + @Inject private IdentifiedUser.GenericFactory userFactory; + @Inject private SchemaFactory<ReviewDb> schemaFactory; + @Inject private SchemaCreator schemaCreator; + @Inject private ThreadLocalRequestContext requestContext; + @Inject private Schema_151 schema151; + @Inject private CreateGroup.Factory createGroupFactory; + + // Only for use in setting up/tearing down injector. + @Inject private InMemoryDatabase inMemoryDatabase; + + private LifecycleManager lifecycle; + private ReviewDb db; + + @Before + public void setUp() throws Exception { + Injector injector = Guice.createInjector(new InMemoryModule()); + injector.injectMembers(this); + lifecycle = new LifecycleManager(); + lifecycle.add(injector); + lifecycle.start(); + + try (ReviewDb underlyingDb = inMemoryDatabase.getDatabase().open()) { + schemaCreator.create(underlyingDb); + } + db = schemaFactory.open(); + Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId(); + IdentifiedUser user = userFactory.create(userId); + + requestContext.setContext( + new RequestContext() { + @Override + public CurrentUser getUser() { + return user; + } + + @Override + public Provider<ReviewDb> getReviewDbProvider() { + return Providers.of(db); + } + }); + } + + @After + public void tearDown() { + if (lifecycle != null) { + lifecycle.stop(); + } + requestContext.setContext(null); + if (db != null) { + db.close(); + } + InMemoryDatabase.drop(inMemoryDatabase); + } + + @Test + public void createdOnIsPopulatedForGroupsCreatedAfterAudit() throws Exception { + Timestamp testStartTime = TimeUtil.nowTs(); + AccountGroup.Id groupId = createGroup("Group for schema migration"); + setCreatedOnToVeryOldTimestamp(groupId); + + schema151.migrateData(db, new TestUpdateUI()); + + AccountGroup group = db.accountGroups().get(groupId); + assertThat(group.getCreatedOn()).isAtLeast(testStartTime); + } + + @Test + public void createdOnIsPopulatedForGroupsCreatedBeforeAudit() throws Exception { + AccountGroup.Id groupId = createGroup("Ancient group for schema migration"); + setCreatedOnToVeryOldTimestamp(groupId); + removeAuditEntriesFor(groupId); + + schema151.migrateData(db, new TestUpdateUI()); + + AccountGroup group = db.accountGroups().get(groupId); + assertThat(group.getCreatedOn()).isEqualTo(AccountGroup.auditCreationInstantTs()); + } + + private AccountGroup.Id createGroup(String name) throws Exception { + GroupInput groupInput = new GroupInput(); + groupInput.name = name; + GroupInfo groupInfo = + createGroupFactory.create(name).apply(TopLevelResource.INSTANCE, groupInput); + return new Id(groupInfo.groupId); + } + + private void setCreatedOnToVeryOldTimestamp(Id groupId) throws OrmException { + AccountGroup group = db.accountGroups().get(groupId); + Instant instant = LocalDateTime.of(1800, Month.JANUARY, 1, 0, 0).toInstant(ZoneOffset.UTC); + group.setCreatedOn(Timestamp.from(instant)); + db.accountGroups().update(ImmutableList.of(group)); + } + + private void removeAuditEntriesFor(AccountGroup.Id groupId) throws Exception { + ResultSet<AccountGroupMemberAudit> groupMemberAudits = + db.accountGroupMembersAudit().byGroup(groupId); + db.accountGroupMembersAudit().delete(groupMemberAudits); + } + + private static class TestUpdateUI implements UpdateUI { + + @Override + public void message(String msg) {} + + @Override + public boolean yesno(boolean def, String msg) { + return false; + } + + @Override + public boolean isBatch() { + return false; + } + + @Override + public void pruneSchema(StatementExecutor e, List<String> pruneList) throws OrmException {} + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java index f53a59b..98160a9 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
@@ -621,11 +621,11 @@ "RealTag: abc\n")); } - private void hookDoesNotModify(final String in) throws Exception { + private void hookDoesNotModify(String in) throws Exception { assertEquals(in, call(in)); } - private String call(final String body) throws Exception { + private String call(String body) throws Exception { final File tmp = write(body); try { final File hook = getHook("commit-msg"); @@ -636,7 +636,7 @@ } } - private DirCacheEntry file(final String name) throws IOException { + private DirCacheEntry file(String name) throws IOException { try (ObjectInserter oi = repository.newObjectInserter()) { final DirCacheEntry e = new DirCacheEntry(name); e.setFileMode(FileMode.REGULAR_FILE);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java index 3d4a1a0..ec2057e 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
@@ -94,7 +94,7 @@ cleanup.clear(); } - protected File getHook(final String name) throws IOException { + protected File getHook(String name) throws IOException { File hook = hooks.get(name); if (hook != null) { return hook;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/update/BatchUpdateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/update/BatchUpdateTest.java index 892d037..dba3b3d 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/update/BatchUpdateTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -1,3 +1,17 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 org.junit.Assert.assertEquals; @@ -17,6 +31,7 @@ import com.google.gerrit.testutil.InMemoryDatabase; import com.google.gerrit.testutil.InMemoryModule; import com.google.gerrit.testutil.InMemoryRepositoryManager; +import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Guice; import com.google.inject.Inject; import com.google.inject.Injector; @@ -25,26 +40,22 @@ import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.revwalk.RevCommit; -import org.eclipse.jgit.transport.ReceiveCommand; import org.junit.After; import org.junit.Before; import org.junit.Test; public class BatchUpdateTest { @Inject private AccountManager accountManager; - @Inject private IdentifiedUser.GenericFactory userFactory; - - @Inject private InMemoryDatabase schemaFactory; - + @Inject private SchemaFactory<ReviewDb> schemaFactory; @Inject private InMemoryRepositoryManager repoManager; - @Inject private SchemaCreator schemaCreator; - @Inject private ThreadLocalRequestContext requestContext; - @Inject private BatchUpdate.Factory batchUpdateFactory; + // Only for use in setting up/tearing down injector; other users should use schemaFactory. + @Inject private InMemoryDatabase inMemoryDatabase; + private LifecycleManager lifecycle; private ReviewDb db; private TestRepository<InMemoryRepository> repo; @@ -59,8 +70,10 @@ lifecycle.add(injector); lifecycle.start(); + try (ReviewDb underlyingDb = inMemoryDatabase.getDatabase().open()) { + schemaCreator.create(underlyingDb); + } db = schemaFactory.open(); - schemaCreator.create(db); Account.Id userId = accountManager.authenticate(AuthRequest.forUser("user")).getAccountId(); user = userFactory.create(userId); @@ -95,7 +108,7 @@ if (db != null) { db.close(); } - InMemoryDatabase.drop(schemaFactory); + InMemoryDatabase.drop(inMemoryDatabase); } @Test @@ -108,9 +121,7 @@ new RepoOnlyOp() { @Override public void updateRepo(RepoContext ctx) throws Exception { - ctx.addRefUpdate( - new ReceiveCommand( - masterCommit.getId(), branchCommit.getId(), "refs/heads/master")); + ctx.addRefUpdate(masterCommit.getId(), branchCommit.getId(), "refs/heads/master"); } }); bu.execute();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/update/RefUpdateUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/update/RefUpdateUtilTest.java new file mode 100644 index 0000000..286827a --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/update/RefUpdateUtilTest.java
@@ -0,0 +1,118 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.checkState; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assert_; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.server.git.LockFailureException; +import java.io.IOException; +import java.util.function.Consumer; +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.lib.BatchRefUpdate; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class RefUpdateUtilTest { + private static final Consumer<ReceiveCommand> OK = c -> c.setResult(ReceiveCommand.Result.OK); + private static final Consumer<ReceiveCommand> LOCK_FAILURE = + c -> c.setResult(ReceiveCommand.Result.LOCK_FAILURE); + private static final Consumer<ReceiveCommand> REJECTED = + c -> c.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON); + private static final Consumer<ReceiveCommand> ABORTED = + c -> { + c.setResult(ReceiveCommand.Result.NOT_ATTEMPTED); + ReceiveCommand.abort(ImmutableList.of(c)); + checkState( + c.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED + && c.getResult() != ReceiveCommand.Result.LOCK_FAILURE + && c.getResult() != ReceiveCommand.Result.OK, + "unexpected state after abort: %s", + c); + }; + + @Test + public void checkBatchRefUpdateResults() throws Exception { + checkResults(OK); + checkResults(OK, OK); + + assertIoException(REJECTED); + assertIoException(OK, REJECTED); + assertIoException(LOCK_FAILURE, REJECTED); + assertIoException(LOCK_FAILURE, OK); + assertIoException(LOCK_FAILURE, REJECTED, OK); + assertIoException(LOCK_FAILURE, LOCK_FAILURE, REJECTED); + assertIoException(LOCK_FAILURE, ABORTED, REJECTED); + assertIoException(LOCK_FAILURE, ABORTED, OK); + + assertLockFailureException(LOCK_FAILURE); + assertLockFailureException(LOCK_FAILURE, LOCK_FAILURE); + assertLockFailureException(LOCK_FAILURE, LOCK_FAILURE, ABORTED); + assertLockFailureException(LOCK_FAILURE, LOCK_FAILURE, ABORTED, ABORTED); + assertLockFailureException(ABORTED); + assertLockFailureException(ABORTED, ABORTED); + } + + @SafeVarargs + private static void checkResults(Consumer<ReceiveCommand>... resultSetters) throws Exception { + RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters)); + } + + @SafeVarargs + private static void assertIoException(Consumer<ReceiveCommand>... resultSetters) { + try { + RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters)); + assert_().fail("expected IOException"); + } catch (IOException e) { + assertThat(e).isNotInstanceOf(LockFailureException.class); + } + } + + @SafeVarargs + private static void assertLockFailureException(Consumer<ReceiveCommand>... resultSetters) + throws Exception { + try { + RefUpdateUtil.checkResults(newBatchRefUpdate(resultSetters)); + assert_().fail("expected LockFailureException"); + } catch (LockFailureException e) { + // Expected. + } + } + + @SafeVarargs + private static BatchRefUpdate newBatchRefUpdate(Consumer<ReceiveCommand>... resultSetters) { + try (Repository repo = new InMemoryRepository(new DfsRepositoryDescription("repo"))) { + BatchRefUpdate bru = repo.getRefDatabase().newBatchUpdate(); + for (int i = 0; i < resultSetters.length; i++) { + ReceiveCommand cmd = + new ReceiveCommand( + ObjectId.fromString(String.format("%039x1", i)), + ObjectId.fromString(String.format("%039x2", i)), + "refs/heads/branch" + i); + bru.addCommand(cmd); + resultSetters[i].accept(cmd); + } + return bru; + } + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/update/RepoViewTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/update/RepoViewTest.java new file mode 100644 index 0000000..0ea9f83 --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/server/update/RepoViewTest.java
@@ -0,0 +1,154 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static org.eclipse.jgit.lib.Constants.R_HEADS; + +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.testutil.InMemoryRepositoryManager; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.transport.ReceiveCommand; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class RepoViewTest { + private static final String MASTER = "refs/heads/master"; + private static final String BRANCH = "refs/heads/branch"; + + private Repository repo; + private TestRepository<?> tr; + private RepoView view; + + @Before + public void setUp() throws Exception { + InMemoryRepositoryManager repoManager = new InMemoryRepositoryManager(); + Project.NameKey project = new Project.NameKey("project"); + repo = repoManager.createRepository(project); + tr = new TestRepository<>(repo); + tr.branch(MASTER).commit().create(); + view = new RepoView(repoManager, project); + } + + @After + public void tearDown() { + view.close(); + repo.close(); + } + + @Test + public void getConfigIsDefensiveCopy() throws Exception { + StoredConfig orig = repo.getConfig(); + orig.setString("a", "config", "option", "yes"); + orig.save(); + + Config copy = view.getConfig(); + copy.setString("a", "config", "option", "no"); + + assertThat(orig.getString("a", "config", "option")).isEqualTo("yes"); + assertThat(repo.getConfig().getString("a", "config", "option")).isEqualTo("yes"); + } + + @Test + public void getRef() throws Exception { + ObjectId oldMaster = repo.exactRef(MASTER).getObjectId(); + assertThat(repo.exactRef(MASTER).getObjectId()).isEqualTo(oldMaster); + assertThat(repo.exactRef(BRANCH)).isNull(); + assertThat(view.getRef(MASTER)).hasValue(oldMaster); + assertThat(view.getRef(BRANCH)).isEmpty(); + + tr.branch(MASTER).commit().create(); + tr.branch(BRANCH).commit().create(); + assertThat(repo.exactRef(MASTER).getObjectId()).isNotEqualTo(oldMaster); + assertThat(repo.exactRef(BRANCH)).isNotNull(); + assertThat(view.getRef(MASTER)).hasValue(oldMaster); + assertThat(view.getRef(BRANCH)).isEmpty(); + } + + @Test + public void getRefsRescansWhenNotCaching() throws Exception { + ObjectId oldMaster = repo.exactRef(MASTER).getObjectId(); + assertThat(view.getRefs(R_HEADS)).containsExactly("master", oldMaster); + + ObjectId newBranch = tr.branch(BRANCH).commit().create(); + assertThat(view.getRefs(R_HEADS)).containsExactly("master", oldMaster, "branch", newBranch); + } + + @Test + public void getRefsUsesCachedValueMatchingGetRef() throws Exception { + ObjectId master1 = repo.exactRef(MASTER).getObjectId(); + assertThat(view.getRefs(R_HEADS)).containsExactly("master", master1); + assertThat(view.getRef(MASTER)).hasValue(master1); + + // Doesn't reflect new value for master. + ObjectId master2 = tr.branch(MASTER).commit().create(); + assertThat(repo.exactRef(MASTER).getObjectId()).isEqualTo(master2); + assertThat(view.getRefs(R_HEADS)).containsExactly("master", master1); + + // Branch wasn't previously cached, so does reflect new value. + ObjectId branch1 = tr.branch(BRANCH).commit().create(); + assertThat(view.getRefs(R_HEADS)).containsExactly("master", master1, "branch", branch1); + + // Looking up branch causes it to be cached. + assertThat(view.getRef(BRANCH)).hasValue(branch1); + ObjectId branch2 = tr.branch(BRANCH).commit().create(); + assertThat(repo.exactRef(BRANCH).getObjectId()).isEqualTo(branch2); + assertThat(view.getRefs(R_HEADS)).containsExactly("master", master1, "branch", branch1); + } + + @Test + public void getRefsReflectsCommands() throws Exception { + ObjectId master1 = repo.exactRef(MASTER).getObjectId(); + assertThat(view.getRefs(R_HEADS)).containsExactly("master", master1); + + ObjectId master2 = tr.commit().create(); + view.getCommands().add(new ReceiveCommand(master1, master2, MASTER)); + + assertThat(repo.exactRef(MASTER).getObjectId()).isEqualTo(master1); + assertThat(view.getRef(MASTER)).hasValue(master2); + assertThat(view.getRefs(R_HEADS)).containsExactly("master", master2); + + view.getCommands().add(new ReceiveCommand(master2, ObjectId.zeroId(), MASTER)); + + assertThat(repo.exactRef(MASTER).getObjectId()).isEqualTo(master1); + assertThat(view.getRef(MASTER)).isEmpty(); + assertThat(view.getRefs(R_HEADS)).isEmpty(); + } + + @Test + public void getRefsOverwritesCachedValueWithCommand() throws Exception { + ObjectId master1 = repo.exactRef(MASTER).getObjectId(); + assertThat(view.getRef(MASTER)).hasValue(master1); + + ObjectId master2 = tr.commit().create(); + view.getCommands().add(new ReceiveCommand(master1, master2, MASTER)); + + assertThat(repo.exactRef(MASTER).getObjectId()).isEqualTo(master1); + assertThat(view.getRef(MASTER)).hasValue(master2); + assertThat(view.getRefs(R_HEADS)).containsExactly("master", master2); + + view.getCommands().add(new ReceiveCommand(master2, ObjectId.zeroId(), MASTER)); + + assertThat(repo.exactRef(MASTER).getObjectId()).isEqualTo(master1); + assertThat(view.getRef(MASTER)).isEmpty(); + assertThat(view.getRefs(R_HEADS)).isEmpty(); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/ConfigSuite.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/ConfigSuite.java index c29b06e..7a4b2b0 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/ConfigSuite.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/ConfigSuite.java
@@ -18,8 +18,10 @@ import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static java.util.stream.Collectors.toSet; import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import java.lang.annotation.Annotation; @@ -29,7 +31,10 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.List; +import java.util.Map; import org.junit.runner.Runner; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.Suite; @@ -80,11 +85,26 @@ * Additionally, config values used by <strong>default</strong> can be set in a method annotated * with {@code @ConfigSuite.Default}. * + * <p>In addition groups of tests for different configurations can be defined by annotating a method + * that returns a Map<String, Config> with {@link Configs}. The map keys define the test suite + * names, while the values define the configurations for the test suites. + * + * <pre> + * {@literal @}ConfigSuite.Configs + * public static Map<String, Config> configs() { + * Config cfgA = new Config(); + * cfgA.setString("gerrit", null, "testValue", "a"); + * Config cfgB = new Config(); + * cfgB.setString("gerrit", null, "testValue", "b"); + * return ImmutableMap.of("testWithValueA", cfgA, "testWithValueB", cfgB); + * } + * </pre> + * * <p>The name of the config method corresponding to the currently-running test can be stored in a * field annotated with {@code @ConfigSuite.Name}. */ public class ConfigSuite extends Suite { - private static final String DEFAULT = "default"; + public static final String DEFAULT = "default"; @Target({METHOD}) @Retention(RUNTIME) @@ -94,6 +114,10 @@ @Retention(RUNTIME) public static @interface Config {} + @Target({METHOD}) + @Retention(RUNTIME) + public static @interface Configs {} + @Target({FIELD}) @Retention(RUNTIME) public static @interface Parameter {} @@ -103,25 +127,29 @@ public static @interface Name {} private static class ConfigRunner extends BlockJUnit4ClassRunner { - private final Method configMethod; + private final org.eclipse.jgit.lib.Config cfg; private final Field parameterField; private final Field nameField; private final String name; private ConfigRunner( - Class<?> clazz, Field parameterField, Field nameField, String name, Method configMethod) + Class<?> clazz, + Field parameterField, + Field nameField, + String name, + org.eclipse.jgit.lib.Config cfg) throws InitializationError { super(clazz); this.parameterField = parameterField; this.nameField = nameField; this.name = name; - this.configMethod = configMethod; + this.cfg = cfg; } @Override public Object createTest() throws Exception { Object test = getTestClass().getJavaClass().newInstance(); - parameterField.set(test, callConfigMethod(configMethod)); + parameterField.set(test, new org.eclipse.jgit.lib.Config(cfg)); if (nameField != null) { nameField.set(test, name); } @@ -143,14 +171,23 @@ private static List<Runner> runnersFor(Class<?> clazz) { Method defaultConfig = getDefaultConfig(clazz); List<Method> configs = getConfigs(clazz); + Map<String, org.eclipse.jgit.lib.Config> configMap = + callConfigMapMethod(getConfigMap(clazz), configs); + Field parameterField = getOnlyField(clazz, Parameter.class); checkArgument(parameterField != null, "No @ConfigSuite.Parameter found"); Field nameField = getOnlyField(clazz, Name.class); List<Runner> result = Lists.newArrayListWithCapacity(configs.size() + 1); try { - result.add(new ConfigRunner(clazz, parameterField, nameField, null, defaultConfig)); + result.add( + new ConfigRunner( + clazz, parameterField, nameField, null, callConfigMethod(defaultConfig))); for (Method m : configs) { - result.add(new ConfigRunner(clazz, parameterField, nameField, m.getName(), m)); + result.add( + new ConfigRunner(clazz, parameterField, nameField, m.getName(), callConfigMethod(m))); + } + for (Map.Entry<String, org.eclipse.jgit.lib.Config> e : configMap.entrySet()) { + result.add(new ConfigRunner(clazz, parameterField, nameField, e.getKey(), e.getValue())); } return result; } catch (InitializationError e) { @@ -163,15 +200,20 @@ } private static Method getDefaultConfig(Class<?> clazz) { + return getAnnotatedMethod(clazz, Default.class); + } + + private static Method getConfigMap(Class<?> clazz) { + return getAnnotatedMethod(clazz, Configs.class); + } + + private static <T extends Annotation> Method getAnnotatedMethod( + Class<?> clazz, Class<T> annotationClass) { Method result = null; for (Method m : clazz.getMethods()) { - Default ann = m.getAnnotation(Default.class); + T ann = m.getAnnotation(annotationClass); if (ann != null) { - checkArgument( - result == null, - "Multiple methods annotated with @ConfigSuite.Method: %s, %s", - result, - m); + checkArgument(result == null, "Multiple methods annotated with %s: %s, %s", ann, result, m); result = m; } } @@ -183,8 +225,7 @@ for (Method m : clazz.getMethods()) { Config ann = m.getAnnotation(Config.class); if (ann != null) { - checkArgument( - !m.getName().equals(DEFAULT), "@ConfigSuite.Config cannot be named %s", DEFAULT); + checkArgument(!m.getName().equals(DEFAULT), "%s cannot be named %s", ann, DEFAULT); result.add(m); } } @@ -208,6 +249,45 @@ } } + private static Map<String, org.eclipse.jgit.lib.Config> callConfigMapMethod( + Method m, List<Method> configs) { + if (m == null) { + return ImmutableMap.of(); + } + checkArgument(Map.class.isAssignableFrom(m.getReturnType()), "%s must return Map", m); + Type[] types = ((ParameterizedType) m.getGenericReturnType()).getActualTypeArguments(); + checkArgument( + String.class.isAssignableFrom((Class<?>) types[0]), + "The map returned by %s must have String as key", + m); + checkArgument( + org.eclipse.jgit.lib.Config.class.isAssignableFrom((Class<?>) types[1]), + "The map returned by %s must have Config as value", + m); + checkArgument((m.getModifiers() & Modifier.STATIC) != 0, "%s must be static", m); + checkArgument(m.getParameterTypes().length == 0, "%s must take no parameters", m); + try { + @SuppressWarnings("unchecked") + Map<String, org.eclipse.jgit.lib.Config> configMap = + (Map<String, org.eclipse.jgit.lib.Config>) m.invoke(null); + checkArgument( + !configMap.containsKey(DEFAULT), + "The map returned by %s cannot contain key %s (duplicate test suite name)", + m, + DEFAULT); + for (String name : configs.stream().map(cm -> cm.getName()).collect(toSet())) { + checkArgument( + !configMap.containsKey(name), + "The map returned by %s cannot contain key %s (duplicate test suite name)", + m, + name); + } + return configMap; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new IllegalArgumentException(e); + } + } + private static Field getOnlyField(Class<?> clazz, Class<? extends Annotation> ann) { List<Field> fields = Lists.newArrayListWithExpectedSize(1); for (Field f : clazz.getFields()) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java index 885a1f5..870dba7 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
@@ -15,7 +15,6 @@ package com.google.gerrit.testutil; import com.google.gerrit.reviewdb.server.AccountAccess; -import com.google.gerrit.reviewdb.server.AccountExternalIdAccess; import com.google.gerrit.reviewdb.server.AccountGroupAccess; import com.google.gerrit.reviewdb.server.AccountGroupByIdAccess; import com.google.gerrit.reviewdb.server.AccountGroupByIdAudAccess; @@ -89,11 +88,6 @@ } @Override - public AccountExternalIdAccess accountExternalIds() { - throw new Disabled(); - } - - @Override public AccountGroupAccess accountGroups() { throw new Disabled(); }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java index 3c5bc85..242f208 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeAccountCache.java
@@ -35,7 +35,7 @@ @Override public synchronized AccountState get(Account.Id accountId) { - AccountState state = getIfPresent(accountId); + AccountState state = byId.get(accountId); if (state != null) { return state; } @@ -49,11 +49,6 @@ } @Override - public synchronized AccountState getIfPresent(Account.Id accountId) { - return byId.get(accountId); - } - - @Override public synchronized AccountState getByUsername(String username) { return byUsername.get(username); } @@ -69,7 +64,7 @@ } @Override - public synchronized void evictAll() { + public synchronized void evictAllNoReindex() { byId.clear(); byUsername.clear(); }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java index ed3e41f..c70d241 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/FakeEmailSender.java
@@ -82,11 +82,13 @@ private final WorkQueue workQueue; private final List<Message> messages; + private int messagesRead; @Inject FakeEmailSender(WorkQueue workQueue) { this.workQueue = workQueue; messages = Collections.synchronizedList(new ArrayList<Message>()); + messagesRead = 0; } @Override @@ -121,9 +123,23 @@ waitForEmails(); synchronized (messages) { messages.clear(); + messagesRead = 0; } } + public synchronized @Nullable Message peekMessage() { + if (messagesRead >= messages.size()) { + return null; + } + return messages.get(messagesRead); + } + + public synchronized @Nullable Message nextMessage() { + Message msg = peekMessage(); + messagesRead++; + return msg; + } + public ImmutableList<Message> getMessages() { waitForEmails(); synchronized (messages) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritBaseTests.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritBaseTests.java index 4f08c9e..4ba5d3a 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritBaseTests.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritBaseTests.java
@@ -14,11 +14,13 @@ package com.google.gerrit.testutil; +import com.google.common.base.CharMatcher; import com.google.gwtorm.client.KeyUtil; import com.google.gwtorm.server.StandardKeyEncoder; import org.junit.Ignore; import org.junit.Rule; import org.junit.rules.ExpectedException; +import org.junit.rules.TestName; @Ignore public abstract class GerritBaseTests { @@ -27,4 +29,12 @@ } @Rule public ExpectedException exception = ExpectedException.none(); + @Rule public final TestName testName = new TestName(); + + protected String getSanitizedMethodName() { + String name = testName.getMethodName().toLowerCase(); + name = CharMatcher.javaLetterOrDigit().negate().replaceFrom(name, '_'); + name = CharMatcher.is('_').trimTrailingFrom(name); + return name; + } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java index 038baac..8f37c20 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/GerritServerTests.java
@@ -33,7 +33,7 @@ public TestRule testRunner = new TestRule() { @Override - public Statement apply(final Statement base, final Description description) { + public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { @@ -49,8 +49,10 @@ }; public void beforeTest() throws Exception { - notesMigration = new TestNotesMigration().setFromEnv(); + notesMigration = new TestNotesMigration(); } - public void afterTest() {} + public void afterTest() { + notesMigration.resetFromEnv(); + } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java index bff27ca..21b21ef 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -65,7 +65,7 @@ } /** Drop the database from memory; does nothing if the instance was null. */ - public static void drop(final InMemoryDatabase db) { + public static void drop(InMemoryDatabase db) { if (db != null) { db.drop(); }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java index 9944508..365ddab 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryModule.java
@@ -14,6 +14,7 @@ package com.google.gerrit.testutil; +import static com.google.common.base.Preconditions.checkState; import static com.google.inject.Scopes.SINGLETON; import com.google.common.util.concurrent.ListeningExecutorService; @@ -48,17 +49,23 @@ import com.google.gerrit.server.git.SearchingChangeCacheImpl; import com.google.gerrit.server.git.SendEmailExecutor; import com.google.gerrit.server.index.IndexModule.IndexType; +import com.google.gerrit.server.index.SchemaDefinitions; +import com.google.gerrit.server.index.account.AccountSchemaDefinitions; import com.google.gerrit.server.index.account.AllAccountsIndexer; import com.google.gerrit.server.index.change.AllChangesIndexer; import com.google.gerrit.server.index.change.ChangeSchemaDefinitions; import com.google.gerrit.server.index.group.AllGroupsIndexer; +import com.google.gerrit.server.index.group.GroupSchemaDefinitions; import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier; import com.google.gerrit.server.notedb.ChangeBundleReader; import com.google.gerrit.server.notedb.GwtormChangeBundleReader; import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.patch.DiffExecutor; +import com.google.gerrit.server.project.DefaultPermissionBackendModule; import com.google.gerrit.server.schema.DataSourceType; import com.google.gerrit.server.schema.H2AccountPatchReviewStore; +import com.google.gerrit.server.schema.NotesMigrationSchemaFactory; +import com.google.gerrit.server.schema.ReviewDbFactory; import com.google.gerrit.server.schema.SchemaCreator; import com.google.gerrit.server.securestore.DefaultSecureStore; import com.google.gerrit.server.securestore.SecureStore; @@ -69,6 +76,7 @@ import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; +import com.google.inject.Key; import com.google.inject.Module; import com.google.inject.Provider; import com.google.inject.Provides; @@ -146,6 +154,7 @@ }); bind(MetricMaker.class).to(DisabledMetricMaker.class); install(cfgInjector.getInstance(GerritGlobalModule.class)); + install(new DefaultPermissionBackendModule()); install(new SearchingChangeCacheImpl.Module()); factory(GarbageCollection.Factory.class); @@ -171,13 +180,15 @@ bind(ListeningExecutorService.class) .annotatedWith(ChangeUpdateExecutor.class) .toInstance(MoreExecutors.newDirectExecutorService()); - bind(DataSourceType.class).to(InMemoryH2Type.class); - bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).to(InMemoryDatabase.class); bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class); - bind(SecureStore.class).to(DefaultSecureStore.class); + TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory = + new TypeLiteral<SchemaFactory<ReviewDb>>() {}; + bind(schemaFactory).to(NotesMigrationSchemaFactory.class); + bind(Key.get(schemaFactory, ReviewDbFactory.class)).to(InMemoryDatabase.class); + install(NoSshKeyCache.module()); install( new CanonicalWebUrlModule() { @@ -252,14 +263,9 @@ private Module indexModule(String moduleClassName) { try { - Map<String, Integer> singleVersions = new HashMap<>(); - int version = cfg.getInt("index", "lucene", "testVersion", -1); - if (version > 0) { - singleVersions.put(ChangeSchemaDefinitions.INSTANCE.getName(), version); - } Class<?> clazz = Class.forName(moduleClassName); Method m = clazz.getMethod("singleVersionWithExplicitVersions", Map.class, int.class); - return (Module) m.invoke(null, singleVersions, 0); + return (Module) m.invoke(null, getSingleSchemaVersions(), 0); } catch (ClassNotFoundException | SecurityException | NoSuchMethodException @@ -272,4 +278,25 @@ throw pe; } } + + private Map<String, Integer> getSingleSchemaVersions() { + Map<String, Integer> singleVersions = new HashMap<>(); + putSchemaVersion(singleVersions, AccountSchemaDefinitions.INSTANCE); + putSchemaVersion(singleVersions, ChangeSchemaDefinitions.INSTANCE); + putSchemaVersion(singleVersions, GroupSchemaDefinitions.INSTANCE); + return singleVersions; + } + + private void putSchemaVersion( + Map<String, Integer> singleVersions, SchemaDefinitions<?> schemaDef) { + String schemaName = schemaDef.getName(); + int version = cfg.getInt("index", "lucene", schemaName + "TestVersion", -1); + if (version > 0) { + checkState( + !singleVersions.containsKey(schemaName), + "version for schema %s was alreay set", + schemaName); + singleVersions.put(schemaName, version); + } + } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java index 4826d9e..a90cbb9 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
@@ -19,6 +19,8 @@ import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.RepositoryCaseMismatchException; +import com.google.gerrit.server.notedb.NotesMigration; +import com.google.inject.Inject; import java.util.HashMap; import java.util.Map; import java.util.SortedSet; @@ -30,7 +32,7 @@ /** Repository manager that uses in-memory repositories. */ public class InMemoryRepositoryManager implements GitRepositoryManager { public static InMemoryRepository newRepository(Project.NameKey name) { - return new Repo(name); + return new Repo(new TestNotesMigration(), name); } public static class Description extends DfsRepositoryDescription { @@ -49,11 +51,15 @@ public static class Repo extends InMemoryRepository { private String description; - private Repo(Project.NameKey name) { + private Repo(NotesMigration notesMigration, Project.NameKey name) { super(new Description(name)); - // TODO(dborowitz): Allow atomic transactions when this is supported: - // https://git.eclipse.org/r/#/c/61841/2/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/dfs/InMemoryRepository.java@313 - setPerformsAtomicTransactions(false); + // Normally, mimic the behavior of JGit FileRepository, the standard Gerrit repository + // backend, and don't support atomic ref updates. The exception is when we're testing with + // fused ref updates, which requires atomic ref updates to function. + // + // TODO(dborowitz): Change to match the behavior of JGit FileRepository after fixing + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=515678 + setPerformsAtomicTransactions(notesMigration.fuseUpdates()); } @Override @@ -72,7 +78,18 @@ } } - private Map<String, Repo> repos = new HashMap<>(); + private final NotesMigration notesMigration; + private final Map<String, Repo> repos; + + public InMemoryRepositoryManager() { + this(new TestNotesMigration()); + } + + @Inject + InMemoryRepositoryManager(NotesMigration notesMigration) { + this.notesMigration = notesMigration; + this.repos = new HashMap<>(); + } @Override public synchronized Repo openRepository(Project.NameKey name) throws RepositoryNotFoundException { @@ -89,7 +106,7 @@ throw new RepositoryCaseMismatchException(name); } } catch (RepositoryNotFoundException e) { - repo = new Repo(name); + repo = new Repo(notesMigration, name); repos.put(normalize(name), repo); } return repo;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/IndexVersions.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/IndexVersions.java new file mode 100644 index 0000000..825cd7b --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/IndexVersions.java
@@ -0,0 +1,146 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.testutil; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.stream.Collectors.toMap; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Ints; +import com.google.gerrit.server.index.Schema; +import com.google.gerrit.server.index.SchemaDefinitions; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import org.eclipse.jgit.lib.Config; + +public class IndexVersions { + static final String ALL = "all"; + static final String CURRENT = "current"; + static final String PREVIOUS = "previous"; + + /** + * Returns the index versions from {@link IndexVersions#get(SchemaDefinitions)} without the latest + * schema version. + * + * @param schemaDef the schema definition + * @return the index versions from {@link IndexVersions#get(SchemaDefinitions)} without the latest + * schema version + */ + public static <V> ImmutableList<Integer> getWithoutLatest(SchemaDefinitions<V> schemaDef) { + List<Integer> schemaVersions = new ArrayList<>(get(schemaDef)); + schemaVersions.remove(Integer.valueOf(schemaDef.getLatest().getVersion())); + return ImmutableList.copyOf(schemaVersions); + } + + /** + * Returns the schema versions against which the query tests should be executed. + * + * <p>The schema versions are read from the '<schema-name>_INDEX_VERSIONS' env var if it is set, + * e.g. 'ACCOUNTS_INDEX_VERSIONS', 'CHANGES_INDEX_VERSIONS', 'GROUPS_INDEX_VERSIONS'. + * + * <p>If schema versions were not specified by an env var, they are read from the + * 'gerrit.index.<schema-name>.versions' system property, e.g. 'gerrit.index.accounts.version', + * 'gerrit.index.changes.version', 'gerrit.index.groups.version'. + * + * <p>As value a comma-separated list of schema versions is expected. {@code current} can be used + * for the latest schema version and {@code previous} is resolved to the second last schema + * version. Alternatively the value can also be {@code all} for all schema versions. + * + * <p>If schema versions were neither specified by an env var nor by a system property, the + * current and the second last schema versions are returned. If there is no other schema version + * than the current schema version, only the current schema version is returned. + * + * @param schemaDef the schema definition + * @return the schema versions against which the query tests should be executed + * @throws IllegalArgumentException if the value of the env var or system property is invalid or + * if any of the specified schema versions doesn't exist + */ + public static <V> ImmutableList<Integer> get(SchemaDefinitions<V> schemaDef) { + String envVar = schemaDef.getName().toUpperCase() + "_INDEX_VERSIONS"; + String value = System.getenv(envVar); + if (!Strings.isNullOrEmpty(value)) { + return get(schemaDef, "env variable " + envVar, value); + } + + String systemProperty = "gerrit.index." + schemaDef.getName().toLowerCase() + ".versions"; + value = System.getProperty(systemProperty); + return get(schemaDef, "system property " + systemProperty, value); + } + + @VisibleForTesting + static <V> ImmutableList<Integer> get(SchemaDefinitions<V> schemaDef, String name, String value) { + if (value != null) { + value = value.trim(); + } + + SortedMap<Integer, Schema<V>> schemas = schemaDef.getSchemas(); + if (!Strings.isNullOrEmpty(value)) { + if (ALL.equals(value)) { + return ImmutableList.copyOf(schemas.keySet()); + } + + List<Integer> versions = new ArrayList<>(); + for (String s : Splitter.on(',').trimResults().split(value)) { + if (CURRENT.equals(s)) { + versions.add(schemaDef.getLatest().getVersion()); + } else if (PREVIOUS.equals(s)) { + checkArgument(schemaDef.getPrevious() != null, "previous version does not exist"); + versions.add(schemaDef.getPrevious().getVersion()); + } else { + Integer version = Ints.tryParse(s); + checkArgument(version != null, "Invalid value for %s: %s", name, s); + checkArgument( + schemas.containsKey(version), + "Index version %s that was specified by %s not found." + " Possible versions are: %s", + version, + name, + schemas.keySet()); + versions.add(version); + } + } + return ImmutableList.copyOf(versions); + } + + List<Integer> schemaVersions = new ArrayList<>(2); + if (schemaDef.getPrevious() != null) { + schemaVersions.add(schemaDef.getPrevious().getVersion()); + } + schemaVersions.add(schemaDef.getLatest().getVersion()); + return ImmutableList.copyOf(schemaVersions); + } + + public static <V> Map<String, Config> asConfigMap( + SchemaDefinitions<V> schemaDef, + List<Integer> schemaVersions, + String testSuiteNamePrefix, + Config baseConfig) { + return schemaVersions + .stream() + .collect( + toMap( + i -> testSuiteNamePrefix + i, + i -> { + Config cfg = baseConfig; + cfg.setInt( + "index", "lucene", schemaDef.getName().toLowerCase() + "TestVersion", i); + return cfg; + })); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/IndexVersionsTest.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/IndexVersionsTest.java new file mode 100644 index 0000000..d3c889a --- /dev/null +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/IndexVersionsTest.java
@@ -0,0 +1,140 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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.testutil; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; +import static com.google.gerrit.testutil.IndexVersions.ALL; +import static com.google.gerrit.testutil.IndexVersions.CURRENT; +import static com.google.gerrit.testutil.IndexVersions.PREVIOUS; + +import com.google.gerrit.server.index.change.ChangeSchemaDefinitions; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +public class IndexVersionsTest extends GerritBaseTests { + private static final ChangeSchemaDefinitions SCHEMA_DEF = ChangeSchemaDefinitions.INSTANCE; + + @Test + public void noValue() { + List<Integer> expected = new ArrayList<>(); + if (SCHEMA_DEF.getPrevious() != null) { + expected.add(SCHEMA_DEF.getPrevious().getVersion()); + } + expected.add(SCHEMA_DEF.getLatest().getVersion()); + + assertThat(get(null)).containsExactlyElementsIn(expected).inOrder(); + } + + @Test + public void emptyValue() { + List<Integer> expected = new ArrayList<>(); + if (SCHEMA_DEF.getPrevious() != null) { + expected.add(SCHEMA_DEF.getPrevious().getVersion()); + } + expected.add(SCHEMA_DEF.getLatest().getVersion()); + + assertThat(get("")).containsExactlyElementsIn(expected).inOrder(); + } + + @Test + public void all() { + assertThat(get(ALL)).containsExactlyElementsIn(SCHEMA_DEF.getSchemas().keySet()).inOrder(); + } + + @Test + public void current() { + assertThat(get(CURRENT)).containsExactly(SCHEMA_DEF.getLatest().getVersion()); + } + + @Test + public void previous() { + assume().that(SCHEMA_DEF.getPrevious()).isNotNull(); + + assertThat(get(PREVIOUS)).containsExactly(SCHEMA_DEF.getPrevious().getVersion()); + } + + @Test + public void versionNumber() { + assume().that(SCHEMA_DEF.getPrevious()).isNotNull(); + + assertThat(get(Integer.toString(SCHEMA_DEF.getPrevious().getVersion()))) + .containsExactly(SCHEMA_DEF.getPrevious().getVersion()); + } + + @Test + public void invalid() { + assertIllegalArgument("foo", "Invalid value for test: foo"); + } + + @Test + public void currentAndPrevious() { + if (SCHEMA_DEF.getPrevious() == null) { + assertIllegalArgument(CURRENT + "," + PREVIOUS, "previous version does not exist"); + return; + } + + assertThat(get(CURRENT + "," + PREVIOUS)) + .containsExactly(SCHEMA_DEF.getLatest().getVersion(), SCHEMA_DEF.getPrevious().getVersion()) + .inOrder(); + assertThat(get(PREVIOUS + "," + CURRENT)) + .containsExactly(SCHEMA_DEF.getPrevious().getVersion(), SCHEMA_DEF.getLatest().getVersion()) + .inOrder(); + } + + @Test + public void currentAndVersionNumber() { + assume().that(SCHEMA_DEF.getPrevious()).isNotNull(); + + assertThat(get(CURRENT + "," + SCHEMA_DEF.getPrevious().getVersion())) + .containsExactly(SCHEMA_DEF.getLatest().getVersion(), SCHEMA_DEF.getPrevious().getVersion()) + .inOrder(); + assertThat(get(SCHEMA_DEF.getPrevious().getVersion() + "," + CURRENT)) + .containsExactly(SCHEMA_DEF.getPrevious().getVersion(), SCHEMA_DEF.getLatest().getVersion()) + .inOrder(); + } + + @Test + public void currentAndAll() { + assertIllegalArgument(CURRENT + "," + ALL, "Invalid value for test: " + ALL); + } + + @Test + public void currentAndInvalid() { + assertIllegalArgument(CURRENT + ",foo", "Invalid value for test: foo"); + } + + @Test + public void nonExistingVersion() { + int nonExistingVersion = SCHEMA_DEF.getLatest().getVersion() + 1; + assertIllegalArgument( + Integer.toString(nonExistingVersion), + "Index version " + + nonExistingVersion + + " that was specified by test not found. Possible versions are: " + + SCHEMA_DEF.getSchemas().keySet()); + } + + private static List<Integer> get(String value) { + return IndexVersions.get(ChangeSchemaDefinitions.INSTANCE, "test", value); + } + + private void assertIllegalArgument(String value, String expectedMessage) { + exception.expect(IllegalArgumentException.class); + exception.expectMessage(expectedMessage); + get(value); + } +}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java index aeaaa47..ad876ce 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
@@ -89,7 +89,7 @@ List<ChangeBundle> allExpected = readExpected(changeIds); - boolean oldWrite = notesMigration.writeChanges(); + boolean oldWrite = notesMigration.rawWriteChangesSetting(); boolean oldRead = notesMigration.readChanges(); try { notesMigration.setWriteChanges(true); @@ -162,7 +162,7 @@ private void checkActual(List<ChangeBundle> allExpected, List<String> msgs) throws Exception { ReviewDb db = getUnwrappedDb(); boolean oldRead = notesMigration.readChanges(); - boolean oldWrite = notesMigration.writeChanges(); + boolean oldWrite = notesMigration.rawWriteChangesSetting(); try { notesMigration.setWriteChanges(true); notesMigration.setReadChanges(true);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java index 552f6f1..45d8546 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
@@ -18,28 +18,33 @@ import com.google.common.base.Enums; import com.google.common.base.Strings; +import com.google.gerrit.server.notedb.NotesMigration; +import com.google.gerrit.server.notedb.NotesMigrationState; public enum NoteDbMode { /** NoteDb is disabled. */ - OFF(false), + OFF(NotesMigrationState.REVIEW_DB), /** Writing data to NoteDb is enabled. */ - WRITE(false), + WRITE(NotesMigrationState.WRITE), /** Reading and writing all data to NoteDb is enabled. */ - READ_WRITE(true), + READ_WRITE(NotesMigrationState.READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY), /** Changes are created with their primary storage as NoteDb. */ - PRIMARY(true), + PRIMARY(NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY), /** All change tables are entirely disabled. */ - DISABLE_CHANGE_REVIEW_DB(true), + DISABLE_CHANGE_REVIEW_DB(NotesMigrationState.NOTE_DB_UNFUSED), + + /** All change tables are entirely disabled, and code/meta ref updates are fused. */ + FUSED(NotesMigrationState.NOTE_DB), /** * Run tests with NoteDb disabled, then convert ReviewDb to NoteDb and check that the results * match. */ - CHECK(false); + CHECK(NotesMigrationState.REVIEW_DB); private static final String ENV_VAR = "GERRIT_NOTEDB"; private static final String SYS_PROP = "gerrit.notedb"; @@ -68,12 +73,13 @@ } public static boolean readWrite() { - return get().readWrite; + NotesMigration migration = get().migration; + return migration.rawWriteChangesSetting() && migration.readChanges(); } - private final boolean readWrite; + final NotesMigration migration; - private NoteDbMode(boolean readWrite) { - this.readWrite = readWrite; + private NoteDbMode(NotesMigrationState state) { + migration = state.migration(); } }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/SshMode.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/SshMode.java index 9320331..0bf643cc 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/SshMode.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/SshMode.java
@@ -19,6 +19,12 @@ import com.google.common.base.Enums; import com.google.common.base.Strings; +/** + * Whether to enable/disable tests using SSH by inspecting the global environment. + * + * <p>Acceptance tests should generally not inspect this directly, since SSH may also be disabled on + * a per-class or per-method basis. Inject {@code @SshEnabled boolean} instead. + */ public enum SshMode { /** Tests annotated with UseSsh will be disabled. */ NO,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java index 459bccd..eadbaae 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestChanges.java
@@ -81,7 +81,7 @@ return ps; } - public static ChangeUpdate newUpdate(Injector injector, Change c, final CurrentUser user) + public static ChangeUpdate newUpdate(Injector injector, Change c, CurrentUser user) throws Exception { injector = injector.createChildInjector(
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java index e6a72fc..a891358 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java +++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
@@ -25,10 +25,16 @@ public class TestNotesMigration extends NotesMigration { private volatile boolean readChanges; private volatile boolean writeChanges; + private volatile boolean readChangeSequence; private volatile PrimaryStorage changePrimaryStorage = PrimaryStorage.REVIEW_DB; private volatile boolean disableChangeReviewDb; + private volatile boolean fuseUpdates; private volatile boolean failOnLoad; + public TestNotesMigration() { + resetFromEnv(); + } + @Override public boolean readChanges() { return readChanges; @@ -36,9 +42,7 @@ @Override public boolean readChangeSequence() { - // Unlike ConfigNotesMigration, read change numbers from NoteDb by default - // when reads are enabled, to improve test coverage. - return readChanges; + return readChangeSequence; } @Override @@ -51,10 +55,15 @@ return disableChangeReviewDb; } + @Override + public boolean fuseUpdates() { + return fuseUpdates; + } + // Increase visbility from superclass, as tests may want to check whether // NoteDb data is written in specific migration scenarios. @Override - public boolean writeChanges() { + public boolean rawWriteChangesSetting() { return writeChanges; } @@ -73,6 +82,11 @@ return this; } + public TestNotesMigration setReadChangeSequence(boolean readChangeSequence) { + this.readChangeSequence = readChangeSequence; + return this; + } + public TestNotesMigration setChangePrimaryStorage(PrimaryStorage changePrimaryStorage) { this.changePrimaryStorage = checkNotNull(changePrimaryStorage); return this; @@ -83,6 +97,11 @@ return this; } + public TestNotesMigration setFuseUpdates(boolean fuseUpdates) { + this.fuseUpdates = fuseUpdates; + return this; + } + public TestNotesMigration setFailOnLoad(boolean failOnLoad) { this.failOnLoad = failOnLoad; return this; @@ -92,41 +111,18 @@ return setReadChanges(enabled).setWriteChanges(enabled); } - public TestNotesMigration setFromEnv() { - switch (NoteDbMode.get()) { - case READ_WRITE: - setWriteChanges(true); - setReadChanges(true); - setChangePrimaryStorage(PrimaryStorage.REVIEW_DB); - setDisableChangeReviewDb(false); - break; - case WRITE: - setWriteChanges(true); - setReadChanges(false); - setChangePrimaryStorage(PrimaryStorage.REVIEW_DB); - setDisableChangeReviewDb(false); - break; - case PRIMARY: - setWriteChanges(true); - setReadChanges(true); - setChangePrimaryStorage(PrimaryStorage.NOTE_DB); - setDisableChangeReviewDb(false); - break; - case DISABLE_CHANGE_REVIEW_DB: - setWriteChanges(true); - setReadChanges(true); - setChangePrimaryStorage(PrimaryStorage.NOTE_DB); - setDisableChangeReviewDb(true); - break; - case CHECK: - case OFF: - default: - setWriteChanges(false); - setReadChanges(false); - setChangePrimaryStorage(PrimaryStorage.REVIEW_DB); - setDisableChangeReviewDb(false); - break; - } + public TestNotesMigration resetFromEnv() { + return setFrom(NoteDbMode.get().migration); + } + + @Override + public TestNotesMigration setFrom(NotesMigration other) { + setWriteChanges(other.rawWriteChangesSetting()); + setReadChanges(other.readChanges()); + setReadChangeSequence(other.readChangeSequence()); + setChangePrimaryStorage(other.changePrimaryStorage()); + setDisableChangeReviewDb(other.disableChangeReviewDb()); + setFuseUpdates(other.fuseUpdates()); return this; } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java index 4144ed2..318e67f 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -18,6 +18,7 @@ import com.google.gerrit.server.AccessPath; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.sshd.SshScope.Context; import com.google.inject.Inject; @@ -47,7 +48,7 @@ protected Project project; @Override - public void start(final Environment env) { + public void start(Environment env) { Context ctx = context.subContext(newSession(), context.getCommandLine()); final Context old = sshScope.set(ctx); try { @@ -84,7 +85,7 @@ return n; } - private void service() throws IOException, Failure { + private void service() throws IOException, PermissionBackendException, Failure { project = projectControl.getProjectState().getProject(); try { @@ -100,5 +101,5 @@ } } - protected abstract void runImpl() throws IOException, Failure; + protected abstract void runImpl() throws IOException, PermissionBackendException, Failure; }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java index 45835d9..0ac7765 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -16,12 +16,16 @@ import com.google.common.base.Throwables; import com.google.common.util.concurrent.Atomics; -import com.google.gerrit.extensions.annotations.RequiresCapability; +import com.google.gerrit.extensions.api.access.GlobalOrPluginPermission; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.server.CurrentUser; -import com.google.gerrit.server.account.CapabilityControl; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import java.io.IOException; import java.util.LinkedList; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import org.apache.sshd.server.Command; import org.apache.sshd.server.Environment; @@ -30,14 +34,17 @@ public class AliasCommand extends BaseCommand { private final DispatchCommandProvider root; private final CurrentUser currentUser; + private final PermissionBackend permissionBackend; private final CommandName command; private final AtomicReference<Command> atomicCmd; AliasCommand( @CommandName(Commands.ROOT) DispatchCommandProvider root, + PermissionBackend permissionBackend, CurrentUser currentUser, CommandName command) { this.root = root; + this.permissionBackend = permissionBackend; this.currentUser = currentUser; this.command = command; this.atomicCmd = Atomics.newReference(); @@ -47,7 +54,7 @@ public void start(Environment env) throws IOException { try { begin(env); - } catch (UnloggedFailure e) { + } catch (Failure e) { String msg = e.getMessage(); if (!msg.endsWith("\n")) { msg += "\n"; @@ -58,7 +65,7 @@ } } - private void begin(Environment env) throws UnloggedFailure, IOException { + private void begin(Environment env) throws IOException, Failure { Map<String, CommandProvider> map = root.getMap(); for (String name : chain(command)) { CommandProvider p = map.get(name); @@ -103,17 +110,16 @@ } } - private void checkRequiresCapability(Command cmd) throws UnloggedFailure { - RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class); - if (rc != null) { - CapabilityControl ctl = currentUser.getCapabilities(); - if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) { - String msg = - String.format( - "fatal: %s does not have \"%s\" capability.", - currentUser.getUserName(), rc.value()); - throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg); + private void checkRequiresCapability(Command cmd) throws Failure { + try { + Set<GlobalOrPluginPermission> check = GlobalPermission.fromAnnotation(cmd.getClass()); + try { + permissionBackend.user(currentUser).checkAny(check); + } catch (AuthException err) { + throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, "fatal: " + err.getMessage()); } + } catch (PermissionBackendException err) { + throw new Failure(1, "fatal: permissions unavailable", err); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java index 10beb40..0ef0473 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
@@ -15,6 +15,7 @@ package com.google.gerrit.sshd; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.inject.Inject; import com.google.inject.Provider; import org.apache.sshd.server.Command; @@ -27,6 +28,7 @@ @CommandName(Commands.ROOT) private DispatchCommandProvider root; + @Inject private PermissionBackend permissionBackend; @Inject private CurrentUser currentUser; public AliasCommandProvider(CommandName command) { @@ -35,6 +37,6 @@ @Override public Command get() { - return new AliasCommand(root, currentUser, command); + return new AliasCommand(root, permissionBackend, currentUser, command); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java index bc465ec..6923ad1 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -20,19 +20,25 @@ import com.google.gerrit.common.Nullable; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.extensions.registration.DynamicMap; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.DynamicOptions; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.RequestCleanup; import com.google.gerrit.server.git.ProjectRunnable; -import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.git.WorkQueue.CancelableRunnable; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.NoSuchProjectException; import com.google.gerrit.sshd.SshScope.Context; import com.google.gerrit.util.cli.CmdLineParser; import com.google.gerrit.util.cli.EndOfOptionsHandler; import com.google.inject.Inject; +import com.google.inject.Injector; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; @@ -43,6 +49,7 @@ import java.io.StringWriter; import java.nio.charset.Charset; import java.util.concurrent.Future; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.atomic.AtomicReference; import org.apache.sshd.common.SshException; import org.apache.sshd.server.Command; @@ -78,8 +85,9 @@ @Inject private RequestCleanup cleanup; - @Inject @CommandExecutor private WorkQueue.Executor executor; + @Inject @CommandExecutor private ScheduledThreadPoolExecutor executor; + @Inject private PermissionBackend permissionBackend; @Inject private CurrentUser user; @Inject private SshScope.Context context; @@ -89,6 +97,10 @@ @PluginName private String pluginName; + @Inject private Injector injector; + + @Inject private DynamicMap<DynamicOptions.DynamicBean> dynamicBeans = null; + /** The task, as scheduled on a worker thread. */ private final AtomicReference<Future<?>> task; @@ -103,22 +115,22 @@ } @Override - public void setInputStream(final InputStream in) { + public void setInputStream(InputStream in) { this.in = in; } @Override - public void setOutputStream(final OutputStream out) { + public void setOutputStream(OutputStream out) { this.out = out; } @Override - public void setErrorStream(final OutputStream err) { + public void setErrorStream(OutputStream err) { this.err = err; } @Override - public void setExitCallback(final ExitCallback callback) { + public void setExitCallback(ExitCallback callback) { this.exit = callback; } @@ -131,7 +143,7 @@ return commandName; } - void setName(final String prefix) { + void setName(String prefix) { this.commandName = prefix; } @@ -139,7 +151,7 @@ return argv; } - public void setArguments(final String[] argv) { + public void setArguments(String[] argv) { this.argv = argv; } @@ -160,7 +172,7 @@ * * @param cmd the command that will receive the current state. */ - protected void provideStateTo(final Command cmd) { + protected void provideStateTo(Command cmd) { cmd.setInputStream(in); cmd.setOutputStream(out); cmd.setErrorStream(err); @@ -193,6 +205,10 @@ */ protected void parseCommandLine(Object options) throws UnloggedFailure { final CmdLineParser clp = newCmdLineParser(options); + DynamicOptions pluginOptions = new DynamicOptions(options, injector, dynamicBeans); + pluginOptions.parseDynamicBeans(clp); + pluginOptions.setDynamicBeans(); + pluginOptions.onBeanParseStart(); try { clp.parseArgument(argv); } catch (IllegalArgumentException | CmdLineException err) { @@ -207,6 +223,7 @@ msg.write(usage()); throw new UnloggedFailure(1, msg.toString()); } + pluginOptions.onBeanParseEnd(); } protected String usage() { @@ -224,31 +241,6 @@ * <p>Typically this should be invoked within {@link Command#start(Environment)}, such as: * * <pre> - * startThread(new Runnable() { - * public void run() { - * runImp(); - * } - * }); - * </pre> - * - * @param thunk the runnable to execute on the thread, performing the command's logic. - */ - protected void startThread(final Runnable thunk) { - startThread( - new CommandRunnable() { - @Override - public void run() throws Exception { - thunk.run(); - } - }); - } - - /** - * Spawn a function into its own thread. - * - * <p>Typically this should be invoked within {@link Command#start(Environment)}, such as: - * - * <pre> * startThread(new CommandRunnable() { * public void run() throws Exception { * runImp(); @@ -261,10 +253,10 @@ * * @param thunk the runnable to execute on the thread, performing the command's logic. */ - protected void startThread(final CommandRunnable thunk) { + protected void startThread(CommandRunnable thunk) { final TaskThunk tt = new TaskThunk(thunk); - if (isAdminHighPriorityCommand() && user.getCapabilities().canAdministrateServer()) { + if (isAdminHighPriorityCommand()) { // Admin commands should not block the main work threads (there // might be an interactive shell there), nor should they wait // for the main work threads. @@ -276,7 +268,15 @@ } private boolean isAdminHighPriorityCommand() { - return getClass().getAnnotation(AdminHighPriorityCommand.class) != null; + if (getClass().getAnnotation(AdminHighPriorityCommand.class) != null) { + try { + permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER); + return true; + } catch (AuthException | PermissionBackendException e) { + return false; + } + } + return false; } /** @@ -288,7 +288,7 @@ * * @param rc exit code for the remote client. */ - protected void onExit(final int rc) { + protected void onExit(int rc) { exit.onExit(rc); if (cleanup != null) { cleanup.run(); @@ -296,11 +296,11 @@ } /** Wrap the supplied output stream in a UTF-8 encoded PrintWriter. */ - protected static PrintWriter toPrintWriter(final OutputStream o) { + protected static PrintWriter toPrintWriter(OutputStream o) { return new PrintWriter(new BufferedWriter(new OutputStreamWriter(o, ENC))); } - private int handleError(final Throwable e) { + private int handleError(Throwable e) { if ((e.getClass() == IOException.class && "Pipe closed".equals(e.getMessage())) || // (e.getClass() == SshException.class && "Already closed".equals(e.getMessage())) @@ -374,7 +374,7 @@ private final String taskName; private Project.NameKey projectName; - private TaskThunk(final CommandRunnable thunk) { + private TaskThunk(CommandRunnable thunk) { this.thunk = thunk; StringBuilder m = new StringBuilder(); @@ -469,6 +469,7 @@ } /** Runnable function which can throw an exception. */ + @FunctionalInterface public interface CommandRunnable { void run() throws Exception; } @@ -495,7 +496,7 @@ * command. Should be between 1 and 255, inclusive. * @param msg message to also send to the client's stderr. */ - public Failure(final int exitCode, final String msg) { + public Failure(int exitCode, String msg) { this(exitCode, msg, null); } @@ -508,7 +509,7 @@ * @param why stack trace to include in the server's log, but is not sent to the client's * stderr. */ - public Failure(final int exitCode, final String msg, final Throwable why) { + public Failure(int exitCode, String msg, Throwable why) { super(msg, why); this.exitCode = exitCode; } @@ -523,7 +524,7 @@ * * @param msg message to also send to the client's stderr. */ - public UnloggedFailure(final String msg) { + public UnloggedFailure(String msg) { this(1, msg); } @@ -534,7 +535,7 @@ * command. Should be between 1 and 255, inclusive. * @param msg message to also send to the client's stderr. */ - public UnloggedFailure(final int exitCode, final String msg) { + public UnloggedFailure(int exitCode, String msg) { this(exitCode, msg, null); } @@ -547,7 +548,7 @@ * @param why stack trace to include in the server's log, but is not sent to the client's * stderr. */ - public UnloggedFailure(final int exitCode, final String msg, final Throwable why) { + public UnloggedFailure(int exitCode, String msg, Throwable why) { super(exitCode, msg, why); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java index 4488c71..e64ab0e 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/ChangeArgumentParser.java
@@ -16,6 +16,7 @@ import static java.util.stream.Collectors.toList; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.server.ReviewDb; @@ -24,6 +25,9 @@ import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.ChangesCollection; import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.ProjectControl; @@ -43,6 +47,7 @@ private final ReviewDb db; private final ChangeNotes.Factory changeNotesFactory; private final ChangeControl.GenericFactory changeControlFactory; + private final PermissionBackend permissionBackend; @Inject ChangeArgumentParser( @@ -51,13 +56,15 @@ ChangeFinder changeFinder, ReviewDb db, ChangeNotes.Factory changeNotesFactory, - ChangeControl.GenericFactory changeControlFactory) { + ChangeControl.GenericFactory changeControlFactory, + PermissionBackend permissionBackend) { this.currentUser = currentUser; this.changesCollection = changesCollection; this.changeFinder = changeFinder; this.db = db; this.changeNotesFactory = changeNotesFactory; this.changeControlFactory = changeControlFactory; + this.permissionBackend = permissionBackend; } public void addChange(String id, Map<Change.Id, ChangeResource> changes) @@ -80,9 +87,13 @@ List<ChangeControl> matched = useIndex ? changeFinder.find(id, currentUser) : changeFromNotesFactory(id, currentUser); List<ChangeControl> toAdd = new ArrayList<>(changes.size()); - boolean canMaintainServer = - currentUser.isIdentifiedUser() - && currentUser.asIdentifiedUser().getCapabilities().canMaintainServer(); + boolean canMaintainServer; + try { + permissionBackend.user(currentUser).check(GlobalPermission.MAINTAIN_SERVER); + canMaintainServer = true; + } catch (AuthException | PermissionBackendException e) { + canMaintainServer = false; + } for (ChangeControl ctl : matched) { if (!changes.containsKey(ctl.getId()) && inProject(projectControl, ctl.getProject())
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutor.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutor.java index fa21c58..4fd55a1 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutor.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutor.java
@@ -16,11 +16,11 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.google.gerrit.server.git.WorkQueue.Executor; import com.google.inject.BindingAnnotation; import java.lang.annotation.Retention; +import java.util.concurrent.ScheduledThreadPoolExecutor; -/** Marker on {@link Executor} used by SSH threads. */ +/** Marker on {@link ScheduledThreadPoolExecutor} used by SSH threads. */ @Retention(RUNTIME) @BindingAnnotation public @interface CommandExecutor {}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorProvider.java index 8c47144..5bc59e9 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorProvider.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorProvider.java
@@ -15,24 +15,27 @@ package com.google.gerrit.sshd; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.git.QueueProvider; -import com.google.gerrit.server.git.WorkQueue; import com.google.inject.Inject; import com.google.inject.Provider; +import java.util.concurrent.ScheduledThreadPoolExecutor; -class CommandExecutorProvider implements Provider<WorkQueue.Executor> { - +class CommandExecutorProvider implements Provider<ScheduledThreadPoolExecutor> { + private final CapabilityControl.Factory capabilityFactory; private final QueueProvider queues; private final CurrentUser user; @Inject - CommandExecutorProvider(QueueProvider queues, CurrentUser user) { + CommandExecutorProvider( + CapabilityControl.Factory capabilityFactory, QueueProvider queues, CurrentUser user) { + this.capabilityFactory = capabilityFactory; this.queues = queues; this.user = user; } @Override - public WorkQueue.Executor get() { - return queues.getQueue(user.getCapabilities().getQueueType()); + public ScheduledThreadPoolExecutor get() { + return queues.getQueue(capabilityFactory.create(user).getQueueType()); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java index 075dca3..f729b931 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
@@ -19,15 +19,15 @@ import com.google.gerrit.server.git.QueueProvider; import com.google.gerrit.server.git.WorkQueue; import com.google.inject.Inject; -import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ScheduledThreadPoolExecutor; import org.eclipse.jgit.lib.Config; public class CommandExecutorQueueProvider implements QueueProvider { private int poolSize; private final int batchThreads; - private final WorkQueue.Executor interactiveExecutor; - private final WorkQueue.Executor batchExecutor; + private final ScheduledThreadPoolExecutor interactiveExecutor; + private final ScheduledThreadPoolExecutor batchExecutor; @Inject public CommandExecutorQueueProvider( @@ -41,31 +41,17 @@ poolSize += batchThreads; } int interactiveThreads = Math.max(1, poolSize - batchThreads); - interactiveExecutor = queues.createQueue(interactiveThreads, "SSH-Interactive-Worker"); + interactiveExecutor = + queues.createQueue(interactiveThreads, "SSH-Interactive-Worker", Thread.MIN_PRIORITY); if (batchThreads != 0) { - batchExecutor = queues.createQueue(batchThreads, "SSH-Batch-Worker"); - setThreadFactory(batchExecutor); + batchExecutor = queues.createQueue(batchThreads, "SSH-Batch-Worker", Thread.MIN_PRIORITY); } else { batchExecutor = interactiveExecutor; } - setThreadFactory(interactiveExecutor); - } - - private void setThreadFactory(WorkQueue.Executor executor) { - final ThreadFactory parent = executor.getThreadFactory(); - executor.setThreadFactory( - new ThreadFactory() { - @Override - public Thread newThread(final Runnable task) { - final Thread t = parent.newThread(task); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); } @Override - public WorkQueue.Executor getQueue(QueueType type) { + public ScheduledThreadPoolExecutor getQueue(QueueType type) { switch (type) { case INTERACTIVE: return interactiveExecutor;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java index c6d750c..0287ceb 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -60,11 +60,11 @@ @Inject CommandFactoryProvider( - @CommandName(Commands.ROOT) final DispatchCommandProvider d, - @GerritServerConfig final Config cfg, - final WorkQueue workQueue, - final SshLog l, - final SshScope s, + @CommandName(Commands.ROOT) DispatchCommandProvider d, + @GerritServerConfig Config cfg, + WorkQueue workQueue, + SshLog l, + SshScope s, SchemaFactory<ReviewDb> sf) { dispatcher = d; log = l; @@ -93,7 +93,7 @@ public CommandFactory get() { return new CommandFactory() { @Override - public Command createCommand(final String requestCommand) { + public Command createCommand(String requestCommand) { return new Trampoline(requestCommand); } }; @@ -112,7 +112,7 @@ private final AtomicBoolean logged; private final AtomicReference<Future<?>> task; - Trampoline(final String cmdLine) { + Trampoline(String cmdLine) { commandLine = cmdLine; argv = split(cmdLine); logged = new AtomicBoolean(); @@ -120,33 +120,33 @@ } @Override - public void setInputStream(final InputStream in) { + public void setInputStream(InputStream in) { this.in = in; } @Override - public void setOutputStream(final OutputStream out) { + public void setOutputStream(OutputStream out) { this.out = out; } @Override - public void setErrorStream(final OutputStream err) { + public void setErrorStream(OutputStream err) { this.err = err; } @Override - public void setExitCallback(final ExitCallback callback) { + public void setExitCallback(ExitCallback callback) { this.exit = callback; } @Override - public void setSession(final ServerSession session) { + public void setSession(ServerSession session) { final SshSession s = session.getAttribute(SshSession.KEY); this.ctx = sshScope.newContext(schemaFactory, s, commandLine); } @Override - public void start(final Environment env) throws IOException { + public void start(Environment env) throws IOException { this.env = env; final Context ctx = this.ctx; task.set( @@ -203,7 +203,7 @@ } } - private int translateExit(final int rc) { + private int translateExit(int rc) { switch (rc) { case BaseCommand.STATUS_NOT_ADMIN: return 1; @@ -219,7 +219,7 @@ } } - private void log(final int rc) { + private void log(int rc) { if (logged.compareAndSet(false, true)) { log.onExecute(cmd, rc, ctx.getSession()); } @@ -230,13 +230,7 @@ Future<?> future = task.getAndSet(null); if (future != null) { future.cancel(true); - destroyExecutor.execute( - new Runnable() { - @Override - public void run() { - onDestroy(); - } - }); + destroyExecutor.execute(this::onDestroy); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java index 54ffba6..93aab0b 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
@@ -28,7 +28,7 @@ * @param name the name of the command the client will provide in order to call the command. * @return a binding that must be bound to a non-singleton provider for a {@link Command} object. */ - protected LinkedBindingBuilder<Command> command(final String name) { + protected LinkedBindingBuilder<Command> command(String name) { return bind(Commands.key(name)); } @@ -38,7 +38,7 @@ * @param name the name of the command the client will provide in order to call the command. * @return a binding that must be bound to a non-singleton provider for a {@link Command} object. */ - protected LinkedBindingBuilder<Command> command(final CommandName name) { + protected LinkedBindingBuilder<Command> command(CommandName name) { return bind(Commands.key(name)); } @@ -49,7 +49,7 @@ * @param name the name of the command the client will provide in order to call the command. * @return a binding that must be bound to a non-singleton provider for a {@link Command} object. */ - protected LinkedBindingBuilder<Command> command(final CommandName parent, final String name) { + protected LinkedBindingBuilder<Command> command(CommandName parent, String name) { return bind(Commands.key(parent, name)); } @@ -60,7 +60,7 @@ * @param clazz class of the command with {@link CommandMetaData} annotation to retrieve the name * and the description from */ - protected void command(final CommandName parent, final Class<? extends BaseCommand> clazz) { + protected void command(CommandName parent, Class<? extends BaseCommand> clazz) { CommandMetaData meta = clazz.getAnnotation(CommandMetaData.class); if (meta == null) { throw new IllegalStateException("no CommandMetaData annotation found"); @@ -78,8 +78,7 @@ * @param clazz class of the command with {@link CommandMetaData} annotation to retrieve the * description from */ - protected void alias( - final CommandName parent, final String name, final Class<? extends BaseCommand> clazz) { + protected void alias(final CommandName parent, String name, Class<? extends BaseCommand> clazz) { CommandMetaData meta = clazz.getAnnotation(CommandMetaData.class); if (meta == null) { throw new IllegalStateException("no CommandMetaData annotation found"); @@ -95,7 +94,7 @@ * @param to name of an already registered command that will perform the action when {@code from} * is invoked by a client. */ - protected void alias(final String from, final String to) { + protected void alias(String from, String to) { bind(Commands.key(from)).to(Commands.key(to)); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandProvider.java index 200d3a0..61c36cb 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandProvider.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandProvider.java
@@ -22,7 +22,7 @@ private final Provider<Command> provider; private final String description; - CommandProvider(final Provider<Command> p, final String d) { + CommandProvider(Provider<Command> p, String d) { this.provider = p; this.description = d; }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java index 620ffbe..43d2c50 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
@@ -27,40 +27,40 @@ /** Magic value signaling the top level. */ public static final CommandName CMD_ROOT = named(ROOT); - public static Key<Command> key(final String name) { + public static Key<Command> key(String name) { return key(named(name)); } - public static Key<Command> key(final CommandName name) { + public static Key<Command> key(CommandName name) { return Key.get(Command.class, name); } - public static Key<Command> key(final CommandName parent, final String name) { + public static Key<Command> key(CommandName parent, String name) { return Key.get(Command.class, named(parent, name)); } - public static Key<Command> key(final CommandName parent, final String name, final String descr) { + public static Key<Command> key(CommandName parent, String name, String descr) { return Key.get(Command.class, named(parent, name, descr)); } /** Create a CommandName annotation for the supplied name. */ @AutoAnnotation - public static CommandName named(final String value) { + public static CommandName named(String value) { return new AutoAnnotation_Commands_named(value); } /** Create a CommandName annotation for the supplied name. */ - public static CommandName named(final CommandName parent, final String name) { + public static CommandName named(CommandName parent, String name) { return new NestedCommandNameImpl(parent, name); } /** Create a CommandName annotation for the supplied name and description. */ - public static CommandName named(final CommandName parent, final String name, final String descr) { + public static CommandName named(CommandName parent, String name, String descr) { return new NestedCommandNameImpl(parent, name, descr); } /** Return the name of this command, possibly including any parents. */ - public static String nameOf(final CommandName name) { + public static String nameOf(CommandName name) { if (name instanceof NestedCommandNameImpl) { return nameOf(((NestedCommandNameImpl) name).parent) + " " + name.value(); } @@ -68,7 +68,7 @@ } /** Is the second command a direct child of the first command? */ - public static boolean isChild(final CommandName parent, final CommandName name) { + public static boolean isChild(CommandName parent, CommandName name) { if (name instanceof NestedCommandNameImpl) { return parent.equals(((NestedCommandNameImpl) name).parent); } @@ -90,13 +90,13 @@ private final String name; private final String descr; - NestedCommandNameImpl(final CommandName parent, final String name) { + NestedCommandNameImpl(CommandName parent, String name) { this.parent = parent; this.name = name; this.descr = ""; } - NestedCommandNameImpl(final CommandName parent, final String name, final String descr) { + NestedCommandNameImpl(CommandName parent, String name, String descr) { this.parent = parent; this.name = name; this.descr = descr; @@ -122,7 +122,7 @@ } @Override - public boolean equals(final Object obj) { + public boolean equals(Object obj) { return obj instanceof NestedCommandNameImpl && parent.equals(((NestedCommandNameImpl) obj).parent) && value().equals(((NestedCommandNameImpl) obj).value());
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java index d655500..7e0406a 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -62,14 +62,14 @@ @Inject DatabasePubKeyAuth( - final SshKeyCacheImpl skc, - final SshLog l, - final IdentifiedUser.GenericFactory uf, - final PeerDaemonUser.Factory pf, - final SitePaths site, - final KeyPairProvider hostKeyProvider, - @GerritServerConfig final Config cfg, - final SshScope s) { + SshKeyCacheImpl skc, + SshLog l, + IdentifiedUser.GenericFactory uf, + PeerDaemonUser.Factory pf, + SitePaths site, + KeyPairProvider hostKeyProvider, + @GerritServerConfig Config cfg, + SshScope s) { sshKeyCache = skc; sshLog = l; userFactory = uf; @@ -92,7 +92,7 @@ } private static void addPublicKey( - final Collection<PublicKey> out, final KeyPairProvider p, final String type) { + final Collection<PublicKey> out, KeyPairProvider p, String type) { final KeyPair pair = p.loadKey(type); if (pair != null && pair.getPublic() != null) { out.add(pair.getPublic()); @@ -162,9 +162,8 @@ return p.keys; } - private SshKeyCacheEntry find( - final Iterable<SshKeyCacheEntry> keyList, final PublicKey suppliedKey) { - for (final SshKeyCacheEntry k : keyList) { + private SshKeyCacheEntry find(Iterable<SshKeyCacheEntry> keyList, PublicKey suppliedKey) { + for (SshKeyCacheEntry k : keyList) { if (k.match(suppliedKey)) { return k; }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java index 2f3d10f6..3f2e258 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -20,8 +20,10 @@ import com.google.common.util.concurrent.Atomics; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.server.CurrentUser; -import com.google.gerrit.server.account.CapabilityUtils; import com.google.gerrit.server.args4j.SubcommandHandler; +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 com.google.inject.assistedinject.Assisted; import java.io.IOException; @@ -41,6 +43,7 @@ } private final CurrentUser currentUser; + private final PermissionBackend permissionBackend; private final Map<String, CommandProvider> commands; private final AtomicReference<Command> atomicCmd; @@ -51,8 +54,12 @@ private List<String> args = new ArrayList<>(); @Inject - DispatchCommand(CurrentUser cu, @Assisted final Map<String, CommandProvider> all) { - currentUser = cu; + DispatchCommand( + CurrentUser user, + PermissionBackend permissionBackend, + @Assisted Map<String, CommandProvider> all) { + this.currentUser = user; + this.permissionBackend = permissionBackend; commands = all; atomicCmd = Atomics.newReference(); } @@ -62,7 +69,7 @@ } @Override - public void start(final Environment env) throws IOException { + public void start(Environment env) throws IOException { try { parseCommandLine(); if (Strings.isNullOrEmpty(commandName)) { @@ -117,9 +124,13 @@ pluginName = ((BaseCommand) cmd).getPluginName(); } try { - CapabilityUtils.checkRequiresCapability(currentUser, pluginName, cmd.getClass()); + permissionBackend + .user(currentUser) + .checkAny(GlobalPermission.fromAnnotation(pluginName, cmd.getClass())); } catch (AuthException e) { throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, e.getMessage()); + } catch (PermissionBackendException e) { + throw new UnloggedFailure(1, "fatal: permission check unavailable", e); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java index 2a88f63..c782d2f 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
@@ -35,7 +35,7 @@ private final CommandName parent; private volatile ConcurrentMap<String, CommandProvider> map; - public DispatchCommandProvider(final CommandName cn) { + public DispatchCommandProvider(CommandName cn) { this.parent = cn; } @@ -44,7 +44,7 @@ return factory.create(getMap()); } - public RegistrationHandle register(final CommandName name, final Provider<Command> cmd) { + public RegistrationHandle register(CommandName name, Provider<Command> cmd) { final ConcurrentMap<String, CommandProvider> m = getMap(); final CommandProvider commandProvider = new CommandProvider(cmd, null); if (m.putIfAbsent(name.value(), commandProvider) != null) { @@ -58,7 +58,7 @@ }; } - public RegistrationHandle replace(final CommandName name, final Provider<Command> cmd) { + public RegistrationHandle replace(CommandName name, Provider<Command> cmd) { final ConcurrentMap<String, CommandProvider> m = getMap(); final CommandProvider commandProvider = new CommandProvider(cmd, null); m.put(name.value(), commandProvider); @@ -84,7 +84,7 @@ @SuppressWarnings("unchecked") private ConcurrentMap<String, CommandProvider> createMap() { ConcurrentMap<String, CommandProvider> m = Maps.newConcurrentMap(); - for (final Binding<?> b : allCommands()) { + for (Binding<?> b : allCommands()) { final Annotation annotation = b.getKey().getAnnotation(); if (annotation instanceof CommandName) { final CommandName n = (CommandName) annotation;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java index 0b1f3ae..8e4be78 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/GerritGSSAuthenticator.java
@@ -51,7 +51,7 @@ } @Override - public boolean validateIdentity(final ServerSession session, final String identity) { + public boolean validateIdentity(ServerSession session, String identity) { final SshSession sd = session.getAttribute(SshSession.KEY); int at = identity.indexOf('@'); String username;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java index 20694b2..c0b6d5a 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java
@@ -31,7 +31,7 @@ private final SitePaths site; @Inject - HostKeyProvider(final SitePaths site) { + HostKeyProvider(SitePaths site) { this.site = site; }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java index eafdcd6..aec85d4 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
@@ -76,33 +76,33 @@ } @Override - public void setInputStream(final InputStream in) { + public void setInputStream(InputStream in) { this.in = in; } @Override - public void setOutputStream(final OutputStream out) { + public void setOutputStream(OutputStream out) { this.out = out; } @Override - public void setErrorStream(final OutputStream err) { + public void setErrorStream(OutputStream err) { this.err = err; } @Override - public void setExitCallback(final ExitCallback callback) { + public void setExitCallback(ExitCallback callback) { this.exit = callback; } @Override - public void setSession(final ServerSession session) { + public void setSession(ServerSession session) { SshSession s = session.getAttribute(SshSession.KEY); this.context = sshScope.newContext(schemaFactory, s, ""); } @Override - public void start(final Environment env) throws IOException { + public void start(Environment env) throws IOException { Context old = sshScope.set(context); String message; try {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java index fe8197d..b0116e4 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
@@ -46,7 +46,7 @@ command(command, clazz); } - protected void alias(final String name, Class<? extends BaseCommand> clazz) { + protected void alias(String name, Class<? extends BaseCommand> clazz) { alias(command, name, clazz); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java index 3c108b0..4b37dad 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -149,16 +149,16 @@ @Inject SshDaemon( - final CommandFactory commandFactory, - final NoShell noShell, - final PublickeyAuthenticator userAuth, - final GerritGSSAuthenticator kerberosAuth, - final KeyPairProvider hostKeyProvider, - final IdGenerator idGenerator, - @GerritServerConfig final Config cfg, - final SshLog sshLog, - @SshListenAddresses final List<SocketAddress> listen, - @SshAdvertisedAddresses final List<String> advertised, + CommandFactory commandFactory, + NoShell noShell, + PublickeyAuthenticator userAuth, + GerritGSSAuthenticator kerberosAuth, + KeyPairProvider hostKeyProvider, + IdGenerator idGenerator, + @GerritServerConfig Config cfg, + SshLog sshLog, + @SshListenAddresses List<SocketAddress> listen, + @SshAdvertisedAddresses List<String> advertised, MetricMaker metricMaker) { setPort(IANA_SSH_PORT /* never used */); @@ -257,7 +257,7 @@ setSessionFactory( new SessionFactory(this) { @Override - protected ServerSessionImpl createSession(final IoSession io) throws Exception { + protected ServerSessionImpl createSession(IoSession io) throws Exception { connected.incrementAndGet(); sessionsCreated.increment(); if (io instanceof MinaSession) { @@ -392,12 +392,12 @@ final List<PublicKey> keys = myHostKeys(); final List<HostKey> r = new ArrayList<>(); - for (final PublicKey pub : keys) { + for (PublicKey pub : keys) { final Buffer buf = new ByteArrayBuffer(); buf.putRawPublicKey(pub); final byte[] keyBin = buf.getCompactData(); - for (final String addr : advertised) { + for (String addr : advertised) { try { r.add(new HostKey(addr, keyBin)); } catch (JSchException e) { @@ -423,7 +423,7 @@ } private static void addPublicKey( - final Collection<PublicKey> out, final KeyPairProvider p, final String type) { + final Collection<PublicKey> out, KeyPairProvider p, String type) { final KeyPair pair = p.loadKey(type); if (pair != null && pair.getPublic() != null) { out.add(pair.getPublic()); @@ -523,7 +523,7 @@ } @SuppressWarnings("unchecked") - private void initCiphers(final Config cfg) { + private void initCiphers(Config cfg) { final List<NamedFactory<Cipher>> a = BaseBuilder.setUpDefaultCiphers(true); for (Iterator<NamedFactory<Cipher>> i = a.iterator(); i.hasNext(); ) { @@ -561,9 +561,9 @@ @SafeVarargs private static <T> List<NamedFactory<T>> filter( - final Config cfg, final String key, final NamedFactory<T>... avail) { + final Config cfg, String key, NamedFactory<T>... avail) { final ArrayList<NamedFactory<T>> def = new ArrayList<>(); - for (final NamedFactory<T> n : avail) { + for (NamedFactory<T> n : avail) { if (n == null) { break; } @@ -576,7 +576,7 @@ } boolean didClear = false; - for (final String setting : want) { + for (String setting : want) { String name = setting.trim(); boolean add = true; if (name.startsWith("-")) { @@ -617,8 +617,8 @@ } @SafeVarargs - private static <T> NamedFactory<T> find(final String name, final NamedFactory<T>... avail) { - for (final NamedFactory<T> n : avail) { + private static <T> NamedFactory<T> find(String name, NamedFactory<T>... avail) { + for (NamedFactory<T> n : avail) { if (n != null && name.equals(n.getName())) { return n; }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheEntry.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheEntry.java index 2cab00b..1a5e137 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheEntry.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
@@ -22,7 +22,7 @@ private final AccountSshKey.Id id; private final PublicKey publicKey; - SshKeyCacheEntry(final AccountSshKey.Id i, final PublicKey k) { + SshKeyCacheEntry(AccountSshKey.Id i, PublicKey k) { id = i; publicKey = k; } @@ -31,7 +31,7 @@ return id.getParentKey(); } - boolean match(final PublicKey inkey) { + boolean match(PublicKey inkey) { return publicKey.equals(inkey); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java index 837865e..6a68211 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -14,18 +14,17 @@ package com.google.gerrit.sshd; -import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME; +import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.gerrit.reviewdb.client.AccountSshKey; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.account.ExternalId; import com.google.gerrit.server.account.VersionedAuthorizedKeys; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; import com.google.gerrit.server.cache.CacheModule; import com.google.gerrit.server.ssh.SshKeyCache; import com.google.gerrit.server.ssh.SshKeyCreator; -import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject; import com.google.inject.Module; import com.google.inject.Singleton; @@ -91,39 +90,33 @@ } static class Loader extends CacheLoader<String, Iterable<SshKeyCacheEntry>> { - private final SchemaFactory<ReviewDb> schema; + private final ExternalIds externalIds; private final VersionedAuthorizedKeys.Accessor authorizedKeys; @Inject - Loader(SchemaFactory<ReviewDb> schema, VersionedAuthorizedKeys.Accessor authorizedKeys) { - this.schema = schema; + Loader(ExternalIds externalIds, VersionedAuthorizedKeys.Accessor authorizedKeys) { + this.externalIds = externalIds; this.authorizedKeys = authorizedKeys; } @Override public Iterable<SshKeyCacheEntry> load(String username) throws Exception { - try (ReviewDb db = schema.open()) { - ExternalId user = - ExternalId.from( - db.accountExternalIds() - .get( - ExternalId.Key.create(SCHEME_USERNAME, username).asAccountExternalIdKey())); - if (user == null) { - return NO_SUCH_USER; - } - - List<SshKeyCacheEntry> kl = new ArrayList<>(4); - for (AccountSshKey k : authorizedKeys.getKeys(user.accountId())) { - if (k.isValid()) { - add(kl, k); - } - } - - if (kl.isEmpty()) { - return NO_KEYS; - } - return Collections.unmodifiableList(kl); + ExternalId user = externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, username)); + if (user == null) { + return NO_SUCH_USER; } + + List<SshKeyCacheEntry> kl = new ArrayList<>(4); + for (AccountSshKey k : authorizedKeys.getKeys(user.accountId())) { + if (k.isValid()) { + add(kl, k); + } + } + + if (kl.isEmpty()) { + return NO_KEYS; + } + return Collections.unmodifiableList(kl); } private void add(List<SshKeyCacheEntry> kl, AccountSshKey k) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java index dfd56f1..12064c8 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -89,7 +89,7 @@ audit(context.get(), "0", "LOGIN"); } - void onAuthFail(final SshSession sd) { + void onAuthFail(SshSession sd) { final LoggingEvent event = new LoggingEvent( // Logger.class.getName(), // fqnOfCategoryClass @@ -210,7 +210,7 @@ audit(context.get(), "0", "LOGOUT"); } - private LoggingEvent log(final String msg) { + private LoggingEvent log(String msg) { final SshSession sd = session.get(); final CurrentUser user = sd.getUser(); @@ -248,7 +248,7 @@ return event; } - private static String id(final int id) { + private static String id(int id) { return IdGenerator.format(id); }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java index 627bf71..442ec52 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLogLayout.java
@@ -69,7 +69,7 @@ return buf.toString(); } - private void formatDate(final long now, final StringBuffer sbuf) { + private void formatDate(long now, StringBuffer sbuf) { final int millis = (int) (now % 1000); final long rounded = now - millis; if (rounded != lastTimeMillis) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java index d25c58b..b911044 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -17,14 +17,15 @@ import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors; import static com.google.inject.Scopes.SINGLETON; +import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.lifecycle.LifecycleModule; +import com.google.gerrit.server.DynamicOptions; import com.google.gerrit.server.PeerDaemonUser; import com.google.gerrit.server.RemotePeer; import com.google.gerrit.server.config.GerritRequestModule; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.git.AsyncReceiveCommits; import com.google.gerrit.server.git.QueueProvider; -import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.plugins.ModuleGenerator; import com.google.gerrit.server.plugins.ReloadPluginListener; import com.google.gerrit.server.plugins.StartPluginListener; @@ -37,6 +38,7 @@ import java.net.SocketAddress; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ScheduledThreadPoolExecutor; import org.apache.sshd.server.CommandFactory; import org.apache.sshd.server.auth.gss.GSSAuthenticator; import org.apache.sshd.server.auth.pubkey.PublickeyAuthenticator; @@ -75,7 +77,7 @@ .toInstance(new DispatchCommandProvider(Commands.CMD_ROOT)); bind(CommandFactoryProvider.class); bind(CommandFactory.class).toProvider(CommandFactoryProvider.class); - bind(WorkQueue.Executor.class) + bind(ScheduledThreadPoolExecutor.class) .annotatedWith(StreamCommandExecutor.class) .toProvider(StreamCommandExecutorProvider.class) .in(SINGLETON); @@ -94,6 +96,8 @@ .annotatedWith(UniqueAnnotations.create()) .to(SshPluginStarterCallback.class); + DynamicMap.mapOf(binder(), DynamicOptions.DynamicBean.class); + listener().toInstance(registerInParentInjectors()); listener().to(SshLog.class); listener().to(SshDaemon.class); @@ -122,7 +126,7 @@ .toProvider(SshRemotePeerProvider.class) .in(SshScope.REQUEST); - bind(WorkQueue.Executor.class) + bind(ScheduledThreadPoolExecutor.class) .annotatedWith(CommandExecutor.class) .toProvider(CommandExecutorProvider.class) .in(SshScope.REQUEST);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java index 7f76ec6..9ae1814 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -14,6 +14,8 @@ package com.google.gerrit.sshd; +import com.google.gerrit.extensions.registration.DynamicMap; +import com.google.gerrit.server.DynamicOptions; import com.google.gerrit.server.plugins.Plugin; import com.google.gerrit.server.plugins.ReloadPluginListener; import com.google.gerrit.server.plugins.StartPluginListener; @@ -30,10 +32,14 @@ private static final Logger log = LoggerFactory.getLogger(SshPluginStarterCallback.class); private final DispatchCommandProvider root; + private final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans; @Inject - SshPluginStarterCallback(@CommandName(Commands.ROOT) DispatchCommandProvider root) { + SshPluginStarterCallback( + @CommandName(Commands.ROOT) DispatchCommandProvider root, + DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) { this.root = root; + this.dynamicBeans = dynamicBeans; } @Override @@ -58,10 +64,19 @@ try { return plugin.getSshInjector().getProvider(key); } catch (RuntimeException err) { - log.warn( - String.format("Plugin %s did not define its top-level command", plugin.getName()), err); + if (!providesDynamicOptions(plugin)) { + log.warn( + String.format( + "Plugin %s did not define its top-level command nor any DynamicOptions", + plugin.getName()), + err); + } } } return null; } + + private boolean providesDynamicOptions(Plugin plugin) { + return dynamicBeans.plugins().contains(plugin.getName()); + } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshRemotePeerProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshRemotePeerProvider.java index 44554ca..5e2626e 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshRemotePeerProvider.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshRemotePeerProvider.java
@@ -24,7 +24,7 @@ private final Provider<SshSession> session; @Inject - SshRemotePeerProvider(final Provider<SshSession> s) { + SshRemotePeerProvider(Provider<SshSession> s) { session = s; }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java index 202a80e..963a71a 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
@@ -51,7 +51,7 @@ volatile long started; volatile long finished; - private Context(SchemaFactory<ReviewDb> sf, final SshSession s, final String c, final long at) { + private Context(SchemaFactory<ReviewDb> sf, SshSession s, String c, long at) { schemaFactory = sf; session = s; commandLine = c; @@ -179,7 +179,7 @@ public static final Scope REQUEST = new Scope() { @Override - public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) { + public <T> Provider<T> scope(Key<T> key, Provider<T> creator) { return new Provider<T>() { @Override public T get() {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java index f08fb43..1a60a20 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
@@ -35,7 +35,7 @@ private volatile String authError; private volatile String peerAgent; - SshSession(final int sessionId, SocketAddress peer) { + SshSession(int sessionId, SocketAddress peer) { this.sessionId = sessionId; this.remoteAddress = peer; this.remoteAsString = format(remoteAddress); @@ -109,7 +109,7 @@ return authError != null; } - private static String format(final SocketAddress remote) { + private static String format(SocketAddress remote) { if (remote instanceof InetSocketAddress) { final InetSocketAddress sa = (InetSocketAddress) remote;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java index 33d253a..ab0ffcf 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
@@ -48,7 +48,7 @@ * @throws NoSuchAlgorithmException the JVM is missing the key algorithm. * @throws NoSuchProviderException the JVM is missing the provider. */ - public static PublicKey parse(final AccountSshKey key) + public static PublicKey parse(AccountSshKey key) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchProviderException { try { final String s = key.getEncodedKey(); @@ -69,7 +69,7 @@ * @return {@code keyStr} if conversion failed; otherwise the converted key, in OpenSSH key * format. */ - public static String toOpenSshPublicKey(final String keyStr) { + public static String toOpenSshPublicKey(String keyStr) { try { final StringBuilder strBuf = new StringBuilder(); final BufferedReader br = new BufferedReader(new StringReader(keyStr));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/StreamCommandExecutor.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/StreamCommandExecutor.java index 794ff76..9a8e029 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/StreamCommandExecutor.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/StreamCommandExecutor.java
@@ -16,11 +16,11 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; -import com.google.gerrit.server.git.WorkQueue.Executor; import com.google.inject.BindingAnnotation; import java.lang.annotation.Retention; +import java.util.concurrent.ScheduledThreadPoolExecutor; -/** Marker on {@link Executor} used by delayed event streaming. */ +/** Marker on {@link ScheduledThreadPoolExecutor} used by delayed event streaming. */ @Retention(RUNTIME) @BindingAnnotation public @interface StreamCommandExecutor {}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/StreamCommandExecutorProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/StreamCommandExecutorProvider.java index 58fd027..c3c6306 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/StreamCommandExecutorProvider.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/StreamCommandExecutorProvider.java
@@ -18,36 +18,22 @@ import com.google.gerrit.server.git.WorkQueue; import com.google.inject.Inject; import com.google.inject.Provider; -import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ScheduledThreadPoolExecutor; import org.eclipse.jgit.lib.Config; -class StreamCommandExecutorProvider implements Provider<WorkQueue.Executor> { +class StreamCommandExecutorProvider implements Provider<ScheduledThreadPoolExecutor> { private final int poolSize; private final WorkQueue queues; @Inject - StreamCommandExecutorProvider(@GerritServerConfig final Config config, final WorkQueue wq) { + StreamCommandExecutorProvider(@GerritServerConfig Config config, WorkQueue wq) { final int cores = Runtime.getRuntime().availableProcessors(); poolSize = config.getInt("sshd", "streamThreads", cores + 1); queues = wq; } @Override - public WorkQueue.Executor get() { - final WorkQueue.Executor executor; - - executor = queues.createQueue(poolSize, "SSH-Stream-Worker"); - - final ThreadFactory parent = executor.getThreadFactory(); - executor.setThreadFactory( - new ThreadFactory() { - @Override - public Thread newThread(final Runnable task) { - final Thread t = parent.newThread(task); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); - return executor; + public ScheduledThreadPoolExecutor get() { + return queues.createQueue(poolSize, "SSH-Stream-Worker", Thread.MIN_PRIORITY); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java index 53a98eb..54371c1 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -18,11 +18,15 @@ import com.google.common.base.Throwables; import com.google.common.util.concurrent.Atomics; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PeerDaemonUser; import com.google.gerrit.server.config.AuthConfig; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.sshd.SshScope.Context; import com.google.inject.Inject; import java.io.IOException; @@ -45,6 +49,7 @@ public final class SuExec extends BaseCommand { private final SshScope sshScope; private final DispatchCommandProvider dispatcher; + private final PermissionBackend permissionBackend; private boolean enableRunAs; private CurrentUser caller; @@ -67,6 +72,7 @@ SuExec( final SshScope sshScope, @CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher, + PermissionBackend permissionBackend, final CurrentUser caller, final SshSession session, final IdentifiedUser.GenericFactory userFactory, @@ -74,6 +80,7 @@ AuthConfig config) { this.sshScope = sshScope; this.dispatcher = dispatcher; + this.permissionBackend = permissionBackend; this.caller = caller; this.session = session; this.userFactory = userFactory; @@ -115,8 +122,14 @@ // OK. } else if (!enableRunAs) { throw die("suexec disabled by auth.enableRunAs = false"); - } else if (!caller.getCapabilities().canRunAs()) { - throw die("suexec not permitted"); + } else { + try { + permissionBackend.user(caller).check(GlobalPermission.RUN_AS); + } catch (AuthException e) { + throw die("suexec not permitted"); + } catch (PermissionBackendException e) { + throw die("suexec not available: " + e); + } } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java index 42c7578..ef1cd81 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
@@ -15,9 +15,12 @@ package com.google.gerrit.sshd.commands; import com.google.gerrit.common.data.GlobalCapability; -import com.google.gerrit.common.errors.PermissionDeniedException; import com.google.gerrit.extensions.annotations.RequiresCapability; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.sshd.AdminHighPriorityCommand; import com.google.gerrit.sshd.CommandMetaData; import com.google.gerrit.sshd.SshCommand; @@ -29,8 +32,8 @@ @RequiresCapability(GlobalCapability.ACCESS_DATABASE) @CommandMetaData(name = "gsql", description = "Administrative interface to active database") final class AdminQueryShell extends SshCommand { + @Inject private PermissionBackend permissionBackend; @Inject private QueryShell.Factory factory; - @Inject private IdentifiedUser currentUser; @Option(name = "--format", usage = "Set output format") @@ -42,9 +45,11 @@ @Override protected void run() throws Failure { try { - checkPermission(); - } catch (PermissionDeniedException err) { + permissionBackend.user(currentUser).check(GlobalPermission.ACCESS_DATABASE); + } catch (AuthException err) { throw die(err.getMessage()); + } catch (PermissionBackendException e) { + throw new Failure(1, "unavailable", e); } QueryShell shell = factory.create(in, out); @@ -55,22 +60,4 @@ shell.run(); } } - - /** - * Assert that the current user is permitted to perform raw queries. - * - * <p>As the @RequireCapability guards at various entry points of internal commands implicitly add - * administrators (which we want to avoid), we also check permissions within QueryShell and grant - * access only to those who can access the database, regardless of whether they are administrators - * or not. - * - * @throws PermissionDeniedException - */ - private void checkPermission() throws PermissionDeniedException { - if (!currentUser.getCapabilities().canAccessDatabase()) { - throw new PermissionDeniedException( - String.format( - "%s does not have \"Access Database\" capability.", currentUser.getUserName())); - } - } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java index 35ddc2a..35788fd 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -21,6 +21,7 @@ import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ListChildProjects; import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectControl; @@ -125,14 +126,18 @@ } final List<Project.NameKey> childProjects = new ArrayList<>(); - for (final ProjectControl pc : children) { + for (ProjectControl pc : children) { childProjects.add(pc.getProject().getNameKey()); } if (oldParent != null) { - childProjects.addAll(getChildrenForReparenting(oldParent)); + try { + childProjects.addAll(getChildrenForReparenting(oldParent)); + } catch (PermissionBackendException e) { + throw new Failure(1, "permissions unavailable", e); + } } - for (final Project.NameKey nameKey : childProjects) { + for (Project.NameKey nameKey : childProjects) { final String name = nameKey.get(); if (allProjectsName.equals(nameKey)) { @@ -185,17 +190,18 @@ * list of child projects does not contain projects that were specified to be excluded from * reparenting. */ - private List<Project.NameKey> getChildrenForReparenting(final ProjectControl parent) { + private List<Project.NameKey> getChildrenForReparenting(ProjectControl parent) + throws PermissionBackendException { final List<Project.NameKey> childProjects = new ArrayList<>(); final List<Project.NameKey> excluded = new ArrayList<>(excludedChildren.size()); - for (final ProjectControl excludedChild : excludedChildren) { + for (ProjectControl excludedChild : excludedChildren) { excluded.add(excludedChild.getProject().getNameKey()); } final List<Project.NameKey> automaticallyExcluded = new ArrayList<>(excludedChildren.size()); if (newParentKey != null) { automaticallyExcluded.addAll(getAllParents(newParentKey)); } - for (final ProjectInfo child : listChildProjects.apply(new ProjectResource(parent))) { + for (ProjectInfo child : listChildProjects.apply(new ProjectResource(parent))) { final Project.NameKey childName = new Project.NameKey(child.name); if (!excluded.contains(childName)) { if (!automaticallyExcluded.contains(childName)) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java index 92098cc..633eaa0 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java
@@ -34,7 +34,7 @@ private Short value; - ApproveOption(final String name, final String usage, final LabelType type) { + ApproveOption(String name, String usage, LabelType type) { this.name = name; this.usage = usage; this.type = type; @@ -100,7 +100,7 @@ } @Override - public void addValue(final Short val) { + public void addValue(Short val) { this.value = val; } @@ -122,13 +122,13 @@ private final ApproveOption cmdOption; // CS IGNORE RedundantModifier FOR NEXT 1 LINES. REASON: needed by org.kohsuke.args4j.Option - public Handler(final CmdLineParser parser, final OptionDef option, final Setter<Short> setter) { + public Handler(CmdLineParser parser, OptionDef option, Setter<Short> setter) { super(parser, option, setter); this.cmdOption = (ApproveOption) setter; } @Override - protected Short parse(final String token) throws NumberFormatException, CmdLineException { + protected Short parse(String token) throws NumberFormatException, CmdLineException { String argument = token; if (argument.startsWith("+")) { argument = argument.substring(1);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java index 78726a0..22d7e9a 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -18,7 +18,6 @@ import com.google.common.base.Joiner; import com.google.common.collect.Lists; -import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.server.project.BanCommit; import com.google.gerrit.server.project.BanCommit.BanResultInfo; import com.google.gerrit.server.project.ProjectControl; @@ -26,7 +25,6 @@ import com.google.gerrit.sshd.CommandMetaData; import com.google.gerrit.sshd.SshCommand; import com.google.inject.Inject; -import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.eclipse.jgit.lib.ObjectId; @@ -77,7 +75,7 @@ printCommits(r.newlyBanned, "The following commits were banned"); printCommits(r.alreadyBanned, "The following commits were already banned"); printCommits(r.ignored, "The following ids do not represent commits and were ignored"); - } catch (RestApiException | IOException e) { + } catch (Exception e) { throw die(e); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java index 22bca56..a742c35 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -37,6 +37,7 @@ import java.io.IOException; import java.util.HashSet; import java.util.Set; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; @@ -75,7 +76,7 @@ metaVar = "USERNAME", usage = "initial set of users to become members of the group" ) - void addMember(final Account.Id id) { + void addMember(Account.Id id) { initialMembers.add(id); } @@ -90,7 +91,7 @@ metaVar = "GROUP", usage = "initial set of groups to be included in the group" ) - void addGroup(final AccountGroup.UUID id) { + void addGroup(AccountGroup.UUID id) { initialGroups.add(id); } @@ -103,7 +104,7 @@ @Inject private AddIncludedGroups addIncludedGroups; @Override - protected void run() throws Failure, OrmException, IOException { + protected void run() throws Failure, OrmException, IOException, ConfigInvalidException { try { GroupResource rsrc = createGroup(); @@ -119,7 +120,8 @@ } } - private GroupResource createGroup() throws RestApiException, OrmException, IOException { + private GroupResource createGroup() + throws RestApiException, OrmException, IOException, ConfigInvalidException { GroupInput input = new GroupInput(); input.description = groupDescription; input.visibleToAll = visibleToAll; @@ -132,7 +134,8 @@ return groups.parse(TopLevelResource.INSTANCE, IdString.fromUrl(group.id)); } - private void addMembers(GroupResource rsrc) throws RestApiException, OrmException, IOException { + private void addMembers(GroupResource rsrc) + throws RestApiException, OrmException, IOException, ConfigInvalidException { AddMembers.Input input = AddMembers.Input.fromMembers( initialMembers.stream().map(Object::toString).collect(toList()));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java index 8b323dc..0df2a80 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -27,7 +27,7 @@ import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.server.project.NoSuchProjectException; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.SuggestParentCandidates; import com.google.gerrit.sshd.CommandMetaData; @@ -175,7 +175,7 @@ @Inject private SuggestParentCandidates suggestParentCandidates; @Override - protected void run() throws UnloggedFailure { + protected void run() throws Failure { try { if (!suggestParent) { if (projectName == null) { @@ -207,14 +207,14 @@ gApi.projects().create(input); } else { - List<Project.NameKey> parentCandidates = suggestParentCandidates.getNameKeys(); - - for (Project.NameKey parent : parentCandidates) { - stdout.print(parent + "\n"); + for (Project.NameKey parent : suggestParentCandidates.getNameKeys()) { + stdout.print(parent.get() + '\n'); } } - } catch (RestApiException | NoSuchProjectException err) { + } catch (RestApiException err) { throw die(err); + } catch (PermissionBackendException err) { + throw new Failure(1, "permissions unavailable", err); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java index 21bfe9b..392fd29 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -26,6 +26,7 @@ import com.google.gerrit.server.config.ListCaches; import com.google.gerrit.server.config.ListCaches.OutputFormat; import com.google.gerrit.server.config.PostCaches; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.sshd.CommandMetaData; import com.google.gerrit.sshd.SshCommand; import com.google.inject.Inject; @@ -81,6 +82,8 @@ } } catch (RestApiException e) { throw die(e.getMessage()); + } catch (PermissionBackendException e) { + throw new Failure(1, "unavailable", e); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java index fc65cf3..86209fe 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/IndexChangesCommand.java
@@ -14,7 +14,6 @@ package com.google.gerrit.sshd.commands; -import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.Index; @@ -23,7 +22,6 @@ import com.google.gerrit.sshd.SshCommand; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; -import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map; import org.kohsuke.args4j.Argument; @@ -57,7 +55,7 @@ for (ChangeResource rsrc : changes.values()) { try { index.apply(rsrc, new Index.Input()); - } catch (IOException | RestApiException | OrmException e) { + } catch (Exception e) { ok = false; writeError( "error", String.format("failed to index change %s: %s", rsrc.getId(), e.getMessage()));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java index 4ebc568..3465a9c 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -25,6 +25,7 @@ import com.google.gerrit.server.config.DeleteTask; import com.google.gerrit.server.config.TaskResource; import com.google.gerrit.server.config.TasksCollection; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.sshd.AdminHighPriorityCommand; import com.google.gerrit.sshd.SshCommand; import com.google.inject.Inject; @@ -50,7 +51,7 @@ try { TaskResource taskRsrc = tasksCollection.parse(cfgRsrc, IdString.fromDecoded(id)); deleteTask.apply(taskRsrc, null); - } catch (AuthException | ResourceNotFoundException e) { + } catch (AuthException | ResourceNotFoundException | PermissionBackendException e) { stderr.print("kill: " + id + ": No such task\n"); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java index 8357a91..c510680 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -91,9 +91,9 @@ groupBackend); } - void display(final PrintWriter out) throws OrmException, BadRequestException { + void display(PrintWriter out) throws OrmException, BadRequestException { final ColumnFormatter formatter = new ColumnFormatter(out, '\t'); - for (final GroupInfo info : get()) { + for (GroupInfo info : get()) { formatter.addColumn(MoreObjects.firstNonNull(info.name, "n/a")); if (verboseOutput) { AccountGroup o =
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java index 5b7f23b7..bbe736d 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -17,26 +17,25 @@ import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE; import static org.eclipse.jgit.lib.RefDatabase.ALL; -import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.extensions.annotations.RequiresCapability; import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountResolver; import com.google.gerrit.server.git.GitRepositoryManager; -import com.google.gerrit.server.git.SearchingChangeCacheImpl; -import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.VisibleRefFilter; -import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.project.ProjectControl; +import com.google.gerrit.server.util.ManualRequestContext; +import com.google.gerrit.server.util.OneOffRequestContext; import com.google.gerrit.sshd.CommandMetaData; import com.google.gerrit.sshd.SshCommand; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import java.io.IOException; import java.util.Map; +import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; @@ -50,16 +49,10 @@ ) public class LsUserRefs extends SshCommand { @Inject private AccountResolver accountResolver; - - @Inject private IdentifiedUser.GenericFactory userFactory; - + @Inject private OneOffRequestContext requestContext; + @Inject private VisibleRefFilter.Factory refFilterFactory; @Inject private ReviewDb db; - - @Inject private TagCache tagCache; - - @Inject private ChangeNotes.Factory changeNotesFactory; - - @Inject @Nullable private SearchingChangeCacheImpl changeCache; + @Inject private GitRepositoryManager repoManager; @Option( name = "--project", @@ -82,46 +75,41 @@ @Option(name = "--only-refs-heads", usage = "list only refs under refs/heads") private boolean onlyRefsHeads; - @Inject private GitRepositoryManager repoManager; - @Override protected void run() throws Failure { Account userAccount; try { userAccount = accountResolver.find(db, userName); - } catch (OrmException e) { + } catch (OrmException | IOException | ConfigInvalidException e) { throw die(e); } - if (userAccount == null) { stdout.print("No single user could be found when searching for: " + userName + '\n'); stdout.flush(); return; } - IdentifiedUser user = userFactory.create(userAccount.getId()); - ProjectControl userProjectControl = projectControl.forUser(user); - try (Repository repo = - repoManager.openRepository(userProjectControl.getProject().getNameKey())) { + Project.NameKey projectName = projectControl.getProject().getNameKey(); + try (Repository repo = repoManager.openRepository(projectName); + ManualRequestContext ctx = requestContext.openAs(userAccount.getId())) { try { Map<String, Ref> refsMap = - new VisibleRefFilter( - tagCache, changeNotesFactory, changeCache, repo, userProjectControl, db, true) + refFilterFactory + .create(projectControl.getProjectState(), repo) .filter(repo.getRefDatabase().getRefs(ALL), false); - for (final String ref : refsMap.keySet()) { + for (String ref : refsMap.keySet()) { if (!onlyRefsHeads || ref.startsWith(RefNames.REFS_HEADS)) { stdout.println(ref); } } } catch (IOException e) { - throw new Failure( - 1, "fatal: Error reading refs: '" + projectControl.getProject().getNameKey(), e); + throw new Failure(1, "fatal: Error reading refs: '" + projectName, e); } } catch (RepositoryNotFoundException e) { - throw die("'" + projectControl.getProject().getNameKey() + "': not a git archive"); - } catch (IOException e) { - throw die("Error opening: '" + projectControl.getProject().getNameKey()); + throw die("'" + projectName + "': not a git archive"); + } catch (IOException | OrmException e) { + throw die("Error opening: '" + projectName); } } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java index 1192eb5..2e5bf71 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -24,7 +24,7 @@ import org.kohsuke.args4j.Option; @CommandMetaData(name = "query", description = "Query the change database") -class Query extends SshCommand { +public class Query extends SshCommand { @Inject private OutputStreamQuery processor; @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java index 4201c2c..3f592be 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
@@ -272,7 +272,7 @@ Identity.create(rs, "COLUMN_NAME"), new Function("TYPE") { @Override - String apply(final ResultSet rs) throws SQLException { + String apply(ResultSet rs) throws SQLException { String type = rs.getString("TYPE_NAME"); switch (rs.getInt("DATA_TYPE")) { case java.sql.Types.CHAR: @@ -344,7 +344,7 @@ println(""); } - private void executeStatement(final String sql) { + private void executeStatement(String sql) { final long start = TimeUtil.nowMs(); final boolean hasResultSet; try { @@ -397,7 +397,7 @@ * @param show Functions to map columns * @throws SQLException */ - private void showResultSet(final ResultSet rs, boolean alreadyOnRow, long start, Function... show) + private void showResultSet(ResultSet rs, boolean alreadyOnRow, long start, Function... show) throws SQLException { switch (outputFormat) { case JSON_SINGLE: @@ -620,7 +620,7 @@ } } - private void warning(final String msg) { + private void warning(String msg) { switch (outputFormat) { case JSON_SINGLE: case JSON: @@ -639,7 +639,7 @@ } } - private void error(final SQLException err) { + private void error(SQLException err) { switch (outputFormat) { case JSON_SINGLE: case JSON: @@ -718,7 +718,7 @@ private abstract static class Function { final String name; - Function(final String name) { + Function(String name) { this.name = name; } @@ -726,19 +726,19 @@ } private static class Identity extends Function { - static Identity create(final ResultSet rs, final String name) throws SQLException { + static Identity create(ResultSet rs, String name) throws SQLException { return new Identity(rs.findColumn(name), name); } final int colId; - Identity(final int colId, final String name) { + Identity(int colId, String name) { super(name); this.colId = colId; } @Override - String apply(final ResultSet rs) throws SQLException { + String apply(ResultSet rs) throws SQLException { return rs.getString(colId); } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java index 7789c65..1fbcc17 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -61,7 +61,7 @@ metaVar = "EMAIL", usage = "request reviewer for change(s)" ) - void addReviewer(final Account.Id id) { + void addReviewer(Account.Id id) { reviewerId.add(id); } @@ -71,7 +71,7 @@ metaVar = "EMAIL", usage = "CC user on change(s)" ) - void addCC(final Account.Id id) { + void addCC(Account.Id id) { ccId.add(id); }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java index ca69b54..491455d 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -78,7 +78,7 @@ metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "list of commits or patch sets to review" ) - void addPatchSetId(final String token) { + void addPatchSetId(String token) { try { PatchSet ps = psParser.parsePatchSet(token, projectControl, branch); patchSets.add(ps); @@ -159,7 +159,7 @@ usage = "custom label(s) to assign", metaVar = "LABEL=VALUE" ) - void addLabel(final String token) { + void addLabel(String token) { LabelVote v = LabelVote.parseWithEquals(token); LabelType.checkName(v.label()); // Disallow SUBM. customLabels.put(v.label(), v.value()); @@ -256,7 +256,7 @@ input = reviewFromJson(); } - for (final PatchSet patchSet : patchSets) { + for (PatchSet patchSet : patchSets) { try { if (input != null) { applyReview(patchSet, input); @@ -281,7 +281,7 @@ } } - private void applyReview(PatchSet patchSet, final ReviewInput review) throws RestApiException { + private void applyReview(PatchSet patchSet, ReviewInput review) throws RestApiException { gApi.changes() .id(patchSet.getId().getParentKey().get()) .revision(patchSet.getRevision().get()) @@ -297,7 +297,7 @@ } } - private void reviewPatchSet(final PatchSet patchSet) throws Exception { + private void reviewPatchSet(PatchSet patchSet) throws Exception { if (notify == null) { notify = NotifyHandling.ALL; }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java index 1ed0bb0..1306c52 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -50,7 +50,7 @@ private IOException error; @Override - public void setArguments(final String[] args) { + public void setArguments(String[] args) { root = ""; for (int i = 0; i < args.length; i++) { if (args[i].charAt(0) == '-') { @@ -81,14 +81,8 @@ } @Override - public void start(final Environment env) { - startThread( - new Runnable() { - @Override - public void run() { - runImp(); - } - }); + public void start(Environment env) { + startThread(this::runImp); } private void runImp() { @@ -161,7 +155,7 @@ } } - private void readFile(final Entry ent) throws IOException { + private void readFile(Entry ent) throws IOException { byte[] data = ent.getBytes(); if (data == null) { throw new FileNotFoundException(ent.getPath()); @@ -175,7 +169,7 @@ readAck(); } - private void readDir(final Entry dir) throws IOException { + private void readDir(Entry dir) throws IOException { header(dir, 0); readAck(); @@ -192,8 +186,7 @@ readAck(); } - private void header(final Entry dir, final int len) - throws IOException, UnsupportedEncodingException { + private void header(Entry dir, int len) throws IOException, UnsupportedEncodingException { final StringBuilder buf = new StringBuilder(); switch (dir.getType()) { case DIR:
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java index 21591dd..033b4c6 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -42,6 +42,7 @@ import com.google.gerrit.server.account.PutHttpPassword; import com.google.gerrit.server.account.PutName; import com.google.gerrit.server.account.PutPreferred; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.sshd.CommandMetaData; import com.google.gerrit.sshd.SshCommand; import com.google.gwtorm.server.OrmException; @@ -174,7 +175,8 @@ } private void setAccount() - throws OrmException, IOException, UnloggedFailure, ConfigInvalidException { + throws OrmException, IOException, UnloggedFailure, ConfigInvalidException, + PermissionBackendException { user = genericUserFactory.create(id); rsrc = new AccountResource(user); try { @@ -227,8 +229,9 @@ } private void addSshKeys(List<String> sshKeys) - throws RestApiException, OrmException, IOException, ConfigInvalidException { - for (final String sshKey : sshKeys) { + throws RestApiException, OrmException, IOException, ConfigInvalidException, + PermissionBackendException { + for (String sshKey : sshKeys) { AddSshKey.Input in = new AddSshKey.Input(); in.raw = RawInputUtil.create(sshKey.getBytes(UTF_8), "plain/text"); addSshKey.apply(rsrc, in); @@ -237,7 +240,7 @@ private void deleteSshKeys(List<String> sshKeys) throws RestApiException, OrmException, RepositoryNotFoundException, IOException, - ConfigInvalidException { + ConfigInvalidException, PermissionBackendException { List<SshKeyInfo> infos = getSshKeys.apply(rsrc); if (sshKeys.contains("ALL")) { for (SshKeyInfo i : infos) { @@ -256,14 +259,15 @@ private void deleteSshKey(SshKeyInfo i) throws AuthException, OrmException, RepositoryNotFoundException, IOException, - ConfigInvalidException { + ConfigInvalidException, PermissionBackendException { AccountSshKey sshKey = new AccountSshKey(new AccountSshKey.Id(user.getAccountId(), i.seq), i.sshPublicKey); deleteSshKey.apply(new AccountResource.SshKey(user, sshKey), null); } private void addEmail(String email) - throws UnloggedFailure, RestApiException, OrmException, IOException, ConfigInvalidException { + throws UnloggedFailure, RestApiException, OrmException, IOException, ConfigInvalidException, + PermissionBackendException { EmailInput in = new EmailInput(); in.email = email; in.noConfirmation = true; @@ -275,7 +279,8 @@ } private void deleteEmail(String email) - throws RestApiException, OrmException, IOException, ConfigInvalidException { + throws RestApiException, OrmException, IOException, ConfigInvalidException, + PermissionBackendException { if (email.equals("ALL")) { List<EmailInfo> emails = getEmails.apply(rsrc); for (EmailInfo e : emails) { @@ -286,7 +291,9 @@ } } - private void putPreferred(String email) throws RestApiException, OrmException, IOException { + private void putPreferred(String email) + throws RestApiException, OrmException, IOException, PermissionBackendException, + ConfigInvalidException { for (EmailInfo e : getEmails.apply(rsrc)) { if (e.email.equals(email)) { putPreferred.apply(new AccountResource.Email(user, email), null); @@ -296,7 +303,7 @@ stderr.println("preferred email not found: " + email); } - private List<String> readSshKey(final List<String> sshKeys) + private List<String> readSshKey(List<String> sshKeys) throws UnsupportedEncodingException, IOException { if (!sshKeys.isEmpty()) { int idx = sshKeys.indexOf("-");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java index 20f65ad..c275af8 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -14,29 +14,23 @@ package com.google.gerrit.sshd.commands; +import com.google.common.base.Strings; +import com.google.gerrit.extensions.api.projects.ConfigInput; import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.client.ProjectState; import com.google.gerrit.extensions.client.SubmitType; -import com.google.gerrit.reviewdb.client.Project; -import com.google.gerrit.server.git.MetaDataUpdate; -import com.google.gerrit.server.git.ProjectConfig; -import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.server.project.ProjectControl; +import com.google.gerrit.server.project.ProjectResource; +import com.google.gerrit.server.project.PutConfig; import com.google.gerrit.sshd.CommandMetaData; import com.google.gerrit.sshd.SshCommand; import com.google.inject.Inject; -import java.io.IOException; -import org.eclipse.jgit.errors.ConfigInvalidException; -import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.Option; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @CommandMetaData(name = "set-project", description = "Change a project's settings") final class SetProjectCommand extends SshCommand { - private static final Logger log = LoggerFactory.getLogger(SetProjectCommand.class); - @Argument(index = 0, required = true, metaVar = "NAME", usage = "name of the project") private ProjectControl projectControl; @@ -144,64 +138,30 @@ @Option(name = "--max-object-size-limit", usage = "max Git object size for this project") private String maxObjectSizeLimit; - @Inject private MetaDataUpdate.User metaDataUpdateFactory; - - @Inject private ProjectCache projectCache; + @Inject private PutConfig putConfig; @Override protected void run() throws Failure { - if (!projectControl.isOwner()) { - throw new UnloggedFailure(1, "restricted to project owner"); + ConfigInput configInput = new ConfigInput(); + configInput.requireChangeId = requireChangeID; + configInput.submitType = submitType; + configInput.useContentMerge = contentMerge; + configInput.useContributorAgreements = contributorAgreements; + configInput.useSignedOffBy = signedOffBy; + configInput.state = state; + configInput.maxObjectSizeLimit = maxObjectSizeLimit; + // Description is different to other parameters, null won't result in + // keeping the existing description, it would delete it. + if (Strings.emptyToNull(projectDescription) != null) { + configInput.description = projectDescription; + } else { + configInput.description = projectControl.getProject().getDescription(); } - Project ctlProject = projectControl.getProject(); - Project.NameKey nameKey = ctlProject.getNameKey(); - String name = ctlProject.getName(); - final StringBuilder err = new StringBuilder(); - try (MetaDataUpdate md = metaDataUpdateFactory.create(nameKey)) { - ProjectConfig config = ProjectConfig.read(md); - Project project = config.getProject(); - - if (requireChangeID != null) { - project.setRequireChangeID(requireChangeID); - } - if (submitType != null) { - project.setSubmitType(submitType); - } - if (contentMerge != null) { - project.setUseContentMerge(contentMerge); - } - if (contributorAgreements != null) { - project.setUseContributorAgreements(contributorAgreements); - } - if (signedOffBy != null) { - project.setUseSignedOffBy(signedOffBy); - } - if (projectDescription != null) { - project.setDescription(projectDescription); - } - if (state != null) { - project.setState(state); - } - if (maxObjectSizeLimit != null) { - project.setMaxObjectSizeLimit(maxObjectSizeLimit); - } - md.setMessage("Project settings updated"); - config.commit(md); - } catch (RepositoryNotFoundException notFound) { - err.append("Project ").append(name).append(" not found\n"); - } catch (IOException | ConfigInvalidException e) { - final String msg = "Cannot update project " + name; - log.error(msg, e); - err.append("error: ").append(msg).append("\n"); - } - projectCache.evict(ctlProject); - - if (err.length() > 0) { - while (err.charAt(err.length() - 1) == '\n') { - err.setLength(err.length() - 1); - } - throw die(err.toString()); + try { + putConfig.apply(new ProjectResource(projectControl), configInput); + } catch (RestApiException e) { + throw die(e); } } }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java index e16f270..1ed7db3 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -23,6 +23,7 @@ import com.google.gerrit.common.Version; import com.google.gerrit.extensions.annotations.RequiresAnyCapability; import com.google.gerrit.extensions.events.LifecycleListener; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.config.ConfigResource; import com.google.gerrit.server.config.GetSummary; @@ -34,6 +35,9 @@ import com.google.gerrit.server.config.ListCaches; import com.google.gerrit.server.config.ListCaches.CacheInfo; import com.google.gerrit.server.config.ListCaches.CacheType; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.sshd.CommandMetaData; import com.google.gerrit.sshd.SshCommand; import com.google.gerrit.sshd.SshDaemon; @@ -80,12 +84,10 @@ private boolean showThreads; @Inject private SshDaemon daemon; - @Inject private ListCaches listCaches; - @Inject private GetSummary getSummary; - @Inject private CurrentUser self; + @Inject private PermissionBackend permissionBackend; @Option( name = "--width", @@ -168,7 +170,15 @@ printDiskCaches(caches); stdout.print('\n'); - if (self.getCapabilities().canMaintainServer()) { + boolean showJvm; + try { + permissionBackend.user(self).check(GlobalPermission.MAINTAIN_SERVER); + showJvm = true; + } catch (AuthException | PermissionBackendException e) { + // Silently ignore and do not display detailed JVM information. + showJvm = false; + } + if (showJvm) { sshSummary(); SummaryInfo summary = getSummary.setGc(gc).setJvm(showJVM).apply(new ConfigResource());
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java index 5dee105..a8452c8 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -75,7 +75,7 @@ private int columns = 80; @Override - public void start(final Environment env) throws IOException { + public void start(Environment env) throws IOException { String s = env.getEnv().get(Environment.ENV_COLUMNS); if (s != null && !s.isEmpty()) { try { @@ -122,7 +122,7 @@ String.format( "%-8s %8s %8s %-15s %s\n", "Session", "Start", "Idle", "User", "Remote Host")); stdout.print("--------------------------------------------------------------\n"); - for (final IoSession io : list) { + for (IoSession io : list) { checkState(io instanceof MinaSession, "expected MinaSession"); MinaSession minaSession = (MinaSession) io; long start = minaSession.getSession().getCreationTime(); @@ -142,7 +142,7 @@ } else { stdout.print(String.format("%-8s %-15s %s\n", "Session", "User", "Remote Host")); stdout.print("--------------------------------------------------------------\n"); - for (final IoSession io : list) { + for (IoSession io : list) { AbstractSession s = AbstractSession.getSession(io, true); SshSession sd = s != null ? s.getAttribute(SshSession.KEY) : null; @@ -169,11 +169,11 @@ } } - private static String id(final SshSession sd) { + private static String id(SshSession sd) { return sd != null ? IdGenerator.format(sd.getSessionId()) : ""; } - private static String time(final long now, final long time) { + private static String time(long now, long time) { if (time - now < 24 * 60 * 60 * 1000L) { return new SimpleDateFormat("HH:mm:ss").format(new Date(time)); } @@ -193,7 +193,7 @@ return String.format("%02d:%02d:%02d", hr, min, sec); } - private String username(final SshSession sd) { + private String username(SshSession sd) { if (sd == null) { return ""; } @@ -214,7 +214,7 @@ return ""; } - private String hostname(final SocketAddress remoteAddress) { + private String hostname(SocketAddress remoteAddress) { if (remoteAddress == null) { return "?"; }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java index 13db697..0296690 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -27,6 +27,9 @@ import com.google.gerrit.server.config.ListTasks.TaskInfo; import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.git.WorkQueue.Task; +import com.google.gerrit.server.permissions.GlobalPermission; +import com.google.gerrit.server.permissions.PermissionBackend; +import com.google.gerrit.server.permissions.PermissionBackendException; import com.google.gerrit.sshd.AdminHighPriorityCommand; import com.google.gerrit.sshd.CommandMetaData; import com.google.gerrit.sshd.SshCommand; @@ -35,6 +38,7 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; +import java.util.concurrent.ScheduledThreadPoolExecutor; import org.apache.sshd.server.Environment; import org.kohsuke.args4j.Option; @@ -60,10 +64,9 @@ ) private boolean groupByQueue; + @Inject private PermissionBackend permissionBackend; @Inject private ListTasks listTasks; - @Inject private IdentifiedUser currentUser; - @Inject private WorkQueue workQueue; private int columns = 80; @@ -83,7 +86,7 @@ } @Override - protected void run() throws UnloggedFailure { + protected void run() throws Failure { maxCommandWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 12 - 4 - 4; stdout.print( String.format( @@ -97,14 +100,16 @@ tasks = listTasks.apply(new ConfigResource()); } catch (AuthException e) { throw die(e); + } catch (PermissionBackendException e) { + throw new Failure(1, "permission backend unavailable", e); } - boolean viewAll = currentUser.getCapabilities().canViewQueue(); - long now = TimeUtil.nowMs(); + boolean viewAll = permissionBackend.user(currentUser).testOrFalse(GlobalPermission.VIEW_QUEUE); + long now = TimeUtil.nowMs(); if (groupByQueue) { ListMultimap<String, TaskInfo> byQueue = byQueue(tasks); for (String queueName : byQueue.keySet()) { - WorkQueue.Executor e = workQueue.getExecutor(queueName); + ScheduledThreadPoolExecutor e = workQueue.getExecutor(queueName); stdout.print(String.format("Queue: %s\n", queueName)); print(byQueue.get(queueName), now, viewAll, e.getCorePoolSize()); } @@ -176,7 +181,7 @@ return format(when, delay); } - private static String startTime(final Date when) { + private static String startTime(Date when) { return format(when, TimeUtil.nowMs() - when.getTime()); }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java index 1c0a424..91e0cb1 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
@@ -29,7 +29,6 @@ import com.google.gerrit.server.events.EventTypes; import com.google.gerrit.server.events.ProjectNameKeySerializer; import com.google.gerrit.server.events.SupplierSerializer; -import com.google.gerrit.server.git.WorkQueue; import com.google.gerrit.server.git.WorkQueue.CancelableRunnable; import com.google.gerrit.sshd.BaseCommand; import com.google.gerrit.sshd.CommandMetaData; @@ -43,6 +42,7 @@ import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledThreadPoolExecutor; import org.apache.sshd.server.Environment; import org.kohsuke.args4j.Option; import org.slf4j.Logger; @@ -71,7 +71,7 @@ @Inject private DynamicSet<UserScopedEventListener> eventListeners; - @Inject @StreamCommandExecutor private WorkQueue.Executor pool; + @Inject @StreamCommandExecutor private ScheduledThreadPoolExecutor pool; /** Queue of events to stream to the connected user. */ private final LinkedBlockingQueue<Event> queue = new LinkedBlockingQueue<>(MAX_EVENTS); @@ -131,7 +131,7 @@ private PrintWriter stdout; @Override - public void start(final Environment env) throws IOException { + public void start(Environment env) throws IOException { try { parseCommandLine(); } catch (UnloggedFailure e) { @@ -150,7 +150,7 @@ eventListeners.add( new UserScopedEventListener() { @Override - public void onEvent(final Event event) { + public void onEvent(Event event) { if (subscribedToEvents.isEmpty() || subscribedToEvents.contains(event.getType())) { offer(event); } @@ -170,7 +170,7 @@ } @Override - protected void onExit(final int rc) { + protected void onExit(int rc) { eventListenerRegistration.remove(); synchronized (taskLock) { @@ -199,7 +199,7 @@ } } - private void offer(final Event event) { + private void offer(Event event) { synchronized (taskLock) { if (!queue.offer(event)) { dropped = true; @@ -263,7 +263,7 @@ } } - private void write(final Object message) { + private void write(Object message) { String msg = null; try { msg = gson.toJson(message) + "\n";
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java index 67dfe96..fc3a917 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
@@ -15,16 +15,11 @@ package com.google.gerrit.sshd.commands; import com.google.common.collect.Lists; -import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.registration.DynamicSet; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.git.SearchingChangeCacheImpl; -import com.google.gerrit.server.git.TagCache; import com.google.gerrit.server.git.TransferConfig; import com.google.gerrit.server.git.VisibleRefFilter; import com.google.gerrit.server.git.validators.UploadValidationException; import com.google.gerrit.server.git.validators.UploadValidators; -import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.sshd.AbstractGitCommand; import com.google.gerrit.sshd.SshSession; import com.google.inject.Inject; @@ -38,22 +33,11 @@ /** Publishes Git repositories over SSH using the Git upload-pack protocol. */ final class Upload extends AbstractGitCommand { - @Inject private ReviewDb db; - @Inject private TransferConfig config; - - @Inject private TagCache tagCache; - - @Inject private ChangeNotes.Factory changeNotesFactory; - - @Inject @Nullable private SearchingChangeCacheImpl changeCache; - + @Inject private VisibleRefFilter.Factory refFilterFactory; @Inject private DynamicSet<PreUploadHook> preUploadHooks; - @Inject private DynamicSet<PostUploadHook> postUploadHooks; - @Inject private UploadValidators.Factory uploadValidatorsFactory; - @Inject private SshSession session; @Override @@ -63,9 +47,7 @@ } final UploadPack up = new UploadPack(repo); - up.setAdvertiseRefsHook( - new VisibleRefFilter( - tagCache, changeNotesFactory, changeCache, repo, projectControl, db, true)); + up.setAdvertiseRefsHook(refFilterFactory.create(projectControl.getProjectState(), repo)); up.setPackConfig(config.getPackConfig()); up.setTimeout(config.getTimeout()); up.setPostUploadHook(PostUploadHookChain.newChain(Lists.newArrayList(postUploadHooks)));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java index 4b8771a..093808c 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -17,9 +17,14 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.collect.ImmutableMap; +import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.change.AllowedFormats; import com.google.gerrit.server.change.ArchiveFormat; +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.sshd.AbstractGitCommand; import com.google.inject.Inject; import java.io.IOException; @@ -115,6 +120,8 @@ private List<String> path; } + @Inject private PermissionBackend permissionBackend; + @Inject private IdentifiedUser user; @Inject private AllowedFormats allowedFormats; @Inject private ReviewDb db; private Options options = new Options(); @@ -156,7 +163,7 @@ } @Override - protected void runImpl() throws IOException, Failure { + protected void runImpl() throws IOException, PermissionBackendException, Failure { PacketLineOut packetOut = new PacketLineOut(out); packetOut.setFlushOnEnd(true); packetOut.writeString("ACK"); @@ -177,8 +184,8 @@ throw new Failure(4, "fatal: reference not found"); } - // Verify the user has permissions to read the specified reference - if (!projectControl.allRefsAreVisible() && !canRead(treeId)) { + // Verify the user has permissions to read the specified tree. + if (!canRead(treeId)) { throw new Failure(5, "fatal: cannot perform upload-archive operation"); } @@ -235,10 +242,16 @@ return Collections.emptyMap(); } - private boolean canRead(ObjectId revId) throws IOException { - try (RevWalk rw = new RevWalk(repo)) { - RevCommit commit = rw.parseCommit(revId); - return projectControl.canReadCommit(db, repo, commit); + private boolean canRead(ObjectId revId) throws IOException, PermissionBackendException { + try { + permissionBackend.user(user).project(project.getNameKey()).check(ProjectPermission.READ); + return true; + } catch (AuthException e) { + // Check reachability of the specific revision. + try (RevWalk rw = new RevWalk(repo)) { + RevCommit commit = rw.parseCommit(revId); + return projectControl.canReadCommit(db, repo, commit); + } } } }
diff --git a/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/ContentEntrySubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/ContentEntrySubject.java new file mode 100644 index 0000000..9c9893c --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/ContentEntrySubject.java
@@ -0,0 +1,81 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.StringSubject; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; +import com.google.gerrit.extensions.common.DiffInfo.ContentEntry; +import com.google.gerrit.truth.ListSubject; + +public class ContentEntrySubject extends Subject<ContentEntrySubject, ContentEntry> { + + private static final SubjectFactory<ContentEntrySubject, ContentEntry> DIFF_INFO_SUBJECT_FACTORY = + new SubjectFactory<ContentEntrySubject, ContentEntry>() { + @Override + public ContentEntrySubject getSubject( + FailureStrategy failureStrategy, ContentEntry contentEntry) { + return new ContentEntrySubject(failureStrategy, contentEntry); + } + }; + + public static ContentEntrySubject assertThat(ContentEntry contentEntry) { + return assertAbout(DIFF_INFO_SUBJECT_FACTORY).that(contentEntry); + } + + private ContentEntrySubject(FailureStrategy failureStrategy, ContentEntry contentEntry) { + super(failureStrategy, contentEntry); + } + + public void isDueToRebase() { + isNotNull(); + ContentEntry contentEntry = actual(); + Truth.assertWithMessage("Entry should be marked 'dueToRebase'") + .that(contentEntry.dueToRebase) + .named("dueToRebase") + .isTrue(); + } + + public void isNotDueToRebase() { + isNotNull(); + ContentEntry contentEntry = actual(); + Truth.assertWithMessage("Entry should not be marked 'dueToRebase'") + .that(contentEntry.dueToRebase) + .named("dueToRebase") + .isNull(); + } + + public ListSubject<StringSubject, String> commonLines() { + isNotNull(); + ContentEntry contentEntry = actual(); + return ListSubject.assertThat(contentEntry.ab, Truth::assertThat).named("common lines"); + } + + public ListSubject<StringSubject, String> linesOfA() { + isNotNull(); + ContentEntry contentEntry = actual(); + return ListSubject.assertThat(contentEntry.a, Truth::assertThat).named("lines of 'a'"); + } + + public ListSubject<StringSubject, String> linesOfB() { + isNotNull(); + ContentEntry contentEntry = actual(); + return ListSubject.assertThat(contentEntry.b, Truth::assertThat).named("lines of 'b'"); + } +}
diff --git a/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/DiffInfoSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/DiffInfoSubject.java new file mode 100644 index 0000000..1b1b847 --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/DiffInfoSubject.java
@@ -0,0 +1,57 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.ComparableSubject; +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; +import com.google.gerrit.extensions.common.DiffInfo.ContentEntry; +import com.google.gerrit.truth.ListSubject; + +public class DiffInfoSubject extends Subject<DiffInfoSubject, DiffInfo> { + + private static final SubjectFactory<DiffInfoSubject, DiffInfo> DIFF_INFO_SUBJECT_FACTORY = + new SubjectFactory<DiffInfoSubject, DiffInfo>() { + @Override + public DiffInfoSubject getSubject(FailureStrategy failureStrategy, DiffInfo diffInfo) { + return new DiffInfoSubject(failureStrategy, diffInfo); + } + }; + + public static DiffInfoSubject assertThat(DiffInfo diffInfo) { + return assertAbout(DIFF_INFO_SUBJECT_FACTORY).that(diffInfo); + } + + private DiffInfoSubject(FailureStrategy failureStrategy, DiffInfo diffInfo) { + super(failureStrategy, diffInfo); + } + + public ListSubject<ContentEntrySubject, ContentEntry> content() { + isNotNull(); + DiffInfo diffInfo = actual(); + return ListSubject.assertThat(diffInfo.content, ContentEntrySubject::assertThat) + .named("content"); + } + + public ComparableSubject<?, ChangeType> changeType() { + isNotNull(); + DiffInfo diffInfo = actual(); + return Truth.assertThat(diffInfo.changeType).named("changeType"); + } +}
diff --git a/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/FileInfoSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/FileInfoSubject.java new file mode 100644 index 0000000..19ab556 --- /dev/null +++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/common/FileInfoSubject.java
@@ -0,0 +1,54 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 static com.google.common.truth.Truth.assertAbout; + +import com.google.common.truth.FailureStrategy; +import com.google.common.truth.IntegerSubject; +import com.google.common.truth.Subject; +import com.google.common.truth.SubjectFactory; +import com.google.common.truth.Truth; + +public class FileInfoSubject extends Subject<FileInfoSubject, FileInfo> { + + private static final SubjectFactory<FileInfoSubject, FileInfo> FILE_INFO_SUBJECT_FACTORY = + new SubjectFactory<FileInfoSubject, FileInfo>() { + @Override + public FileInfoSubject getSubject(FailureStrategy failureStrategy, FileInfo fileInfo) { + return new FileInfoSubject(failureStrategy, fileInfo); + } + }; + + public static FileInfoSubject assertThat(FileInfo fileInfo) { + return assertAbout(FILE_INFO_SUBJECT_FACTORY).that(fileInfo); + } + + private FileInfoSubject(FailureStrategy failureStrategy, FileInfo fileInfo) { + super(failureStrategy, fileInfo); + } + + public IntegerSubject linesInserted() { + isNotNull(); + FileInfo fileInfo = actual(); + return Truth.assertThat(fileInfo.linesInserted).named("linesInserted"); + } + + public IntegerSubject linesDeleted() { + isNotNull(); + FileInfo fileInfo = actual(); + return Truth.assertThat(fileInfo.linesDeleted).named("linesDeleted"); + } +}
diff --git a/gerrit-test-util/src/main/java/com/google/gerrit/extensions/restapi/BinaryResultSubject.java b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/restapi/BinaryResultSubject.java index 989ab0f..30ac496 100644 --- a/gerrit-test-util/src/main/java/com/google/gerrit/extensions/restapi/BinaryResultSubject.java +++ b/gerrit-test-util/src/main/java/com/google/gerrit/extensions/restapi/BinaryResultSubject.java
@@ -18,6 +18,7 @@ import com.google.common.truth.FailureStrategy; import com.google.common.truth.PrimitiveByteArraySubject; +import com.google.common.truth.StringSubject; import com.google.common.truth.Subject; import com.google.common.truth.SubjectFactory; import com.google.common.truth.Truth; @@ -51,6 +52,15 @@ super(failureStrategy, binaryResult); } + public StringSubject asString() throws IOException { + isNotNull(); + // We shouldn't close the BinaryResult within this method as it might still + // be used afterwards. Besides, closing it doesn't have an effect for most + // implementations of a BinaryResult. + BinaryResult binaryResult = actual(); + return Truth.assertThat(binaryResult.asString()); + } + public PrimitiveByteArraySubject bytes() throws IOException { isNotNull(); // We shouldn't close the BinaryResult within this method as it might still
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java index 11f380d..bb293cc 100644 --- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java +++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -44,6 +44,8 @@ import java.io.Writer; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -58,8 +60,10 @@ import org.kohsuke.args4j.spi.BooleanOptionHandler; import org.kohsuke.args4j.spi.EnumOptionHandler; import org.kohsuke.args4j.spi.FieldSetter; +import org.kohsuke.args4j.spi.MethodSetter; import org.kohsuke.args4j.spi.OptionHandler; import org.kohsuke.args4j.spi.Setter; +import org.kohsuke.args4j.spi.Setters; /** * Extended command line parser which handles --foo=value arguments. @@ -191,7 +195,7 @@ return parser.help.value; } - public void parseArgument(final String... args) throws CmdLineException { + public void parseArgument(String... args) throws CmdLineException { List<String> tmp = Lists.newArrayListWithCapacity(args.length); for (int argi = 0; argi < args.length; argi++) { final String str = args[argi]; @@ -228,7 +232,7 @@ public void parseOptionMap(ListMultimap<String, String> params) throws CmdLineException { List<String> tmp = Lists.newArrayListWithCapacity(2 * params.size()); - for (final String key : params.keySet()) { + for (String key : params.keySet()) { String name = makeOption(key); if (isBoolean(name)) { @@ -253,6 +257,10 @@ return findHandler(makeOption(name)) instanceof BooleanOptionHandler; } + public void parseWithPrefix(String prefix, Object bean) { + parser.parseWithPrefix(prefix, bean); + } + private String makeOption(String name) { if (!name.startsWith("-")) { if (name.length() == 1) { @@ -313,20 +321,103 @@ throw new CmdLineException(parser, String.format("invalid boolean \"%s=%s\"", name, value)); } + private static class PrefixedOption implements Option { + String prefix; + Option o; + + PrefixedOption(String prefix, Option o) { + this.prefix = prefix; + this.o = o; + } + + @Override + public String name() { + return getPrefixedName(prefix, o.name()); + } + + @Override + public String[] aliases() { + String[] prefixedAliases = new String[o.aliases().length]; + for (int i = 0; i < prefixedAliases.length; i++) { + prefixedAliases[i] = getPrefixedName(prefix, o.aliases()[i]); + } + return prefixedAliases; + } + + @Override + public String usage() { + return o.usage(); + } + + @Override + public String metaVar() { + return o.metaVar(); + } + + @Override + public boolean required() { + return o.required(); + } + + @Override + public boolean hidden() { + return o.hidden(); + } + + @SuppressWarnings("rawtypes") + @Override + public Class<? extends OptionHandler> handler() { + return o.handler(); + } + + @Override + public String[] depends() { + return o.depends(); + } + + @Override + public Class<? extends Annotation> annotationType() { + return o.annotationType(); + } + + private static String getPrefixedName(String prefix, String name) { + return "--" + prefix + name; + } + } + private class MyParser extends org.kohsuke.args4j.CmdLineParser { @SuppressWarnings("rawtypes") private List<OptionHandler> optionsList; private HelpOption help; - MyParser(final Object bean) { + MyParser(Object bean) { super(bean); ensureOptionsInitialized(); } + // NOTE: Argument annotations on bean are ignored. + public void parseWithPrefix(String prefix, Object bean) { + // recursively process all the methods/fields. + for (Class<?> c = bean.getClass(); c != null; c = c.getSuperclass()) { + for (Method m : c.getDeclaredMethods()) { + Option o = m.getAnnotation(Option.class); + if (o != null) { + addOption(new MethodSetter(this, bean, m), new PrefixedOption(prefix, o)); + } + } + for (Field f : c.getDeclaredFields()) { + Option o = f.getAnnotation(Option.class); + if (o != null) { + addOption(Setters.create(f, bean), new PrefixedOption(prefix, o)); + } + } + } + } + @SuppressWarnings({"unchecked", "rawtypes"}) @Override - protected OptionHandler createOptionHandler(final OptionDef option, final Setter setter) { + protected OptionHandler createOptionHandler(OptionDef option, Setter setter) { if (isHandlerSpecified(option) || isEnum(setter) || isPrimitive(setter)) { return add(super.createOptionHandler(option, setter)); } @@ -353,7 +444,7 @@ } } - private boolean isHandlerSpecified(final OptionDef option) { + private boolean isHandlerSpecified(OptionDef option) { return option.handler() != OptionHandler.class; }
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java index 582bee2..95d11a5 100644 --- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java +++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
@@ -25,7 +25,7 @@ public class OptionHandlerUtil { /** Generate a key for an {@link OptionHandlerFactory} in Guice. */ @SuppressWarnings("unchecked") - public static <T> Key<OptionHandlerFactory<T>> keyFor(final Class<T> valueType) { + public static <T> Key<OptionHandlerFactory<T>> keyFor(Class<T> valueType) { final Type factoryType = Types.newParameterizedType(OptionHandlerFactory.class, valueType); return (Key<OptionHandlerFactory<T>>) Key.get(factoryType); } @@ -36,7 +36,7 @@ return (Key<OptionHandler<T>>) Key.get(handlerType); } - public static <T> Module moduleFor(final Class<T> type, Class<? extends OptionHandler<T>> impl) { + public static <T> Module moduleFor(Class<T> type, Class<? extends OptionHandler<T>> impl) { return new FactoryModuleBuilder().implement(handlerOf(type), impl).build(keyFor(type)); }
diff --git a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java index cb8ed39..56734ff 100644 --- a/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java +++ b/gerrit-util-http/src/test/java/com/google/gerrit/util/http/testutil/FakeHttpServletRequest.java
@@ -19,11 +19,11 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.common.base.Splitter; +import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Maps; -import com.google.gerrit.extensions.restapi.Url; import java.io.BufferedReader; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -60,6 +60,7 @@ private final ListMultimap<String, String> headers; private ListMultimap<String, String> parameters; + private String queryString; private String hostName; private int port; private String contextPath; @@ -158,6 +159,7 @@ } public void setQueryString(String qs) { + this.queryString = qs; ListMultimap<String, String> params = LinkedListMultimap.create(); for (String entry : Splitter.on('&').split(qs)) { List<String> kv = Splitter.on('=').limit(2).splitToList(entry); @@ -306,7 +308,7 @@ @Override public String getQueryString() { - return paramsToString(parameters); + return queryString; } @Override @@ -317,8 +319,8 @@ @Override public String getRequestURI() { String uri = contextPath + servletPath + path; - if (!parameters.isEmpty()) { - uri += "?" + paramsToString(parameters); + if (!Strings.isNullOrEmpty(queryString)) { + uri += '?' + queryString; } return uri; } @@ -379,23 +381,6 @@ throw new UnsupportedOperationException(); } - private static String paramsToString(ListMultimap<String, String> params) { - StringBuilder sb = new StringBuilder(); - boolean first = true; - for (Map.Entry<String, String> e : params.entries()) { - if (!first) { - sb.append('&'); - } else { - first = false; - } - sb.append(Url.encode(e.getKey())); - if (!"".equals(e.getValue())) { - sb.append('=').append(Url.encode(e.getValue())); - } - } - return sb.toString(); - } - @Override public AsyncContext getAsyncContext() { throw new UnsupportedOperationException();
diff --git a/gerrit-util-ssl/src/main/java/com/google/gerrit/util/ssl/BlindSSLSocketFactory.java b/gerrit-util-ssl/src/main/java/com/google/gerrit/util/ssl/BlindSSLSocketFactory.java index 171e059..6dc1006 100644 --- a/gerrit-util-ssl/src/main/java/com/google/gerrit/util/ssl/BlindSSLSocketFactory.java +++ b/gerrit-util-ssl/src/main/java/com/google/gerrit/util/ssl/BlindSSLSocketFactory.java
@@ -63,7 +63,7 @@ private final SSLSocketFactory sslFactory; - private BlindSSLSocketFactory(final SSLSocketFactory sslFactory) { + private BlindSSLSocketFactory(SSLSocketFactory sslFactory) { this.sslFactory = sslFactory; }
diff --git a/gerrit-war/BUILD b/gerrit-war/BUILD index 66a0a47..865f940 100644 --- a/gerrit-war/BUILD +++ b/gerrit-war/BUILD
@@ -18,8 +18,8 @@ "//gerrit-pgm:init-api", "//gerrit-pgm:util", "//gerrit-reviewdb:server", + "//gerrit-server:prolog-common", "//gerrit-server:server", - "//gerrit-server/src/main/prolog:common", "//gerrit-sshd:sshd", "//lib:guava", "//lib:gwtorm",
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml index 2a0abdb..f3dec88c4 100644 --- a/gerrit-war/pom.xml +++ b/gerrit-war/pom.xml
@@ -2,7 +2,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.google.gerrit</groupId> <artifactId>gerrit-war</artifactId> - <version>2.14.1</version> + <version>2.15-SNAPSHOT</version> <packaging>war</packaging> <name>Gerrit Code Review - WAR</name> <description>Gerrit WAR</description>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java index 2693340..616030e 100644 --- a/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java +++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
@@ -54,7 +54,7 @@ } } - private void closeDataSource(final DataSource ds) { + private void closeDataSource(DataSource ds) { try { Class<?> type = Class.forName("org.apache.commons.dbcp.BasicDataSource"); if (type.isInstance(ds)) {
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java index 634abb9..7d003a1 100644 --- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java +++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -63,6 +63,7 @@ import com.google.gerrit.server.plugins.PluginGuiceEnvironment; import com.google.gerrit.server.plugins.PluginModule; import com.google.gerrit.server.plugins.PluginRestApiModule; +import com.google.gerrit.server.project.DefaultPermissionBackendModule; import com.google.gerrit.server.schema.DataSourceModule; import com.google.gerrit.server.schema.DataSourceProvider; import com.google.gerrit.server.schema.DataSourceType; @@ -318,6 +319,7 @@ modules.add(cfgInjector.getInstance(GerritGlobalModule.class)); modules.add(new SearchingChangeCacheImpl.Module()); modules.add(new InternalAccountDirectory.Module()); + modules.add(new DefaultPermissionBackendModule()); modules.add(new DefaultCacheFactory.Module()); modules.add(cfgInjector.getInstance(MailReceiver.Module.class)); modules.add(new SmtpEmailSender.Module());
diff --git a/lib/codemirror/cm.bzl b/lib/codemirror/cm.bzl index 54d60d5..bf10043 100644 --- a/lib/codemirror/cm.bzl +++ b/lib/codemirror/cm.bzl
@@ -214,7 +214,7 @@ "z80", ] -CM_VERSION = "5.25.0" +CM_VERSION = "5.27.2" TOP = "META-INF/resources/webjars/codemirror/%s" % CM_VERSION
diff --git a/lib/elasticsearch/BUILD b/lib/elasticsearch/BUILD index 8dc4bce..c40925e 100644 --- a/lib/elasticsearch/BUILD +++ b/lib/elasticsearch/BUILD
@@ -26,9 +26,6 @@ ], ) -# Java REST client for Elasticsearch. -VERSION = "0.1.7" - java_library( name = "jest-common", data = ["//lib:LICENSE-Apache2.0"],
diff --git a/lib/fonts/BUILD b/lib/fonts/BUILD index fb5ea84..57429f3 100644 --- a/lib/fonts/BUILD +++ b/lib/fonts/BUILD
@@ -1,13 +1,13 @@ load("//tools/bzl:genrule2.bzl", "genrule2") -# Source Code Pro. Version 2.010 Roman / 1.030 Italics -# https://github.com/adobe-fonts/source-code-pro/releases/tag/2.010R-ro%2F1.030R-it +# Roboto Mono. Version 2.136 +# https://github.com/google/roboto/releases/tag/v2.136 filegroup( - name = "sourcecodepro", + name = "robotomono", srcs = [ - "SourceCodePro-Regular.woff", - "SourceCodePro-Regular.woff2", + "RobotoMono-Regular.woff", + "RobotoMono-Regular.woff2", ], - data = ["//lib:LICENSE-OFL1.1"], + data = ["//lib:LICENSE-Apache2.0"], visibility = ["//visibility:public"], )
diff --git a/lib/fonts/RobotoMono-Regular.woff b/lib/fonts/RobotoMono-Regular.woff new file mode 100755 index 0000000..1ed8af5 --- /dev/null +++ b/lib/fonts/RobotoMono-Regular.woff Binary files differ
diff --git a/lib/fonts/RobotoMono-Regular.woff2 b/lib/fonts/RobotoMono-Regular.woff2 new file mode 100755 index 0000000..1142739 --- /dev/null +++ b/lib/fonts/RobotoMono-Regular.woff2 Binary files differ
diff --git a/lib/guava.bzl b/lib/guava.bzl index c71379e..768b99e 100644 --- a/lib/guava.bzl +++ b/lib/guava.bzl
@@ -1,5 +1,5 @@ -GUAVA_VERSION = "21.0" +GUAVA_VERSION = "22.0" -GUAVA_BIN_SHA1 = "3a3d111be1be1b745edfa7d91678a12d7ed38709" +GUAVA_BIN_SHA1 = "3564ef3803de51fb0530a8377ec6100b33b0d073" GUAVA_DOC_URL = "https://google.github.io/guava/releases/" + GUAVA_VERSION + "/api/docs/"
diff --git a/lib/highlightjs/building.md b/lib/highlightjs/building.md index 8cb9e8b..842efa9 100644 --- a/lib/highlightjs/building.md +++ b/lib/highlightjs/building.md
@@ -35,6 +35,7 @@ java \ javascript \ json \ + kotlin \ lisp \ lua \ markdown \
diff --git a/lib/highlightjs/highlight.min.js b/lib/highlightjs/highlight.min.js index cfc8c1c..60a005b 100644 --- a/lib/highlightjs/highlight.min.js +++ b/lib/highlightjs/highlight.min.js
@@ -1,105 +1,117 @@ -/*! highlight.js v9.5.0 | BSD3 License | git.io/hljslicense */ -(function(b){var p="object"===typeof window&&window||"object"===typeof self&&self;"undefined"!==typeof exports?b(exports):p&&(p.hljs=b({}),"function"===typeof define&&define.amd&&define([],function(){return p.hljs}))})(function(b){function p(a){return a.replace(/[&<>]/gm,function(a){return M[a]})}function C(a,c){var e=a&&a.exec(c);return e&&0===e.index}function v(a,c){var e,b={};for(e in a)b[e]=a[e];if(c)for(e in c)b[e]=c[e];return b}function H(a){var c=[];(function g(a,b){for(var k=a.firstChild;k;k= -k.nextSibling)3===k.nodeType?b+=k.nodeValue.length:1===k.nodeType&&(c.push({event:"start",offset:b,node:k}),b=g(k,b),k.nodeName.toLowerCase().match(/br|hr|img|input/)||c.push({event:"stop",offset:b,node:k}));return b})(a,0);return c}function N(a,c,e){function b(){return a.length&&c.length?a[0].offset!==c[0].offset?a[0].offset<c[0].offset?a:c:"start"===c[0].event?a:c:a.length?a:c}function d(a){n+="<"+a.nodeName.toLowerCase()+I.map.call(a.attributes,function(a){return" "+a.nodeName+'="'+p(a.value)+ -'"'}).join("")+">"}function f(a){n+="</"+a.nodeName.toLowerCase()+">"}function k(a){("start"===a.event?d:f)(a.node)}for(var l=0,n="",m=[];a.length||c.length;){var h=b(),n=n+p(e.substr(l,h[0].offset-l)),l=h[0].offset;if(h===a){m.reverse().forEach(f);do k(h.splice(0,1)[0]),h=b();while(h===a&&h.length&&h[0].offset===l);m.reverse().forEach(d)}else"start"===h[0].event?m.push(h[0].node):m.pop(),k(h.splice(0,1)[0])}return n+p(e.substr(l))}function O(a){function c(a){return a&&a.source||a}function e(e,b){return new RegExp(c(e), -"m"+(a.case_insensitive?"i":"")+(b?"g":""))}function b(d,f){if(!d.compiled){d.compiled=!0;d.keywords=d.keywords||d.beginKeywords;if(d.keywords){var k={},l=function(c,e){a.case_insensitive&&(e=e.toLowerCase());e.split(" ").forEach(function(a){a=a.split("|");k[a[0]]=[c,a[1]?Number(a[1]):1]})};"string"===typeof d.keywords?l("keyword",d.keywords):D(d.keywords).forEach(function(a){l(a,d.keywords[a])});d.keywords=k}d.lexemesRe=e(d.lexemes||/\w+/,!0);f&&(d.beginKeywords&&(d.begin="\\b("+d.beginKeywords.split(" ").join("|")+ -")\\b"),d.begin||(d.begin=/\B|\b/),d.beginRe=e(d.begin),d.end||d.endsWithParent||(d.end=/\B|\b/),d.end&&(d.endRe=e(d.end)),d.terminator_end=c(d.end)||"",d.endsWithParent&&f.terminator_end&&(d.terminator_end+=(d.end?"|":"")+f.terminator_end));d.illegal&&(d.illegalRe=e(d.illegal));null==d.relevance&&(d.relevance=1);d.contains||(d.contains=[]);var n=[];d.contains.forEach(function(a){a.variants?a.variants.forEach(function(c){n.push(v(a,c))}):n.push("self"===a?d:a)});d.contains=n;d.contains.forEach(function(a){b(a, -d)});d.starts&&b(d.starts,f);var m=d.contains.map(function(a){return a.beginKeywords?"\\.?("+a.begin+")\\.?":a.begin}).concat([d.terminator_end,d.illegal]).map(c).filter(Boolean);d.terminators=m.length?e(m.join("|"),!0):{exec:function(){return null}}}}b(a)}function A(a,c,e,b){function d(a,c){if(C(a.endRe,c)){for(;a.endsParent&&a.parent;)a=a.parent;return a}if(a.endsWithParent)return d(a.parent,c)}function f(a,c,e,b){return'<span class="'+(b?"":t.classPrefix)+(a+'">')+c+(e?"":"</span>")}function k(){var a= -r,c;if(null!=h.subLanguage)if((c="string"===typeof h.subLanguage)&&!w[h.subLanguage])c=p(q);else{var e=c?A(h.subLanguage,q,!0,u[h.subLanguage]):F(q,h.subLanguage.length?h.subLanguage:void 0);0<h.relevance&&(B+=e.relevance);c&&(u[h.subLanguage]=e.top);c=f(e.language,e.value,!1,!0)}else{var b;if(h.keywords){e="";b=0;h.lexemesRe.lastIndex=0;for(c=h.lexemesRe.exec(q);c;){e+=p(q.substr(b,c.index-b));b=h;var d=c,d=m.case_insensitive?d[0].toLowerCase():d[0];(b=b.keywords.hasOwnProperty(d)&&b.keywords[d])? -(B+=b[1],e+=f(b[0],p(c[0]))):e+=p(c[0]);b=h.lexemesRe.lastIndex;c=h.lexemesRe.exec(q)}c=e+p(q.substr(b))}else c=p(q)}r=a+c;q=""}function l(a){r+=a.className?f(a.className,"",!0):"";h=Object.create(a,{parent:{value:h}})}function n(a,c){q+=a;if(null==c)return k(),0;var b;a:{b=h;var f,g;f=0;for(g=b.contains.length;f<g;f++)if(C(b.contains[f].beginRe,c)){b=b.contains[f];break a}b=void 0}if(b)return b.skip?q+=c:(b.excludeBegin&&(q+=c),k(),b.returnBegin||b.excludeBegin||(q=c)),l(b,c),b.returnBegin?0:c.length; -if(b=d(h,c)){f=h;f.skip?q+=c:(f.returnEnd||f.excludeEnd||(q+=c),k(),f.excludeEnd&&(q=c));do h.className&&(r+="</span>"),h.skip||(B+=h.relevance),h=h.parent;while(h!==b.parent);b.starts&&l(b.starts,"");return f.returnEnd?0:c.length}if(!e&&C(h.illegalRe,c))throw Error('Illegal lexeme "'+c+'" for mode "'+(h.className||"<unnamed>")+'"');q+=c;return c.length||1}var m=x(a);if(!m)throw Error('Unknown language: "'+a+'"');O(m);var h=b||m,u={},r="";for(b=h;b!==m;b=b.parent)b.className&&(r=f(b.className,"", -!0)+r);var q="",B=0;try{for(var y,v,z=0;;){h.terminators.lastIndex=z;y=h.terminators.exec(c);if(!y)break;v=n(c.substr(z,y.index-z),y[0]);z=y.index+v}n(c.substr(z));for(b=h;b.parent;b=b.parent)b.className&&(r+="</span>");return{relevance:B,value:r,language:a,top:h}}catch(E){if(E.message&&-1!==E.message.indexOf("Illegal"))return{relevance:0,value:p(c)};throw E;}}function F(a,c){c=c||t.languages||D(w);var b={relevance:0,value:p(a)},g=b;c.filter(x).forEach(function(c){var f=A(c,a,!1);f.language=c;f.relevance> -g.relevance&&(g=f);f.relevance>b.relevance&&(g=b,b=f)});g.language&&(b.second_best=g);return b}function J(a){return t.tabReplace||t.useBR?a.replace(P,function(a,b){if(t.useBR&&"\n"===a)return"<br>";if(t.tabReplace)return b.replace(/\t/g,t.tabReplace)}):a}function K(a){var c,b,g,d,f;a:if(b=a.className+" ",b+=a.parentNode?a.parentNode.className:"",f=Q.exec(b))f=x(f[1])?f[1]:"no-highlight";else{b=b.split(/\s+/);f=0;for(d=b.length;f<d;f++)if(c=b[f],L.test(c)||x(c)){f=c;break a}f=void 0}L.test(f)||(t.useBR? -(c=document.createElementNS("http://www.w3.org/1999/xhtml","div"),c.innerHTML=a.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")):c=a,d=c.textContent,b=f?A(f,d,!0):F(d),c=H(c),c.length&&(g=document.createElementNS("http://www.w3.org/1999/xhtml","div"),g.innerHTML=b.value,b.value=N(c,H(g),d)),b.value=J(b.value),a.innerHTML=b.value,d=a.className,f=f?G[f]:b.language,c=[d.trim()],d.match(/\bhljs\b/)||c.push("hljs"),-1===d.indexOf(f)&&c.push(f),f=c.join(" ").trim(),a.className=f,a.result={language:b.language, -re:b.relevance},b.second_best&&(a.second_best={language:b.second_best.language,re:b.second_best.relevance}))}function u(){if(!u.called){u.called=!0;var a=document.querySelectorAll("pre code");I.forEach.call(a,K)}}function x(a){a=(a||"").toLowerCase();return w[a]||w[G[a]]}var I=[],D=Object.keys,w={},G={},L=/^(no-?highlight|plain|text)$/i,Q=/\blang(?:uage)?-([\w-]+)\b/i,P=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,t={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},M={"&":"&","<":"<",">":">"}; -b.highlight=A;b.highlightAuto=F;b.fixMarkup=J;b.highlightBlock=K;b.configure=function(a){t=v(t,a)};b.initHighlighting=u;b.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",u,!1);addEventListener("load",u,!1)};b.registerLanguage=function(a,c){var e=w[a]=c(b);e.aliases&&e.aliases.forEach(function(c){G[c]=a})};b.listLanguages=function(){return D(w)};b.getLanguage=x;b.inherit=v;b.IDENT_RE="[a-zA-Z]\\w*";b.UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*";b.NUMBER_RE="\\b\\d+(\\.\\d+)?";b.C_NUMBER_RE= -"(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";b.BINARY_NUMBER_RE="\\b(0b[01]+)";b.RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";b.BACKSLASH_ESCAPE={begin:"\\\\[\\s\\S]",relevance:0};b.APOS_STRING_MODE={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.QUOTE_STRING_MODE={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]}; -b.PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|like)\b/};b.COMMENT=function(a,c,e){a=b.inherit({className:"comment",begin:a,end:c,contains:[]},e||{});a.contains.push(b.PHRASAL_WORDS_MODE);a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|XXX):",relevance:0});return a};b.C_LINE_COMMENT_MODE=b.COMMENT("//","$");b.C_BLOCK_COMMENT_MODE=b.COMMENT("/\\*","\\*/");b.HASH_COMMENT_MODE=b.COMMENT("#", -"$");b.NUMBER_MODE={className:"number",begin:b.NUMBER_RE,relevance:0};b.C_NUMBER_MODE={className:"number",begin:b.C_NUMBER_RE,relevance:0};b.BINARY_NUMBER_MODE={className:"number",begin:b.BINARY_NUMBER_RE,relevance:0};b.CSS_NUMBER_MODE={className:"number",begin:b.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0};b.REGEXP_MODE={className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[b.BACKSLASH_ESCAPE,{begin:/\[/, -end:/\]/,relevance:0,contains:[b.BACKSLASH_ESCAPE]}]};b.TITLE_MODE={className:"title",begin:b.IDENT_RE,relevance:0};b.UNDERSCORE_TITLE_MODE={className:"title",begin:b.UNDERSCORE_IDENT_RE,relevance:0};b.METHOD_GUARD={begin:"\\.\\s*"+b.UNDERSCORE_IDENT_RE,relevance:0};b.registerLanguage("bash",function(a){var c={className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)}/}]},b={className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,c,{className:"variable",begin:/\$\(/, -end:/\)/,contains:[a.BACKSLASH_ESCAPE]}]};return{aliases:["sh","zsh"],lexemes:/-?[a-z\.]+/,keywords:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp", -_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[{className:"meta",begin:/^#![^\n]+sh\s*$/,relevance:10},{className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0},a.HASH_COMMENT_MODE,b,{className:"string",begin:/'/,end:/'/},c]}});b.registerLanguage("clojure",function(a){var c={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",relevance:0},b=a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),g=a.COMMENT(";","$",{relevance:0}), -d={className:"literal",begin:/\b(true|false|nil)\b/},f={begin:"[\\[\\{]",end:"[\\]\\}]"},k={className:"comment",begin:"\\^[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},l=a.COMMENT("\\^\\{","\\}"),n={className:"symbol",begin:"[:]{1,2}[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},m={begin:"\\(",end:"\\)"},h={endsWithParent:!0,relevance:0},p={keywords:{"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"}, -lexemes:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",className:"name",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",starts:h},r=[m,b,k,l,g,n,f,c,d,{begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0}];m.contains=[a.COMMENT("comment",""),p,h];h.contains=r;f.contains=r;return{aliases:["clj"],illegal:/\S/,contains:[m,b,k,l,g,n,f,c,d]}});b.registerLanguage("cpp",function(a){var c={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},b={className:"string",variants:[{begin:'(u8?|U)?L?"', -end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'(u8?|U)?R"',end:'"',contains:[a.BACKSLASH_ESCAPE]},{begin:"'\\\\?.",end:"'",illegal:"."}]},g={className:"number",variants:[{begin:"\\b(0b[01'_]+)"},{begin:"\\b([\\d'_]+(\\.[\\d'_]*)?|\\.[\\d'_]+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9'_]+|(\\b[\\d'_]+(\\.[\\d'_]*)?|\\.[\\d'_]+)([eE][-+]?[\\d'_]+)?)"}],relevance:0},d={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"}, -contains:[{begin:/\\\n/,relevance:0},a.inherit(b,{className:"meta-string"}),{className:"meta-string",begin:"<",end:">",illegal:"\\n"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},f=a.IDENT_RE+"\\s*\\(",k={keyword:"int float while private char catch export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const struct for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using class asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return", +/* + highlight.js v9.12.0 | BSD3 License | git.io/hljslicense */ +var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(b,g,l){b!=Array.prototype&&b!=Object.prototype&&(b[g]=l.value)};$jscomp.getGlobal=function(b){return"undefined"!=typeof window&&window===b?b:"undefined"!=typeof global&&null!=global?global:b};$jscomp.global=$jscomp.getGlobal(this);$jscomp.SYMBOL_PREFIX="jscomp_symbol_"; +$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){};$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)};$jscomp.symbolCounter_=0;$jscomp.Symbol=function(b){return $jscomp.SYMBOL_PREFIX+(b||"")+$jscomp.symbolCounter_++}; +$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var b=$jscomp.global.Symbol.iterator;b||(b=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));"function"!=typeof Array.prototype[b]&&$jscomp.defineProperty(Array.prototype,b,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}});$jscomp.initSymbolIterator=function(){}};$jscomp.arrayIterator=function(b){var g=0;return $jscomp.iteratorPrototype(function(){return g<b.length?{done:!1,value:b[g++]}:{done:!0}})}; +$jscomp.iteratorPrototype=function(b){$jscomp.initSymbolIterator();b={next:b};b[$jscomp.global.Symbol.iterator]=function(){return this};return b};$jscomp.iteratorFromArray=function(b,g){$jscomp.initSymbolIterator();b instanceof String&&(b+="");var l=0,k={next:function(){if(l<b.length){var m=l++;return{value:g(m,b[m]),done:!1}}k.next=function(){return{done:!0,value:void 0}};return k.next()}};k[Symbol.iterator]=function(){return k};return k}; +$jscomp.polyfill=function(b,g,l,k){if(g){l=$jscomp.global;b=b.split(".");for(k=0;k<b.length-1;k++){var m=b[k];m in l||(l[m]={});l=l[m]}b=b[b.length-1];k=l[b];g=g(k);g!=k&&null!=g&&$jscomp.defineProperty(l,b,{configurable:!0,writable:!0,value:g})}};$jscomp.polyfill("Array.prototype.keys",function(b){return b?b:function(){return $jscomp.iteratorFromArray(this,function(b){return b})}},"es6-impl","es3"); +(function(b){var g="object"===typeof window&&window||"object"===typeof self&&self;"undefined"!==typeof exports?b(exports):g&&(g.hljs=b({}),"function"===typeof define&&define.amd&&define([],function(){return g.hljs}))})(function(b){function g(a){return a.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}function l(a,f){return(a=a&&a.exec(f))&&0===a.index}function k(a){var f,b={},e=Array.prototype.slice.call(arguments,1);for(f in a)b[f]=a[f];e.forEach(function(a){for(f in a)b[f]=a[f]}); +return b}function m(a){var f=[];(function e(a,b){for(a=a.firstChild;a;a=a.nextSibling)3===a.nodeType?b+=a.nodeValue.length:1===a.nodeType&&(f.push({event:"start",offset:b,node:a}),b=e(a,b),a.nodeName.toLowerCase().match(/br|hr|img|input/)||f.push({event:"stop",offset:b,node:a}));return b})(a,0);return f}function L(a,f,b){function d(){return a.length&&f.length?a[0].offset!==f[0].offset?a[0].offset<f[0].offset?a:f:"start"===f[0].event?a:f:a.length?a:f}function c(a){z+="<"+a.nodeName.toLowerCase()+H.map.call(a.attributes, +function(a){return" "+a.nodeName+'="'+g(a.value).replace('"',""")+'"'}).join("")+">"}function q(a){z+="</"+a.nodeName.toLowerCase()+">"}function w(a){("start"===a.event?c:q)(a.node)}for(var r=0,z="",n=[];a.length||f.length;){var h=d(),z=z+g(b.substring(r,h[0].offset)),r=h[0].offset;if(h===a){n.reverse().forEach(q);do w(h.splice(0,1)[0]),h=d();while(h===a&&h.length&&h[0].offset===r);n.reverse().forEach(c)}else"start"===h[0].event?n.push(h[0].node):n.pop(),w(h.splice(0,1)[0])}return z+g(b.substr(r))} +function M(a){a.variants&&!a.cached_variants&&(a.cached_variants=a.variants.map(function(f){return k(a,{variants:null},f)}));return a.cached_variants||a.endsWithParent&&[k(a)]||[a]}function N(a){function f(a){return a&&a.source||a}function b(b,d){return new RegExp(f(b),"m"+(a.case_insensitive?"i":"")+(d?"g":""))}function e(c,d){if(!c.compiled){c.compiled=!0;c.keywords=c.keywords||c.beginKeywords;if(c.keywords){var q={},g=function(b,f){a.case_insensitive&&(f=f.toLowerCase());f.split(" ").forEach(function(a){a= +a.split("|");q[a[0]]=[b,a[1]?Number(a[1]):1]})};"string"===typeof c.keywords?g("keyword",c.keywords):x(c.keywords).forEach(function(a){g(a,c.keywords[a])});c.keywords=q}c.lexemesRe=b(c.lexemes||/\w+/,!0);d&&(c.beginKeywords&&(c.begin="\\b("+c.beginKeywords.split(" ").join("|")+")\\b"),c.begin||(c.begin=/\B|\b/),c.beginRe=b(c.begin),c.end||c.endsWithParent||(c.end=/\B|\b/),c.end&&(c.endRe=b(c.end)),c.terminator_end=f(c.end)||"",c.endsWithParent&&d.terminator_end&&(c.terminator_end+=(c.end?"|":"")+ +d.terminator_end));c.illegal&&(c.illegalRe=b(c.illegal));null==c.relevance&&(c.relevance=1);c.contains||(c.contains=[]);c.contains=Array.prototype.concat.apply([],c.contains.map(function(a){return M("self"===a?c:a)}));c.contains.forEach(function(a){e(a,c)});c.starts&&e(c.starts,d);d=c.contains.map(function(a){return a.beginKeywords?"\\.?("+a.begin+")\\.?":a.begin}).concat([c.terminator_end,c.illegal]).map(f).filter(Boolean);c.terminators=d.length?b(d.join("|"),!0):{exec:function(){return null}}}} +e(a)}function C(a,f,b,e){function c(a,b){if(l(a.endRe,b)){for(;a.endsParent&&a.parent;)a=a.parent;return a}if(a.endsWithParent)return c(a.parent,b)}function d(a,b,f,d){return'<span class="'+(d?"":t.classPrefix)+(a+'">')+b+(f?"":"</span>")}function w(){var a=v,b;if(null!=h.subLanguage)if((b="string"===typeof h.subLanguage)&&!y[h.subLanguage])b=g(p);else{var f=b?C(h.subLanguage,p,!0,m[h.subLanguage]):F(p,h.subLanguage.length?h.subLanguage:void 0);0<h.relevance&&(u+=f.relevance);b&&(m[h.subLanguage]= +f.top);b=d(f.language,f.value,!1,!0)}else if(h.keywords){f="";var c=0;h.lexemesRe.lastIndex=0;for(b=h.lexemesRe.exec(p);b;){f+=g(p.substring(c,b.index));c=h;var e=b,e=n.case_insensitive?e[0].toLowerCase():e[0];(c=c.keywords.hasOwnProperty(e)&&c.keywords[e])?(u+=c[1],f+=d(c[0],g(b[0]))):f+=g(b[0]);c=h.lexemesRe.lastIndex;b=h.lexemesRe.exec(p)}b=f+g(p.substr(c))}else b=g(p);v=a+b;p=""}function r(a){v+=a.className?d(a.className,"",!0):"";h=Object.create(a,{parent:{value:h}})}function k(a,f){p+=a;if(null== +f)return w(),0;a:{a=h;var d;var e=0;for(d=a.contains.length;e<d;e++)if(l(a.contains[e].beginRe,f)){a=a.contains[e];break a}a=void 0}if(a)return a.skip?p+=f:(a.excludeBegin&&(p+=f),w(),a.returnBegin||a.excludeBegin||(p=f)),r(a,f),a.returnBegin?0:f.length;if(a=c(h,f)){e=h;e.skip?p+=f:(e.returnEnd||e.excludeEnd||(p+=f),w(),e.excludeEnd&&(p=f));do h.className&&(v+="</span>"),h.skip||(u+=h.relevance),h=h.parent;while(h!==a.parent);a.starts&&r(a.starts,"");return e.returnEnd?0:f.length}if(!b&&l(h.illegalRe, +f))throw Error('Illegal lexeme "'+f+'" for mode "'+(h.className||"<unnamed>")+'"');p+=f;return f.length||1}var n=A(a);if(!n)throw Error('Unknown language: "'+a+'"');N(n);var h=e||n,m={},v="";for(e=h;e!==n;e=e.parent)e.className&&(v=d(e.className,"",!0)+v);var p="",u=0;try{for(var B,x,D=0;;){h.terminators.lastIndex=D;B=h.terminators.exec(f);if(!B)break;x=k(f.substring(D,B.index),B[0]);D=B.index+x}k(f.substr(D));for(e=h;e.parent;e=e.parent)e.className&&(v+="</span>");return{relevance:u,value:v,language:a, +top:h}}catch(E){if(E.message&&-1!==E.message.indexOf("Illegal"))return{relevance:0,value:g(f)};throw E;}}function F(a,f){f=f||t.languages||x(y);var b={relevance:0,value:g(a)},e=b;f.filter(A).forEach(function(f){var c=C(f,a,!1);c.language=f;c.relevance>e.relevance&&(e=c);c.relevance>b.relevance&&(e=b,b=c)});e.language&&(b.second_best=e);return b}function I(a){return t.tabReplace||t.useBR?a.replace(O,function(a,b){return t.useBR&&"\n"===a?"<br>":t.tabReplace?b.replace(/\t/g,t.tabReplace):""}):a}function J(a){var b, +d;a:{var e=a.className+" ";e+=a.parentNode?a.parentNode.className:"";if(d=P.exec(e))d=A(d[1])?d[1]:"no-highlight";else{e=e.split(/\s+/);d=0;for(b=e.length;d<b;d++){var c=e[d];if(K.test(c)||A(c)){d=c;break a}}d=void 0}}if(!K.test(d)){t.useBR?(c=document.createElementNS("http://www.w3.org/1999/xhtml","div"),c.innerHTML=a.innerHTML.replace(/\n/g,"").replace(/<br[ \/]*>/g,"\n")):c=a;b=c.textContent;e=d?C(d,b,!0):F(b);c=m(c);if(c.length){var q=document.createElementNS("http://www.w3.org/1999/xhtml","div"); +q.innerHTML=e.value;e.value=L(c,m(q),b)}e.value=I(e.value);a.innerHTML=e.value;b=a.className;d=d?G[d]:e.language;c=[b.trim()];b.match(/\bhljs\b/)||c.push("hljs");-1===b.indexOf(d)&&c.push(d);d=c.join(" ").trim();a.className=d;a.result={language:e.language,re:e.relevance};e.second_best&&(a.second_best={language:e.second_best.language,re:e.second_best.relevance})}}function u(){if(!u.called){u.called=!0;var a=document.querySelectorAll("pre code");H.forEach.call(a,J)}}function A(a){a=(a||"").toLowerCase(); +return y[a]||y[G[a]]}var H=[],x=Object.keys,y={},G={},K=/^(no-?highlight|plain|text)$/i,P=/\blang(?:uage)?-([\w-]+)\b/i,O=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,t={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0};b.highlight=C;b.highlightAuto=F;b.fixMarkup=I;b.highlightBlock=J;b.configure=function(a){t=k(t,a)};b.initHighlighting=u;b.initHighlightingOnLoad=function(){addEventListener("DOMContentLoaded",u,!1);addEventListener("load",u,!1)};b.registerLanguage=function(a,f){f=y[a]=f(b);f.aliases&& +f.aliases.forEach(function(b){G[b]=a})};b.listLanguages=function(){return x(y)};b.getLanguage=A;b.inherit=k;b.IDENT_RE="[a-zA-Z]\\w*";b.UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*";b.NUMBER_RE="\\b\\d+(\\.\\d+)?";b.C_NUMBER_RE="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)";b.BINARY_NUMBER_RE="\\b(0b[01]+)";b.RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~";b.BACKSLASH_ESCAPE= +{begin:"\\\\[\\s\\S]",relevance:0};b.APOS_STRING_MODE={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.QUOTE_STRING_MODE={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[b.BACKSLASH_ESCAPE]};b.PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/};b.COMMENT=function(a,f,d){a=b.inherit({className:"comment",begin:a,end:f,contains:[]},d||{}); +a.contains.push(b.PHRASAL_WORDS_MODE);a.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|XXX):",relevance:0});return a};b.C_LINE_COMMENT_MODE=b.COMMENT("//","$");b.C_BLOCK_COMMENT_MODE=b.COMMENT("/\\*","\\*/");b.HASH_COMMENT_MODE=b.COMMENT("#","$");b.NUMBER_MODE={className:"number",begin:b.NUMBER_RE,relevance:0};b.C_NUMBER_MODE={className:"number",begin:b.C_NUMBER_RE,relevance:0};b.BINARY_NUMBER_MODE={className:"number",begin:b.BINARY_NUMBER_RE,relevance:0};b.CSS_NUMBER_MODE={className:"number", +begin:b.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0};b.REGEXP_MODE={className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[b.BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0,contains:[b.BACKSLASH_ESCAPE]}]};b.TITLE_MODE={className:"title",begin:b.IDENT_RE,relevance:0};b.UNDERSCORE_TITLE_MODE={className:"title",begin:b.UNDERSCORE_IDENT_RE,relevance:0};b.METHOD_GUARD={begin:"\\.\\s*"+b.UNDERSCORE_IDENT_RE,relevance:0}; +b.registerLanguage("bash",function(a){var b={className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)}/}]},d={className:"string",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE,b,{className:"variable",begin:/\$\(/,end:/\)/,contains:[a.BACKSLASH_ESCAPE]}]};return{aliases:["sh","zsh"],lexemes:/\b-?[a-z\._]+\b/,keywords:{keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp", +_:"-ne -eq -lt -gt -f -d -e -s -l -a"},contains:[{className:"meta",begin:/^#![^\n]+sh\s*$/,relevance:10},{className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0},a.HASH_COMMENT_MODE,d,{className:"string",begin:/'/,end:/'/},b]}});b.registerLanguage("clojure",function(a){var b={className:"number",begin:"[-+]?\\d+(\\.\\d+)?",relevance:0},d=a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),e=a.COMMENT(";","$",{relevance:0}), +c={className:"literal",begin:/\b(true|false|nil)\b/},q={begin:"[\\[\\{]",end:"[\\]\\}]"},g={className:"comment",begin:"\\^[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},r=a.COMMENT("\\^\\{","\\}"),k={className:"symbol",begin:"[:]{1,2}[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*"},n={begin:"\\(",end:"\\)"},h={endsWithParent:!0,relevance:0},l={keywords:{"builtin-name":"def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last drop-while while intern condp case reduced cycle split-at split-with repeat replicate iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or when when-not when-let comp juxt partial sequence memoize constantly complement identity assert peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize"}, +lexemes:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",className:"name",begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",starts:h},m=[n,d,g,r,e,k,q,b,c,{begin:"[a-zA-Z_\\-!.?+*=<>&#'][a-zA-Z_\\-!.?+*=<>&#'0-9/;:]*",relevance:0}];n.contains=[a.COMMENT("comment",""),l,h];h.contains=m;q.contains=m;r.contains=[q];return{aliases:["clj"],illegal:/\S/,contains:[n,d,g,r,e,k,q,b,c]}});b.registerLanguage("cpp",function(a){var b={className:"keyword",begin:"\\b[a-z\\d_]*_t\\b"},d={className:"string", +variants:[{begin:'(u8?|U)?L?"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'(u8?|U)?R"',end:'"',contains:[a.BACKSLASH_ESCAPE]},{begin:"'\\\\?.",end:"'",illegal:"."}]},e={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},c={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{"meta-keyword":"if else elif endif define undef warning error line pragma ifdef ifndef include"}, +contains:[{begin:/\\\n/,relevance:0},a.inherit(d,{className:"meta-string"}),{className:"meta-string",begin:/<[^\n>]*>/,end:/$/,illegal:"\\n"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},q=a.IDENT_RE+"\\s*\\(",g={keyword:"int float while private char catch import module export virtual operator sizeof dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace unsigned long volatile static protected bool template mutable if public friend do goto auto void enum else break extern using asm case typeid short reinterpret_cast|10 default double register explicit signed typename try this switch continue inline delete alignof constexpr decltype noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary atomic_bool atomic_char atomic_schar atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong atomic_ullong new throw return and or not", built_in:"std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr", -literal:"true false nullptr NULL"},l=[c,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,g,b];return{aliases:"c cc h c++ h++ hpp".split(" "),keywords:k,illegal:"</",contains:l.concat([d,{begin:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",end:">",keywords:k,contains:["self",c]},{begin:a.IDENT_RE+"::",keywords:k},{variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else", -end:/;/}],keywords:k,contains:l.concat([{begin:/\(/,end:/\)/,keywords:k,contains:l.concat(["self"]),relevance:0}]),relevance:0},{className:"function",begin:"("+a.IDENT_RE+"[\\*&\\s]+)+"+f,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:k,illegal:/[^\w\s\*&]/,contains:[{begin:f,returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:k,relevance:0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,g,c]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE, -d]}]),exports:{preprocessor:d,strings:b,keywords:k}}});b.registerLanguage("cs",function(a){var c={keyword:"abstract as base bool break byte case catch char checked const continue decimal dynamic default delegate do double else enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long when object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual volatile void while async nameof ascending descending from get group into join let orderby partial select set value var where yield", -literal:"null false true"},b={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},g=a.inherit(b,{illegal:/\n/}),d={className:"subst",begin:"{",end:"}",keywords:c},f=a.inherit(d,{illegal:/\n/}),k={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},a.BACKSLASH_ESCAPE,f]},l={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},d]},n=a.inherit(l,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},f]});d.contains= -[l,k,b,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE];f.contains=[n,k,g,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.inherit(a.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];b={variants:[l,k,b,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]};g=a.IDENT_RE+"(<"+a.IDENT_RE+">)?(\\[\\])?";return{aliases:["csharp"],keywords:c,illegal:/::/,contains:[a.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:"</?", -end:">"}]}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},b,a.C_NUMBER_MODE,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.TITLE_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}, -{beginKeywords:"new return throw await",relevance:0},{className:"function",begin:"("+g+"\\s+)+"+a.IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:c,contains:[{begin:a.IDENT_RE+"\\s*\\(",returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:c,relevance:0,contains:[b,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}});b.registerLanguage("css",function(a){return{case_insensitive:!0, -illegal:/[=\/|'\$]/,contains:[a.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/},{className:"selector-class",begin:/\.[A-Za-z0-9_-]+/},{className:"selector-attr",begin:/\[/,end:/\]/,illegal:"$"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"@(font-face|page)",lexemes:"[a-z-]+",keywords:"font-face page"},{begin:"@",end:"[{;]",illegal:/:/,contains:[{className:"keyword",begin:/\w+/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,contains:[a.APOS_STRING_MODE, -a.QUOTE_STRING_MODE,a.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0},{begin:"{",end:"}",illegal:/\S/,contains:[a.C_BLOCK_COMMENT_MODE,{begin:/[A-Z\_\.\-]+\s*:/,returnBegin:!0,end:";",endsWithParent:!0,contains:[{className:"attribute",begin:/\S/,end:":",excludeEnd:!0,starts:{endsWithParent:!0,excludeEnd:!0,contains:[{begin:/[\w-]+\(/,returnBegin:!0,contains:[{className:"built_in",begin:/[\w-]+/},{begin:/\(/,end:/\)/,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}]}, -a.CSS_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",begin:"#[0-9A-Fa-f]+"},{className:"meta",begin:"!important"}]}}]}]}]}});b.registerLanguage("d",function(a){var b=a.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{lexemes:a.UNDERSCORE_IDENT_RE,keywords:{keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__", +literal:"true false nullptr NULL"},k=[b,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,e,d];return{aliases:"c cc h c++ h++ hpp".split(" "),keywords:g,illegal:"</",contains:k.concat([c,{begin:"\\b(deque|list|queue|stack|vector|map|set|bitset|multiset|multimap|unordered_map|unordered_set|unordered_multiset|unordered_multimap|array)\\s*<",end:">",keywords:g,contains:["self",b]},{begin:a.IDENT_RE+"::",keywords:g},{variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else", +end:/;/}],keywords:g,contains:k.concat([{begin:/\(/,end:/\)/,keywords:g,contains:k.concat(["self"]),relevance:0}]),relevance:0},{className:"function",begin:"("+a.IDENT_RE+"[\\*&\\s]+)+"+q,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:g,illegal:/[^\w\s\*&]/,contains:[{begin:q,returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,keywords:g,relevance:0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d,e,b]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE, +c]},{className:"class",beginKeywords:"class struct",end:/[{;:]/,contains:[{begin:/</,end:/>/,contains:["self"]},a.TITLE_MODE]}]),exports:{preprocessor:c,strings:d,keywords:g}}});b.registerLanguage("cs",function(a){var b={keyword:"abstract as base bool break byte case catch char checked const continue decimal default delegate do double enum event explicit extern finally fixed float for foreach goto if implicit in int interface internal is lock long nameof object operator out override params private protected public readonly ref sbyte sealed short sizeof stackalloc static string struct switch this try typeof uint ulong unchecked unsafe ushort using virtual void volatile while add alias ascending async await by descending dynamic equals from get global group into join let on orderby partial remove select set value var where yield", +literal:"null false true"},d={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},e=a.inherit(d,{illegal:/\n/}),c={className:"subst",begin:"{",end:"}",keywords:b},g=a.inherit(c,{illegal:/\n/}),k={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},a.BACKSLASH_ESCAPE,g]},l={className:"string",begin:/\$@"/,end:'"',contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},c]},m=a.inherit(l,{illegal:/\n/,contains:[{begin:"{{"},{begin:"}}"},{begin:'""'},g]});c.contains= +[l,k,d,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE];g.contains=[m,k,e,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,a.inherit(a.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];d={variants:[l,k,d,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]};e=a.IDENT_RE+"(<"+a.IDENT_RE+"(\\s*,\\s*"+a.IDENT_RE+")*>)?(\\[\\])?";return{aliases:["csharp"],keywords:b,illegal:/::/,contains:[a.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0}, +{begin:"\x3c!--|--\x3e"},{begin:"</?",end:">"}]}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{"meta-keyword":"if else elif endif define undef warning error line region endregion pragma checksum"}},d,a.C_NUMBER_MODE,{beginKeywords:"class interface",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.TITLE_MODE,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",end:/[{;=]/,illegal:/[^\s:]/,contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}), +a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"meta-string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",begin:"("+e+"\\s+)+"+a.IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:b,contains:[{begin:a.IDENT_RE+"\\s*\\(",returnBegin:!0,contains:[a.TITLE_MODE],relevance:0},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0, +keywords:b,relevance:0,contains:[d,a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}});b.registerLanguage("css",function(a){return{case_insensitive:!0,illegal:/[=\/|'\$]/,contains:[a.C_BLOCK_COMMENT_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/},{className:"selector-class",begin:/\.[A-Za-z0-9_-]+/},{className:"selector-attr",begin:/\[/,end:/\]/,illegal:"$"},{className:"selector-pseudo",begin:/:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/},{begin:"@(font-face|page)", +lexemes:"[a-z-]+",keywords:"font-face page"},{begin:"@",end:"[{;]",illegal:/:/,contains:[{className:"keyword",begin:/\w+/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0},{begin:"{",end:"}",illegal:/\S/,contains:[a.C_BLOCK_COMMENT_MODE,{begin:/[A-Z\_\.\-]+\s*:/,returnBegin:!0,end:";",endsWithParent:!0,contains:[{className:"attribute",begin:/\S/,end:":", +excludeEnd:!0,starts:{endsWithParent:!0,excludeEnd:!0,contains:[{begin:/[\w-]+\(/,returnBegin:!0,contains:[{className:"built_in",begin:/[\w-]+/},{begin:/\(/,end:/\)/,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]}]},a.CSS_NUMBER_MODE,a.QUOTE_STRING_MODE,a.APOS_STRING_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",begin:"#[0-9A-Fa-f]+"},{className:"meta",begin:"!important"}]}}]}]}]}});b.registerLanguage("d",function(a){var b=a.COMMENT("\\/\\+","\\+\\/",{contains:["self"],relevance:10});return{lexemes:a.UNDERSCORE_IDENT_RE, +keywords:{keyword:"abstract alias align asm assert auto body break byte case cast catch class const continue debug default delete deprecated do else enum export extern final finally for foreach foreach_reverse|10 goto if immutable import in inout int interface invariant is lazy macro mixin module new nothrow out override package pragma private protected public pure ref return scope shared static struct super switch synchronized template this throw try typedef typeid typeof union unittest version void volatile while with __FILE__ __LINE__ __gshared|10 __thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__", built_in:"bool cdouble cent cfloat char creal dchar delegate double dstring float function idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar wstring",literal:"false null true"},contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,b,{className:"string",begin:'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?',relevance:10},{className:"string",begin:'"',contains:[{begin:"\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};",relevance:0}], end:'"[cwd]?'},{className:"string",begin:'[rq]"',end:'"[cwd]?',relevance:5},{className:"string",begin:"`",end:"`[cwd]?"},{className:"string",begin:'q"\\{',end:'\\}"'},{className:"number",begin:"\\b(((0[xX](([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)\\.([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)|\\.?([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))[pP][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))|((0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(\\.\\d*|([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)))|\\d+\\.(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)|\\.(0|[1-9][\\d_]*)([eE][+-]?(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d))?))([fF]|L|i|[fF]i|Li)?|((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))(i|[fF]i|Li))", relevance:0},{className:"number",begin:"\\b((0|[1-9][\\d_]*)|0[bB][01_]+|0[xX]([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*))(L|u|U|Lu|LU|uL|UL)?",relevance:0},{className:"string",begin:"'(\\\\(['\"\\?\\\\abfnrtv]|u[\\dA-Fa-f]{4}|[0-7]{1,3}|x[\\dA-Fa-f]{2}|U[\\dA-Fa-f]{8})|&[a-zA-Z\\d]{2,};|.)",end:"'",illegal:"."},{className:"meta",begin:"^#!",end:"$",relevance:5},{className:"meta",begin:"#(line)",end:"$",relevance:5},{className:"keyword",begin:"@[a-zA-Z_][a-zA-Z_\\d]*"}]}});b.registerLanguage("markdown", function(a){return{aliases:["md","mkdown","mkd"],contains:[{className:"section",variants:[{begin:"^#{1,6}",end:"$"},{begin:"^.+?\\n[=-]{2,}$"}]},{begin:"<",end:">",subLanguage:"xml",relevance:0},{className:"bullet",begin:"^([*+-]|(\\d+\\.))\\s+"},{className:"strong",begin:"[*_]{2}.+?[*_]{2}"},{className:"emphasis",variants:[{begin:"\\*.+?\\*"},{begin:"_.+?_",relevance:0}]},{className:"quote",begin:"^>\\s+",end:"$"},{className:"code",variants:[{begin:"^```w*s*$",end:"^```s*$"},{begin:"`.+?`"},{begin:"^( {4}|\t)", end:"$",relevance:0}]},{begin:"^[-\\*]{3,}",end:"$"},{begin:"\\[.+?\\][\\(\\[].*?[\\)\\]]",returnBegin:!0,contains:[{className:"string",begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0,relevance:0},{className:"link",begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}],relevance:10},{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link", -begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}});b.registerLanguage("dart",function(a){var b={className:"subst",begin:"\\$\\{",end:"}",keywords:"true false null this is new super"},e={className:"string",variants:[{begin:"r'''",end:"'''"},{begin:'r"""',end:'"""'},{begin:"r'",end:"'",illegal:"\\n"},{begin:'r"',end:'"',illegal:"\\n"},{begin:"'''",end:"'''",contains:[a.BACKSLASH_ESCAPE,b]},{begin:'"""',end:'"""',contains:[a.BACKSLASH_ESCAPE,b]},{begin:"'",end:"'",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE, -b]},{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]}]};b.contains=[a.C_NUMBER_MODE,e];return{keywords:{keyword:"assert async await break case catch class const continue default do else enum extends false final finally for if in is new null rethrow return super switch sync this throw true try var void while with yield abstract as dynamic export external factory get implements import library operator part set static typedef",built_in:"print Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set Stopwatch String StringBuffer StringSink Symbol Type Uri bool double int num document window querySelector querySelectorAll Element ElementList"}, -contains:[e,a.COMMENT("/\\*\\*","\\*/",{subLanguage:"markdown"}),a.COMMENT("///","$",{subLanguage:"markdown"}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{begin:"=>"}]}});b.registerLanguage("go",function(a){var b={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune", +begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}});b.registerLanguage("dart",function(a){var b={className:"subst",begin:"\\$\\{",end:"}",keywords:"true false null this is new super"},d={className:"string",variants:[{begin:"r'''",end:"'''"},{begin:'r"""',end:'"""'},{begin:"r'",end:"'",illegal:"\\n"},{begin:'r"',end:'"',illegal:"\\n"},{begin:"'''",end:"'''",contains:[a.BACKSLASH_ESCAPE,b]},{begin:'"""',end:'"""',contains:[a.BACKSLASH_ESCAPE,b]},{begin:"'",end:"'",illegal:"\\n",contains:[a.BACKSLASH_ESCAPE, +b]},{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]}]};b.contains=[a.C_NUMBER_MODE,d];return{keywords:{keyword:"assert async await break case catch class const continue default do else enum extends false final finally for if in is new null rethrow return super switch sync this throw true try var void while with yield abstract as dynamic export external factory get implements import library operator part set static typedef",built_in:"print Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set Stopwatch String StringBuffer StringSink Symbol Type Uri bool double int num document window querySelector querySelectorAll Element ElementList"}, +contains:[d,a.COMMENT("/\\*\\*","\\*/",{subLanguage:"markdown"}),a.COMMENT("///","$",{subLanguage:"markdown"}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"},{begin:"=>"}]}});b.registerLanguage("go",function(a){var b={keyword:"break default func interface select case map struct chan else goto package switch const fallthrough if range type continue for import return var go defer bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 uint16 uint32 uint64 int uint uintptr rune", literal:"true false iota nil",built_in:"append cap close complex copy imag len make new panic print println real recover delete"};return{aliases:["golang"],keywords:b,illegal:"</",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[a.QUOTE_STRING_MODE,{begin:"'",end:"[^\\\\]'"},{begin:"`",end:"`"}]},{className:"number",variants:[{begin:a.C_NUMBER_RE+"[dflsi]",relevance:1},a.C_NUMBER_MODE]},{begin:/:=/},{className:"function",beginKeywords:"func",end:/\s*\{/,excludeEnd:!0, -contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:b,illegal:/["']/}]}]}});b.registerLanguage("haskell",function(a){var b={variants:[a.COMMENT("--","$"),a.COMMENT("{-","-}",{contains:["self"]})]},e={className:"meta",begin:"{-#",end:"#-}"},g={className:"meta",begin:"^#",end:"$"},d={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},f={begin:"\\(",end:"\\)",illegal:'"',contains:[e,g,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},a.inherit(a.TITLE_MODE,{begin:"[_a-z][\\w']*"}), -b]};return{aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[f,b],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[f,b],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b", -end:"where",keywords:"class family instance where",contains:[d,f,b]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[e,d,f,{begin:"{",end:"}",contains:f.contains},b]},{beginKeywords:"default",end:"$",contains:[d,f,b]},{beginKeywords:"infix infixl infixr",end:"$",contains:[a.C_NUMBER_MODE,b]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[d,a.QUOTE_STRING_MODE, -b]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},e,g,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,d,a.inherit(a.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),b,{begin:"->|<-"}]}});b.registerLanguage("java",function(a){var b=a.UNDERSCORE_IDENT_RE+"(<"+a.UNDERSCORE_IDENT_RE+"(\\s*,\\s*"+a.UNDERSCORE_IDENT_RE+")*>)?";return{aliases:["jsp"],keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports", -illegal:/<\/|#/,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"("+ -b+"\\s+)+"+a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports",contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0, -relevance:0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports",relevance:0,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE, -a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0},{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("javascript",function(a){return{aliases:["js","jsx"],keywords:{keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as", +contains:[a.TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,keywords:b,illegal:/["']/}]}]}});b.registerLanguage("haskell",function(a){var b={variants:[a.COMMENT("--","$"),a.COMMENT("{-","-}",{contains:["self"]})]},d={className:"meta",begin:"{-#",end:"#-}"},e={className:"meta",begin:"^#",end:"$"},c={className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},g={begin:"\\(",end:"\\)",illegal:'"',contains:[d,e,{className:"type",begin:"\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?"},a.inherit(a.TITLE_MODE,{begin:"[_a-z][\\w']*"}), +b]};return{aliases:["hs"],keywords:"let in if then else case of where do module import hiding qualified type data newtype deriving class instance as default infix infixl infixr foreign export ccall stdcall cplusplus jvm dotnet safe unsafe family forall mdo proc rec",contains:[{beginKeywords:"module",end:"where",keywords:"module where",contains:[g,b],illegal:"\\W\\.|;"},{begin:"\\bimport\\b",end:"$",keywords:"import qualified as hiding",contains:[g,b],illegal:"\\W\\.|;"},{className:"class",begin:"^(\\s*)?(class|instance)\\b", +end:"where",keywords:"class family instance where",contains:[c,g,b]},{className:"class",begin:"\\b(data|(new)?type)\\b",end:"$",keywords:"data family type newtype deriving",contains:[d,c,g,{begin:"{",end:"}",contains:g.contains},b]},{beginKeywords:"default",end:"$",contains:[c,g,b]},{beginKeywords:"infix infixl infixr",end:"$",contains:[a.C_NUMBER_MODE,b]},{begin:"\\bforeign\\b",end:"$",keywords:"foreign import export ccall stdcall cplusplus jvm dotnet safe unsafe",contains:[c,a.QUOTE_STRING_MODE, +b]},{className:"meta",begin:"#!\\/usr\\/bin\\/env runhaskell",end:"$"},d,e,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE,c,a.inherit(a.TITLE_MODE,{begin:"^[_a-z][\\w']*"}),b,{begin:"->|<-"}]}});b.registerLanguage("java",function(a){return{aliases:["jsp"],keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do", +illegal:/<\/|#/,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"class",beginKeywords:"class interface",end:/[{;=]/,excludeEnd:!0,keywords:"class interface",illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"new throw return else",relevance:0},{className:"function",begin:"([\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*(<[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*(\\s*,\\s*[\u00c0-\u02b8a-zA-Z_$][\u00c0-\u02b8a-zA-Z_$0-9]*)*>)?\\s+)+"+ +a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0, +contains:[a.UNDERSCORE_TITLE_MODE]},{className:"params",begin:/\(/,end:/\)/,keywords:"false synchronized int abstract float private char boolean static null if const for true while long strictfp finally protected import native final void enum else break transient catch instanceof byte super volatile case assert short package default double public try this switch continue throws protected public private module requires exports do",relevance:0,contains:[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,a.C_NUMBER_MODE, +a.C_BLOCK_COMMENT_MODE]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]},{className:"number",begin:"\\b(0[bB]([01]+[01_]+[01]+|[01]+)|0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)|(([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?|\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))([eE][-+]?\\d+)?)[lLfF]?",relevance:0},{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("javascript",function(a){var b={keyword:"in of if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const export super debugger as async await static import from as", literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect Promise"}, -contains:[{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},{className:"meta",begin:/^#!/,end:/$/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,{className:"subst",begin:"\\$\\{",end:"\\}"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case", -contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{begin:/</,end:/(\/\w+|\w+\/)>/,subLanguage:"xml",contains:[{begin:/<\w+\s*\/>/,skip:!0},{begin:/<\w+/,end:/(\/\w+|\w+\/)>/,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}], -illegal:/\[|%/},{begin:/\$[(.]/},a.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0}],illegal:/#(?!!)/}});b.registerLanguage("json",function(a){var b={literal:"true false null"},e=[a.QUOTE_STRING_MODE,a.C_NUMBER_MODE],g={end:",",endsWithParent:!0,excludeEnd:!0,contains:e,keywords:b},d={begin:"{",end:"}",contains:[{className:"attr",begin:/"/, -end:/"/,contains:[a.BACKSLASH_ESCAPE],illegal:"\\n"},a.inherit(g,{begin:/:/})],illegal:"\\S"};a={begin:"\\[",end:"\\]",contains:[a.inherit(g)],illegal:"\\S"};e.splice(e.length,0,d,a);return{contains:e,keywords:b,illegal:"\\S"}});b.registerLanguage("lisp",function(a){var b={className:"literal",begin:"\\b(t{1}|nil)\\b"},e={className:"number",variants:[{begin:"(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",relevance:0},{begin:"#(b|B)[0-1]+(/[0-1]+)?"},{begin:"#(o|O)[0-7]+(/[0-7]+)?"}, -{begin:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"},{begin:"#(c|C)\\((\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)? +(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",end:"\\)"}]},g=a.inherit(a.QUOTE_STRING_MODE,{illegal:null});a=a.COMMENT(";","$",{relevance:0});var d={begin:"\\*",end:"\\*"},f={className:"symbol",begin:"[:&][a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},k={begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*", -relevance:0},l={contains:[e,g,d,f,{begin:"\\(",end:"\\)",contains:["self",b,g,e,k]},k],variants:[{begin:"['`]\\(",end:"\\)"},{begin:"\\(quote ",end:"\\)",keywords:{name:"quote"}},{begin:"'\\|[^]*?\\|"}]},n={variants:[{begin:"'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"#'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*(::[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*)*"}]},m={begin:"\\(\\s*",end:"\\)"}, -h={endsWithParent:!0,relevance:0};m.contains=[{className:"name",variants:[{begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"\\|[^]*?\\|"}]},h];h.contains=[l,n,m,b,e,g,a,d,f,{begin:"\\|[^]*?\\|"},k];return{illegal:/\S/,contains:[e,{className:"meta",begin:"^#!",end:"$"},b,g,a,l,n,m,k]}});b.registerLanguage("lua",function(a){var b={begin:"\\[=*\\[",end:"\\]=*\\]",contains:["self"]},e=[a.COMMENT("--(?!\\[=*\\[)","$"),a.COMMENT("--\\[=*\\[","\\]=*\\]",{contains:[b], -relevance:10})];return{lexemes:a.UNDERSCORE_IDENT_RE,keywords:{keyword:"and break do else elseif end false for if in local nil not or repeat return then true until while",built_in:"_G _VERSION assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall coroutine debug io math os package string table"},contains:e.concat([{className:"function",beginKeywords:"function", -end:"\\)",contains:[a.inherit(a.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:e}].concat(e)},a.C_NUMBER_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[b],relevance:5}])}});b.registerLanguage("xml",function(a){var b={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:"[A-Za-z0-9\\._:-]+",relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string", -endsParent:!0,variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/},{begin:/[^\s"'=<>`]+/}]}]}]};return{aliases:"html xhtml rss atom xjb xsd xsl plist".split(" "),case_insensitive:!0,contains:[{className:"meta",begin:"<!DOCTYPE",end:">",relevance:10,contains:[{begin:"\\[",end:"\\]"}]},a.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{begin:/<\?(php)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0}]},{className:"tag",begin:"<style(?=\\s|>|$)", -end:">",keywords:{name:"style"},contains:[b],starts:{end:"</style>",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"<script(?=\\s|>|$)",end:">",keywords:{name:"script"},contains:[b],starts:{end:"\x3c/script>",returnEnd:!0,subLanguage:["actionscript","javascript","handlebars","xml"]}},{className:"meta",variants:[{begin:/<\?xml/,end:/\?>/,relevance:10},{begin:/<\?\w+/,end:/\?>/}]},{className:"tag",begin:"</?",end:"/?>",contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},b]}]}}); -b.registerLanguage("objectivec",function(a){var b=/[a-zA-Z@][a-zA-Z0-9_]*/;return{aliases:["mm","objc","obj-c"],keywords:{keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN", +d={className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},e={className:"subst",begin:"\\$\\{",end:"\\}",keywords:b,contains:[]},c={className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,e]};e.contains=[a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,c,d,a.REGEXP_MODE];e=e.contains.concat([a.C_BLOCK_COMMENT_MODE,a.C_LINE_COMMENT_MODE]);return{aliases:["js","jsx"],keywords:b,contains:[{className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/}, +{className:"meta",begin:/^#!/,end:/$/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,c,a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,d,{begin:/[{,]\s*/,relevance:0,contains:[{begin:"[A-Za-z$_][0-9A-Za-z$_]*\\s*:",returnBegin:!0,relevance:0,contains:[{className:"attr",begin:"[A-Za-z$_][0-9A-Za-z$_]*",relevance:0}]}]},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|[A-Za-z$_][0-9A-Za-z$_]*)\\s*=>", +returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:"[A-Za-z$_][0-9A-Za-z$_]*"},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:e}]}]},{begin:/</,end:/(\/\w+|\w+\/)>/,subLanguage:"xml",contains:[{begin:/<\w+\s*\/>/,skip:!0},{begin:/<\w+/,end:/(\/\w+|\w+\/)>/,skip:!0,contains:[{begin:/<\w+\s*\/>/,skip:!0},"self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/\{/,excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z$_][0-9A-Za-z$_]*"}), +{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,contains:e}],illegal:/\[|%/},{begin:/\$[(.]/},a.METHOD_GUARD,{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"\[\]]/,contains:[{beginKeywords:"extends"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0}],illegal:/#(?!!)/}});b.registerLanguage("json",function(a){var b={literal:"true false null"},d=[a.QUOTE_STRING_MODE,a.C_NUMBER_MODE],e={end:",",endsWithParent:!0,excludeEnd:!0, +contains:d,keywords:b},c={begin:"{",end:"}",contains:[{className:"attr",begin:/"/,end:/"/,contains:[a.BACKSLASH_ESCAPE],illegal:"\\n"},a.inherit(e,{begin:/:/})],illegal:"\\S"};a={begin:"\\[",end:"\\]",contains:[a.inherit(e)],illegal:"\\S"};d.splice(d.length,0,c,a);return{contains:d,keywords:b,illegal:"\\S"}});b.registerLanguage("kotlin",function(a){var b={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit initinterface annotation data sealed internal infix operator out by constructor super trait volatile transient native default", +built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",literal:"true false null"},d={className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"@"},e={className:"subst",begin:"\\${",end:"}",contains:[a.APOS_STRING_MODE,a.C_NUMBER_MODE]},c={className:"variable",begin:"\\$"+a.UNDERSCORE_IDENT_RE},e={className:"string",variants:[{begin:'"""',end:'"""',contains:[c,e]},{begin:"'",end:"'",illegal:/\n/,contains:[a.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,contains:[a.BACKSLASH_ESCAPE,c, +e]}]},c={className:"meta",begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+a.UNDERSCORE_IDENT_RE+")?"},g={className:"meta",begin:"@"+a.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,end:/\)/,contains:[a.inherit(e,{className:"meta-string"})]}]};return{keywords:b,contains:[a.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"keyword",begin:/\b(break|continue|return|this)\b/, +starts:{contains:[{className:"symbol",begin:/@\w+/}]}},d,c,g,{className:"function",beginKeywords:"fun",end:"[(]|$",returnBegin:!0,excludeEnd:!0,keywords:b,illegal:/fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/,relevance:5,contains:[{begin:a.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"type",begin:/</,end:/>/,keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,relevance:0,contains:[{begin:/:/,end:/[=,\/]/, +endsWithParent:!0,contains:[{className:"type",begin:a.UNDERSCORE_IDENT_RE},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],relevance:0},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,c,g,e,a.C_NUMBER_MODE]},a.C_BLOCK_COMMENT_MODE]},{className:"class",beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,illegal:"extends implements",contains:[{beginKeywords:"public protected internal private constructor"},a.UNDERSCORE_TITLE_MODE,{className:"type",begin:/</,end:/>/,excludeBegin:!0,excludeEnd:!0, +relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,excludeBegin:!0,returnEnd:!0},c,g]},e,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},a.C_NUMBER_MODE]}});b.registerLanguage("lisp",function(a){var b={className:"literal",begin:"\\b(t{1}|nil)\\b"},d={className:"number",variants:[{begin:"(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",relevance:0},{begin:"#(b|B)[0-1]+(/[0-1]+)?"},{begin:"#(o|O)[0-7]+(/[0-7]+)?"},{begin:"#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?"}, +{begin:"#(c|C)\\((\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)? +(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?",end:"\\)"}]},e=a.inherit(a.QUOTE_STRING_MODE,{illegal:null});a=a.COMMENT(";","$",{relevance:0});var c={begin:"\\*",end:"\\*"},g={className:"symbol",begin:"[:&][a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},k={begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*",relevance:0}, +l={contains:[d,e,c,g,{begin:"\\(",end:"\\)",contains:["self",b,e,d,k]},k],variants:[{begin:"['`]\\(",end:"\\)"},{begin:"\\(quote ",end:"\\)",keywords:{name:"quote"}},{begin:"'\\|[^]*?\\|"}]},m={variants:[{begin:"'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"#'[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*(::[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*)*"}]},n={begin:"\\(\\s*",end:"\\)"},h={endsWithParent:!0, +relevance:0};n.contains=[{className:"name",variants:[{begin:"[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*"},{begin:"\\|[^]*?\\|"}]},h];h.contains=[l,m,n,b,d,e,a,c,g,{begin:"\\|[^]*?\\|"},k];return{illegal:/\S/,contains:[d,{className:"meta",begin:"^#!",end:"$"},b,e,a,l,m,n,k]}});b.registerLanguage("lua",function(a){var b={begin:"\\[=*\\[",end:"\\]=*\\]",contains:["self"]},d=[a.COMMENT("--(?!\\[=*\\[)","$"),a.COMMENT("--\\[=*\\[","\\]=*\\]",{contains:[b],relevance:10})]; +return{lexemes:a.UNDERSCORE_IDENT_RE,keywords:{literal:"true false nil",keyword:"and break do else elseif end for goto if in local not or repeat return then until while",built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstringmodule next pairs pcall print rawequal rawget rawset require select setfenvsetmetatable tonumber tostring type unpack xpcall arg selfcoroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"}, +contains:d.concat([{className:"function",beginKeywords:"function",end:"\\)",contains:[a.inherit(a.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:d}].concat(d)},a.C_NUMBER_MODE,a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"\\[=*\\[",end:"\\]=*\\]",contains:[b],relevance:5}])}});b.registerLanguage("xml",function(a){var b={endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",begin:"[A-Za-z0-9\\._:-]+", +relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/},{begin:/'/,end:/'/},{begin:/[^\s"'=<>`]+/}]}]}]};return{aliases:"html xhtml rss atom xjb xsd xsl plist".split(" "),case_insensitive:!0,contains:[{className:"meta",begin:"<!DOCTYPE",end:">",relevance:10,contains:[{begin:"\\[",end:"\\]"}]},a.COMMENT("\x3c!--","--\x3e",{relevance:10}),{begin:"<\\!\\[CDATA\\[",end:"\\]\\]>",relevance:10},{begin:/<\?(php)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*", +end:"\\*/",skip:!0}]},{className:"tag",begin:"<style(?=\\s|>|$)",end:">",keywords:{name:"style"},contains:[b],starts:{end:"</style>",returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:"<script(?=\\s|>|$)",end:">",keywords:{name:"script"},contains:[b],starts:{end:"\x3c/script>",returnEnd:!0,subLanguage:["actionscript","javascript","handlebars","xml"]}},{className:"meta",variants:[{begin:/<\?xml/,end:/\?>/,relevance:10},{begin:/<\?\w+/,end:/\?>/}]},{className:"tag",begin:"</?",end:"/?>", +contains:[{className:"name",begin:/[^\/><\s]+/,relevance:0},b]}]}});b.registerLanguage("objectivec",function(a){var b=/[a-zA-Z@][a-zA-Z0-9_]*/;return{aliases:["mm","objc","obj-c"],keywords:{keyword:"int float while char export sizeof typedef const struct for union unsigned long volatile static bool mutable if do return goto void enum else break extern asm case short default double register explicit signed typename this switch continue wchar_t inline readonly assign readwrite self @synchronized id typeof nonatomic super unichar IBOutlet IBAction strong weak copy in out inout bycopy byref oneway __strong __weak __block __autoreleasing @private @protected @public @try @property @end @throw @catch @finally @autoreleasepool @synthesize @dynamic @selector @optional @required @encode @package @import @defs @compatibility_alias __bridge __bridge_transfer __bridge_retained __bridge_retain __covariant __contravariant __kindof _Nonnull _Nullable _Null_unspecified __FUNCTION__ __PRETTY_FUNCTION__ __attribute__ getter setter retain unsafe_unretained nonnull nullable null_unspecified null_resettable class instancetype NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN", literal:"false true FALSE TRUE nil YES NO NULL",built_in:"BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once"},lexemes:b,illegal:"</",contains:[{className:"built_in",begin:"\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+"},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.C_NUMBER_MODE,a.QUOTE_STRING_MODE,{className:"string",variants:[{begin:'@"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:"'",end:"[^\\\\]'",illegal:"[^\\\\][^']"}]}, {className:"meta",begin:"#",end:"$",contains:[{className:"meta-string",variants:[{begin:'"',end:'"'},{begin:"<",end:">"}]}]},{className:"class",begin:"(@interface|@class|@protocol|@implementation)\\b",end:"({|$)",excludeEnd:!0,keywords:"@interface @class @protocol @implementation",lexemes:b,contains:[a.UNDERSCORE_TITLE_MODE]},{begin:"\\."+a.UNDERSCORE_IDENT_RE,relevance:0}]}});b.registerLanguage("ocaml",function(a){return{aliases:["ml"],keywords:{keyword:"and as assert asr begin class constraint do done downto else end exception external for fun function functor if in include inherit! inherit initializer land lazy let lor lsl lsr lxor match method!|10 method mod module mutable new object of open! open or private rec sig struct then to try type val! val virtual when while with parser value", built_in:"array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 string unit in_channel out_channel ref",literal:"true false"},illegal:/\/\/|>>/,lexemes:"[a-z_]\\w*!?",contains:[{className:"literal",begin:"\\[(\\|\\|)?\\]|\\(\\)",relevance:0},a.COMMENT("\\(\\*","\\*\\)",{contains:["self"]}),{className:"symbol",begin:"'[A-Za-z_](?!')[\\w']*"},{className:"type",begin:"`[A-Z][\\w']*"},{className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},{begin:"[a-z_]\\w*'[\\w']*",relevance:0}, a.inherit(a.APOS_STRING_MODE,{className:"string",relevance:0}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null}),{className:"number",begin:"\\b(0[xX][a-fA-F0-9_]+[Lln]?|0[oO][0-7_]+[Lln]?|0[bB][01_]+[Lln]?|[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)",relevance:0},{begin:/[-=]>/}]}});b.registerLanguage("perl",function(a){var b={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when"}, -e={begin:"->{",end:"}"},g={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},d=[a.BACKSLASH_ESCAPE,b,g];a=[g,a.HASH_COMMENT_MODE,a.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),e,{className:"string",contains:d,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<", +d={begin:"->{",end:"}"},e={variants:[{begin:/\$\d/},{begin:/[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/},{begin:/[\$%@][^\s\w{]/,relevance:0}]},c=[a.BACKSLASH_ESCAPE,b,e];a=[e,a.HASH_COMMENT_MODE,a.COMMENT("^\\=\\w","\\=cut",{endsWithParent:!0}),d,{className:"string",contains:c,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*\\<", end:"\\>",relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},{begin:"{\\w+}",contains:[],relevance:0},{begin:"-?\\w+\\s*\\=\\>",contains:[],relevance:0}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\/\\/|"+a.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",keywords:"split return print reverse grep", relevance:0,contains:[a.HASH_COMMENT_MODE,{className:"regexp",begin:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",relevance:10},{className:"regexp",begin:"(m|qr)?/",end:"/[a-z]*",contains:[a.BACKSLASH_ESCAPE],relevance:0}]},{className:"function",beginKeywords:"sub",end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[a.TITLE_MODE]},{begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]}];b.contains= -a;e.contains=a;return{aliases:["pl","pm"],lexemes:/[\w\.]+/,keywords:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when", -contains:a}});b.registerLanguage("php",function(a){var b={begin:"\\$+[a-zA-Z_\u007f-\u00ff][a-zA-Z0-9_\u007f-\u00ff]*"},e={className:"meta",begin:/<\?(php)?|\?>/},g={className:"string",contains:[a.BACKSLASH_ESCAPE,e],variants:[{begin:'b"',end:'"'},{begin:"b'",end:"'"},a.inherit(a.APOS_STRING_MODE,{illegal:null}),a.inherit(a.QUOTE_STRING_MODE,{illegal:null})]},d={variants:[a.BINARY_NUMBER_MODE,a.C_NUMBER_MODE]};return{aliases:["php3","php4","php5","php6"],case_insensitive:!0,keywords:"and include_once list abstract global private echo interface as static endswitch array null if endwhile or const for endforeach self var while isset public protected exit foreach throw elseif include __FILE__ empty require_once do xor return parent clone use __CLASS__ __LINE__ else break print eval new catch __METHOD__ case exception default die require __FUNCTION__ enddeclare final try switch continue endfor endif declare unset true false trait goto instanceof insteadof __DIR__ __NAMESPACE__ yield finally", -contains:[a.HASH_COMMENT_MODE,a.COMMENT("//","$",{contains:[e]}),a.COMMENT("/\\*","\\*/",{contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),a.COMMENT("__halt_compiler.+?;",!1,{endsWithParent:!0,keywords:"__halt_compiler",lexemes:a.UNDERSCORE_IDENT_RE}),{className:"string",begin:/<<<['"]?\w+['"]?$/,end:/^\w+;?$/,contains:[a.BACKSLASH_ESCAPE,{className:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]}]},e,{className:"keyword",begin:/\$this\b/},b,{begin:/(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/}, -{className:"function",beginKeywords:"function",end:/[;{]/,excludeEnd:!0,illegal:"\\$|\\[|%",contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:"\\(",end:"\\)",contains:["self",b,a.C_BLOCK_COMMENT_MODE,g,d]}]},{className:"class",beginKeywords:"class interface",end:"{",excludeEnd:!0,illegal:/[:\(\$"]/,contains:[{beginKeywords:"extends implements"},a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",end:";",illegal:/[\.']/,contains:[a.UNDERSCORE_TITLE_MODE]},{beginKeywords:"use",end:";",contains:[a.UNDERSCORE_TITLE_MODE]}, -{begin:"=>"},g,d]}});b.registerLanguage("protobuf",function(a){return{keywords:{keyword:"package import option optional required repeated group",built_in:"double float int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string bytes",literal:"true false"},contains:[a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.C_LINE_COMMENT_MODE,{className:"class",beginKeywords:"message enum service",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]}, -{className:"function",beginKeywords:"rpc",end:/;/,excludeEnd:!0,keywords:"rpc returns"},{begin:/^\s*[A-Z_]+/,end:/\s*=/,excludeEnd:!0}]}});b.registerLanguage("python",function(a){var b={className:"meta",begin:/^(>>>|\.\.\.) /},e={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[b],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[b],relevance:10},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/, -end:/'/},{begin:/(b|br)"/,end:/"/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},g={className:"number",relevance:0,variants:[{begin:a.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:a.C_NUMBER_RE+"[lLjJ]?"}]};return{aliases:["py","gyp"],keywords:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"}, -illegal:/(<\/|->|\?)/,contains:[b,g,e,a.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def",relevance:10},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[a.UNDERSCORE_TITLE_MODE,{className:"params",begin:/\(/,end:/\)/,contains:["self",b,g,e]},{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}});b.registerLanguage("ruby",function(a){var b={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor", -literal:"true false nil"},e={className:"doctag",begin:"@[A-Za-z]+"},g={begin:"#<",end:">"},e=[a.COMMENT("#","$",{contains:[e]}),a.COMMENT("^\\=begin","^\\=end",{contains:[e],relevance:10}),a.COMMENT("^__END__","\\n$")],d={className:"subst",begin:"#\\{",end:"}",keywords:b},f={className:"string",contains:[a.BACKSLASH_ESCAPE,d],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<", -end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-",end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/}]},k={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:b};a=[f,g,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+a.IDENT_RE+"::)?"+a.IDENT_RE}]}].concat(e)}, -{className:"function",beginKeywords:"def",end:"$|;",contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}),k].concat(e)},{begin:a.IDENT_RE+"::"},{className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[f,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b", -relevance:0},{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:b},{begin:"("+a.RE_STARTERS_RE+")\\s*",contains:[g,{className:"regexp",contains:[a.BACKSLASH_ESCAPE,d],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(e),relevance:0}].concat(e);d.contains=a;k.contains=a;return{aliases:["rb","gemspec","podspec","thor","irb"],keywords:b, -illegal:/\/\*/,contains:e.concat([{begin:/^\s*=>/,starts:{end:"$",contains:a}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:a}}]).concat(a)}});b.registerLanguage("rust",function(a){var b=a.inherit(a.C_BLOCK_COMMENT_MODE);b.contains.push("self");return{aliases:["rs"],keywords:{keyword:"alignof as be box break const continue crate do else enum extern false fn for if impl in let loop match mod mut offsetof once priv proc pub pure ref return self Self sizeof static struct super trait true type typeof unsafe unsized use virtual while where yield move default int i8 i16 i32 i64 isize uint u8 u32 u64 usize float f32 f64 str char bool", -literal:"true false Some None Ok Err",built_in:"Copy Send Sized Sync Drop Fn FnMut FnOnce drop Box ToOwned Clone PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator Option Result SliceConcatExt String ToString Vec assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules!"}, -lexemes:a.IDENT_RE+"!?",illegal:"</",contains:[a.C_LINE_COMMENT_MODE,b,a.inherit(a.QUOTE_STRING_MODE,{begin:/b?"/,illegal:null}),{className:"string",variants:[{begin:/r(#*)".*?"\1(?!#)/},{begin:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{className:"symbol",begin:/'[a-zA-Z_][a-zA-Z0-9_]*/},{className:"number",variants:[{begin:"\\b0b([01_]+)([uif](8|16|32|64|size))?"},{begin:"\\b0o([0-7_]+)([uif](8|16|32|64|size))?"},{begin:"\\b0x([A-Fa-f0-9_]+)([uif](8|16|32|64|size))?"},{begin:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)([uif](8|16|32|64|size))?"}], -relevance:0},{className:"function",beginKeywords:"fn",end:"(\\(|<)",excludeEnd:!0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"meta",begin:"#\\!?\\[",end:"\\]",contains:[{className:"meta-string",begin:/"/,end:/"/}]},{className:"class",beginKeywords:"type",end:";",contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"\\S"},{className:"class",beginKeywords:"trait enum struct",end:"{",contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"[\\w\\d]"},{begin:a.IDENT_RE+ -"::",keywords:{built_in:"Copy Send Sized Sync Drop Fn FnMut FnOnce drop Box ToOwned Clone PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator Option Result SliceConcatExt String ToString Vec assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules!"}}, -{begin:"->"}]}});b.registerLanguage("scala",function(a){var b={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},e={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},g={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0};return{keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"}, -contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[b],relevance:10}]},{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},e,{className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[g]},{className:"class",beginKeywords:"class object trait type", -end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[e]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[e]},g]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("sql",function(a){var b=a.COMMENT("--","$");return{case_insensitive:!0,illegal:/[<>{}*#]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke", +a;d.contains=a;return{aliases:["pl","pm"],lexemes:/[\w\.]+/,keywords:"getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qqfileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent shutdown dump chomp connect getsockname die socketpair close flock exists index shmgetsub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedirioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when", +contains:a}});b.registerLanguage("protobuf",function(a){return{keywords:{keyword:"package import option optional required repeated group",built_in:"double float int32 int64 uint32 uint64 sint32 sint64 fixed32 fixed64 sfixed32 sfixed64 bool string bytes",literal:"true false"},contains:[a.QUOTE_STRING_MODE,a.NUMBER_MODE,a.C_LINE_COMMENT_MODE,{className:"class",beginKeywords:"message enum service",end:/\{/,illegal:/\n/,contains:[a.inherit(a.TITLE_MODE,{starts:{endsWithParent:!0,excludeEnd:!0}})]},{className:"function", +beginKeywords:"rpc",end:/;/,excludeEnd:!0,keywords:"rpc returns"},{begin:/^\s*[A-Z_]+/,end:/\s*=/,excludeEnd:!0}]}});b.registerLanguage("python",function(a){var b={keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda async await nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},d={className:"meta",begin:/^(>>>|\.\.\.) /},e={className:"subst",begin:/\{/,end:/\}/,keywords:b, +illegal:/#/},c={className:"string",contains:[a.BACKSLASH_ESCAPE],variants:[{begin:/(u|b)?r?'''/,end:/'''/,contains:[d],relevance:10},{begin:/(u|b)?r?"""/,end:/"""/,contains:[d],relevance:10},{begin:/(fr|rf|f)'''/,end:/'''/,contains:[d,e]},{begin:/(fr|rf|f)"""/,end:/"""/,contains:[d,e]},{begin:/(u|r|ur)'/,end:/'/,relevance:10},{begin:/(u|r|ur)"/,end:/"/,relevance:10},{begin:/(b|br)'/,end:/'/},{begin:/(b|br)"/,end:/"/},{begin:/(fr|rf|f)'/,end:/'/,contains:[e]},{begin:/(fr|rf|f)"/,end:/"/,contains:[e]}, +a.APOS_STRING_MODE,a.QUOTE_STRING_MODE]},g={className:"number",relevance:0,variants:[{begin:a.BINARY_NUMBER_RE+"[lLjJ]?"},{begin:"\\b(0o[0-7]+)[lLjJ]?"},{begin:a.C_NUMBER_RE+"[lLjJ]?"}]},k={className:"params",begin:/\(/,end:/\)/,contains:["self",d,g,c]};e.contains=[c,g,d];return{aliases:["py","gyp"],keywords:b,illegal:/(<\/|->|\?)|=>/,contains:[d,g,c,a.HASH_COMMENT_MODE,{variants:[{className:"function",beginKeywords:"def"},{className:"class",beginKeywords:"class"}],end:/:/,illegal:/[${=;\n,]/,contains:[a.UNDERSCORE_TITLE_MODE, +k,{begin:/->/,endsWithParent:!0,keywords:"None"}]},{className:"meta",begin:/^[\t ]*@/,end:/$/},{begin:/\b(print|exec)\(/}]}});b.registerLanguage("ruby",function(a){var b={keyword:"and then defined module in return redo if BEGIN retry end for self when next until do begin unless END rescue else break undef not super class case require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor",literal:"true false nil"},d={className:"doctag",begin:"@[A-Za-z]+"},e={begin:"#<",end:">"}, +d=[a.COMMENT("#","$",{contains:[d]}),a.COMMENT("^\\=begin","^\\=end",{contains:[d],relevance:10}),a.COMMENT("^__END__","\\n$")],c={className:"subst",begin:"#\\{",end:"}",keywords:b},g={className:"string",contains:[a.BACKSLASH_ESCAPE,c],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:"%[qQwWx]?\\(",end:"\\)"},{begin:"%[qQwWx]?\\[",end:"\\]"},{begin:"%[qQwWx]?{",end:"}"},{begin:"%[qQwWx]?<",end:">"},{begin:"%[qQwWx]?/",end:"/"},{begin:"%[qQwWx]?%",end:"%"},{begin:"%[qQwWx]?-", +end:"-"},{begin:"%[qQwWx]?\\|",end:"\\|"},{begin:/\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/},{begin:/<<(-?)\w+$/,end:/^\s*\w+$/}]},k={className:"params",begin:"\\(",end:"\\)",endsParent:!0,keywords:b};a=[g,e,{className:"class",beginKeywords:"class module",end:"$|;",illegal:/=/,contains:[a.inherit(a.TITLE_MODE,{begin:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?"}),{begin:"<\\s*",contains:[{begin:"("+a.IDENT_RE+"::)?"+a.IDENT_RE}]}].concat(d)},{className:"function",beginKeywords:"def",end:"$|;", +contains:[a.inherit(a.TITLE_MODE,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}),k].concat(d)},{begin:a.IDENT_RE+"::"},{className:"symbol",begin:a.UNDERSCORE_IDENT_RE+"(\\!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[g,{begin:"[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?"}],relevance:0},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0}, +{begin:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},{className:"params",begin:/\|/,end:/\|/,keywords:b},{begin:"("+a.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[e,{className:"regexp",contains:[a.BACKSLASH_ESCAPE,c],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:"%r{",end:"}[a-z]*"},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(d),relevance:0}].concat(d);c.contains=a;k.contains=a;return{aliases:["rb","gemspec","podspec","thor","irb"], +keywords:b,illegal:/\/\*/,contains:d.concat([{begin:/^\s*=>/,starts:{end:"$",contains:a}},{className:"meta",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+>|(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>)",starts:{end:"$",contains:a}}]).concat(a)}});b.registerLanguage("rust",function(a){return{aliases:["rs"],keywords:{keyword:"alignof as be box break const continue crate do else enum extern false fn for if impl in let loop match mod mut offsetof once priv proc pub pure ref return self Self sizeof static struct super trait true type typeof unsafe unsized use virtual while where yield move default", +literal:"true false Some None Ok Err",built_in:"drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!"}, +lexemes:a.IDENT_RE+"!?",illegal:"</",contains:[a.C_LINE_COMMENT_MODE,a.COMMENT("/\\*","\\*/",{contains:["self"]}),a.inherit(a.QUOTE_STRING_MODE,{begin:/b?"/,illegal:null}),{className:"string",variants:[{begin:/r(#*)"(.|\n)*?"\1(?!#)/},{begin:/b?'\\?(x\w{2}|u\w{4}|U\w{8}|.)'/}]},{className:"symbol",begin:/'[a-zA-Z_][a-zA-Z0-9_]*/},{className:"number",variants:[{begin:"\\b0b([01_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},{begin:"\\b0o([0-7_]+)([ui](8|16|32|64|128|size)|f(32|64))?"},{begin:"\\b0x([A-Fa-f0-9_]+)([ui](8|16|32|64|128|size)|f(32|64))?"}, +{begin:"\\b(\\d[\\d_]*(\\.[0-9_]+)?([eE][+-]?[0-9_]+)?)([ui](8|16|32|64|128|size)|f(32|64))?"}],relevance:0},{className:"function",beginKeywords:"fn",end:"(\\(|<)",excludeEnd:!0,contains:[a.UNDERSCORE_TITLE_MODE]},{className:"meta",begin:"#\\!?\\[",end:"\\]",contains:[{className:"meta-string",begin:/"/,end:/"/}]},{className:"class",beginKeywords:"type",end:";",contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"\\S"},{className:"class",beginKeywords:"trait enum struct union",end:"{", +contains:[a.inherit(a.UNDERSCORE_TITLE_MODE,{endsParent:!0})],illegal:"[\\w\\d]"},{begin:a.IDENT_RE+"::",keywords:{built_in:"drop i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize f32 f64 str char bool Box Option Result String Vec Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator Extend IntoIterator DoubleEndedIterator ExactSizeIterator SliceConcatExt ToString assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! debug_assert! debug_assert_eq! env! panic! file! format! format_args! include_bin! include_str! line! local_data_key! module_path! option_env! print! println! select! stringify! try! unimplemented! unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!"}}, +{begin:"->"}]}});b.registerLanguage("scala",function(a){var b={className:"subst",variants:[{begin:"\\$[A-Za-z0-9_]+"},{begin:"\\${",end:"}"}]},d={className:"type",begin:"\\b[A-Z][A-Za-z0-9_]*",relevance:0},e={className:"title",begin:/[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/,relevance:0};return{keywords:{literal:"true false null",keyword:"type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit"}, +contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"string",variants:[{begin:'"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE]},{begin:'"""',end:'"""',relevance:10},{begin:'[a-z]+"',end:'"',illegal:"\\n",contains:[a.BACKSLASH_ESCAPE,b]},{className:"string",begin:'[a-z]+"""',end:'"""',contains:[b],relevance:10}]},{className:"symbol",begin:"'\\w[\\w\\d_]*(?!')"},d,{className:"function",beginKeywords:"def",end:/[:={\[(\n;]/,excludeEnd:!0,contains:[e]},{className:"class",beginKeywords:"class object trait type", +end:/[:={\[\n;]/,excludeEnd:!0,contains:[{beginKeywords:"extends with",relevance:10},{begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[d]},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[d]},e]},a.C_NUMBER_MODE,{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("sql",function(a){var b=a.COMMENT("--","$");return{case_insensitive:!0,illegal:/[<>{}*#]/,contains:[{beginKeywords:"begin end start commit rollback savepoint lock alter create drop rename call delete do handler insert load replace select truncate update set show pragma grant merge describe use explain help declare prepare execute deallocate release unlock purge reset change stop analyze cache flush optimize repair kill install uninstall checksum restore check backup revoke comment", end:/;/,endsWithParent:!0,lexemes:/[\w\.]+/,keywords:{keyword:"abort abs absolute acc acce accep accept access accessed accessible account acos action activate add addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base char_length character_length characters characterset charindex charset charsetform charsetid check checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation collect colu colum column column_value columns columns_updated comment commit compact compatibility compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection consider consistent constant constraint constraints constructor container content contents context contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor deterministic diagnostics difference dimension direct_load directory disable disable_all disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding execu execut execute exempt exists exit exp expire explain export export_set extended extent external external_1 external_2 externally extract failed failed_login_attempts failover failure far fast feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final finish first first_value fixed flash_cache flashback floor flush following follows for forall force form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ftp full function general generated get get_format get_lock getdate getutcdate global global_name globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex hierarchy high high_priority hosts hour http id ident_current ident_incr ident_seed identified identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile initial initialized initially initrans inmemory inner innodb input insert install instance instantiable instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lax lcase lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime managed management manual map mapping mask master master_pos_wait match matched materialized max maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans md5 measures median medium member memcompress memory merge microsecond mid migration min minextents minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month months mount move movement multiset mutex name name_const names nan national native natural nav nchar nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary out outer outfile outline output over overflow overriding package pad parallel parallel_enable parameters parent parse partial partition partitions pascal passing password password_grace_time password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction prediction_cost prediction_details prediction_probability prediction_set prepare present preserve prior priority private private_sga privileges procedural procedure procedure_analyze processlist profiles project prompt protection public publishingservername purge quarter query quick quiesce quota quotename radians raise rand range rank raw read reads readsize rebuild record records recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename repair repeat replace replicate replication required reset resetlogs resize resource respect restore restricted result result_cache resumable resume retention return returning returns reuse reverse revoke right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select self sequence sequential serializable server servererror session session_user sessions_per_user set sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone standby start starting startup statement static statistics stats_binomial_test stats_crosstab stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tan tdo template temporary terminated tertiary_weights test than then thread through tier ties time time_format time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek", literal:"true false null",built_in:"array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number numeric real record serial serial8 smallint text varchar varying void"},contains:[{className:"string",begin:"'",end:"'",contains:[a.BACKSLASH_ESCAPE,{begin:"''"}]},{className:"string",begin:'"',end:'"',contains:[a.BACKSLASH_ESCAPE,{begin:'""'}]},{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE]},a.C_NUMBER_MODE,a.C_BLOCK_COMMENT_MODE,b]}, -a.C_BLOCK_COMMENT_MODE,b]}});b.registerLanguage("swift",function(a){var b={keyword:"__COLUMN__ __FILE__ __FUNCTION__ __LINE__ as as! as? associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet", +a.C_BLOCK_COMMENT_MODE,b]}});b.registerLanguage("swift",function(a){var b={keyword:"__COLUMN__ __FILE__ __FUNCTION__ __LINE__ as as! as? associativity break case catch class continue convenience default defer deinit didSet do dynamic dynamicType else enum extension fallthrough false fileprivate final for func get guard if import in indirect infix init inout internal is lazy left let mutating nil none nonmutating open operator optional override postfix precedence prefix private protocol Protocol public repeat required rethrows return right self Self set static struct subscript super switch throw throws true try try! try? Type typealias unowned var weak where while willSet", literal:"true false nil",built_in:"abs advance alignof alignofValue anyGenerator assert assertionFailure bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC bridgeToObjectiveCUnconditional c contains count countElements countLeadingZeros debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords enumerate equal fatalError filter find getBridgedObjectiveCType getVaList indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare map max maxElement min minElement numericCast overlaps partition posix precondition preconditionFailure print println quickSort readLine reduce reflect reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split startsWith stride strideof strideofValue swap toString transcode underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers withUnsafePointer withUnsafePointers withVaList zip"}, -e=a.COMMENT("/\\*","\\*/",{contains:["self"]}),g={className:"subst",begin:/\\\(/,end:"\\)",keywords:b,contains:[]},d={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0},f=a.inherit(a.QUOTE_STRING_MODE,{contains:[g,a.BACKSLASH_ESCAPE]});g.contains=[d];return{keywords:b,contains:[f,a.C_LINE_COMMENT_MODE,e,{className:"type",begin:"\\b[A-Z][\\w']*",relevance:0},d,{className:"function",beginKeywords:"func",end:"{",excludeEnd:!0, -contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin:/</,end:/>/},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,contains:["self",d,f,a.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:b,end:"\\{",excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/})]},{className:"meta",begin:"(@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain)"}, -{beginKeywords:"import",end:/$/,contains:[a.C_LINE_COMMENT_MODE,e]}]}});b.registerLanguage("typescript",function(a){var b={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void"}; +d=a.COMMENT("/\\*","\\*/",{contains:["self"]}),e={className:"subst",begin:/\\\(/,end:"\\)",keywords:b,contains:[]},c={className:"number",begin:"\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b",relevance:0},g=a.inherit(a.QUOTE_STRING_MODE,{contains:[e,a.BACKSLASH_ESCAPE]});e.contains=[c];return{keywords:b,contains:[g,a.C_LINE_COMMENT_MODE,d,{className:"type",begin:"\\b[A-Z][\\w\u00c0-\u02b8']*",relevance:0},c,{className:"function",beginKeywords:"func",end:"{", +excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{begin:/</,end:/>/},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:b,contains:["self",c,g,a.C_BLOCK_COMMENT_MODE,{begin:":"}],illegal:/["']/}],illegal:/\[|%/},{className:"class",beginKeywords:"struct protocol class extension enum",keywords:b,end:"\\{",excludeEnd:!0,contains:[a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/})]},{className:"meta",begin:"(@warn_unused_result|@exported|@lazy|@noescape|@NSCopying|@NSManaged|@objc|@convention|@required|@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|@infix|@prefix|@postfix|@autoclosure|@testable|@available|@nonobjc|@NSApplicationMain|@UIApplicationMain)"}, +{beginKeywords:"import",end:/$/,contains:[a.C_LINE_COMMENT_MODE,d]}]}});b.registerLanguage("typescript",function(a){var b={keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class public private protected get set super static implements enum export import declare type namespace abstract as from extends async await",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document any number boolean string void Promise"}; return{aliases:["ts"],keywords:b,contains:[{className:"meta",begin:/^\s*['"]use strict['"]/},a.APOS_STRING_MODE,a.QUOTE_STRING_MODE,{className:"string",begin:"`",end:"`",contains:[a.BACKSLASH_ESCAPE,{className:"subst",begin:"\\$\\{",end:"\\}"}]},a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,{className:"number",variants:[{begin:"\\b(0[bB][01]+)"},{begin:"\\b(0[oO][0-7]+)"},{begin:a.C_NUMBER_RE}],relevance:0},{begin:"("+a.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case", -contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE],relevance:0},{className:"function",begin:"function",end:/[\{;]/,excludeEnd:!0,keywords:b,contains:["self",a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}),{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/["'\(]/}],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0},{begin:/module\./,keywords:{built_in:"module"}, -relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0},{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+a.IDENT_RE,relevance:0}]}});b.registerLanguage("yaml",function(a){var b={className:"attr",variants:[{begin:"^[ \\-]*[a-zA-Z_][\\w\\-]*:"},{begin:'^[ \\-]*"[a-zA-Z_][\\w\\-]*":'},{begin:"^[ \\-]*'[a-zA-Z_][\\w\\-]*':"}]},e={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/}],contains:[a.BACKSLASH_ESCAPE, -{className:"template-variable",variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]};return{case_insensitive:!0,aliases:["yml","YAML","yaml"],contains:[b,{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>] *$",returnEnd:!0,contains:e.contains,end:b.variants[0].begin},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!!"+a.UNDERSCORE_IDENT_RE},{className:"meta",begin:"&"+a.UNDERSCORE_IDENT_RE+"$"}, -{className:"meta",begin:"\\*"+a.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"^ *-",relevance:0},e,a.HASH_COMMENT_MODE,a.C_NUMBER_MODE],keywords:{literal:"{ } true false yes no Yes No True False null"}}});return b}); +contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE,a.REGEXP_MODE,{className:"function",begin:"(\\(.*?\\)|"+a.IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:a.IDENT_RE},{begin:/\(\s*\)/},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:["self",a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE]}]}]}],relevance:0},{className:"function",begin:"function",end:/[\{;]/,excludeEnd:!0,keywords:b,contains:["self",a.inherit(a.TITLE_MODE,{begin:/[A-Za-z$_][0-9A-Za-z$_]*/}), +{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/["'\(]/}],illegal:/%/,relevance:0},{beginKeywords:"constructor",end:/\{/,excludeEnd:!0,contains:["self",{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:b,contains:[a.C_LINE_COMMENT_MODE,a.C_BLOCK_COMMENT_MODE],illegal:/["'\(]/}]},{begin:/module\./,keywords:{built_in:"module"},relevance:0},{beginKeywords:"module",end:/\{/,excludeEnd:!0}, +{beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:"interface extends"},{begin:/\$[(.]/},{begin:"\\."+a.IDENT_RE,relevance:0},{className:"meta",begin:"@[A-Za-z]+"}]}});b.registerLanguage("yaml",function(a){var b={className:"attr",variants:[{begin:"^[ \\-]*[a-zA-Z_][\\w\\-]*:"},{begin:'^[ \\-]*"[a-zA-Z_][\\w\\-]*":'},{begin:"^[ \\-]*'[a-zA-Z_][\\w\\-]*':"}]},d={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[a.BACKSLASH_ESCAPE,{className:"template-variable", +variants:[{begin:"{{",end:"}}"},{begin:"%{",end:"}"}]}]};return{case_insensitive:!0,aliases:["yml","YAML","yaml"],contains:[b,{className:"meta",begin:"^---s*$",relevance:10},{className:"string",begin:"[\\|>] *$",returnEnd:!0,contains:d.contains,end:b.variants[0].begin},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!!"+a.UNDERSCORE_IDENT_RE},{className:"meta",begin:"&"+a.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+ +a.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"^ *-",relevance:0},a.HASH_COMMENT_MODE,{beginKeywords:"true false yes no null",keywords:{literal:"true false yes no null"}},a.C_NUMBER_MODE,d]}});return b});
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl index 869daa8..fd10fbb 100644 --- a/lib/jgit/jgit.bzl +++ b/lib/jgit/jgit.bzl
@@ -1,6 +1,6 @@ load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_LOCAL", "MAVEN_CENTRAL", "maven_jar") -_JGIT_VERS = "4.7.1.201706071930-r" +_JGIT_VERS = "4.8.0.201706111038-r" _DOC_VERS = _JGIT_VERS # Set to _JGIT_VERS unless using a snapshot @@ -26,28 +26,28 @@ name = "jgit_lib", artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS, repository = _JGIT_REPO, - sha1 = "0023aa574d6ea984e770af60da94da366a0109d2", - src_sha1 = "0497d0ac4f7c44eea49a32e7e1d6a5eee6343c33", + sha1 = "f0978a9e868accf9a405d9387bec091a99d87633", + src_sha1 = "93cefbf1d73f1179b40419a3898c53a64e52aa93", unsign = True, ) maven_jar( name = "jgit_servlet", artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS, repository = _JGIT_REPO, - sha1 = "0bacf02e5c9c587f8a6e680278d2b4b7fc8df96d", + sha1 = "3c099afdc063bad438a3b87eea643e9722a07de8", unsign = True, ) maven_jar( name = "jgit_archive", artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS, repository = _JGIT_REPO, - sha1 = "83c22720f1b00b4b5e321b9c8b089b91c1d78893", + sha1 = "1350a5cf1fad91dd33b66f9fb804dc8e68270890", ) maven_jar( name = "jgit_junit", artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS, repository = _JGIT_REPO, - sha1 = "5a3f2d6cf33e88f3436acfd22a129bc7e2d2655b", + sha1 = "4f45f8f6714df649dbad8c1b1baf68b9510b5047", unsign = True, )
diff --git a/lib/js/bower_archives.bzl b/lib/js/bower_archives.bzl index 67cc7c0..9b96521 100644 --- a/lib/js/bower_archives.bzl +++ b/lib/js/bower_archives.bzl
@@ -10,8 +10,8 @@ bower_archive( name = "accessibility-developer-tools", package = "accessibility-developer-tools", - version = "2.11.0", - sha1 = "792cb24b649dafb316e7e536f8ae65d0d7b52bab") + version = "2.12.0", + sha1 = "88ae82dcdeb6c658f76eff509d0ee425cae14d49") bower_archive( name = "async", package = "async", @@ -40,13 +40,13 @@ bower_archive( name = "iron-fit-behavior", package = "iron-fit-behavior", - version = "1.2.5", - sha1 = "5938815cd227843fc77ebeac480b999600a76157") + version = "1.2.6", + sha1 = "59daa8526aac59aa72b8edcbbd24d9eed555a0f5") bower_archive( name = "iron-flex-layout", package = "iron-flex-layout", - version = "1.3.1", - sha1 = "ba696394abff5e799fc06eb11bff4720129a1b52") + version = "1.3.2", + sha1 = "b896041aad049a5e889a0165828d7b1262e32612") bower_archive( name = "iron-form-element-behavior", package = "iron-form-element-behavior", @@ -105,5 +105,5 @@ bower_archive( name = "webcomponentsjs", package = "webcomponentsjs", - version = "0.7.23", - sha1 = "3d62269e614175573b0a0f3039aab05d40f0a763") + version = "0.7.24", + sha1 = "559227f8ee9db9bfbd81989f24510cc0c1bfc65c")
diff --git a/lib/js/bower_components.bzl b/lib/js/bower_components.bzl index bb047bd70..9c3763e 100644 --- a/lib/js/bower_components.bzl +++ b/lib/js/bower_components.bzl
@@ -174,6 +174,15 @@ seed = True, ) bower_component( + name = "polymer-resin", + license = "//lib:LICENSE-polymer", + deps = [ + ":polymer", + ":webcomponentsjs", + ], + seed = True, + ) + bower_component( name = "promise-polyfill", license = "//lib:LICENSE-promise-polyfill", deps = [ ":polymer" ],
diff --git a/lib/prolog/prolog.bzl b/lib/prolog/prolog.bzl index 0e87c01..43a8bab 100644 --- a/lib/prolog/prolog.bzl +++ b/lib/prolog/prolog.bzl
@@ -12,14 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -load("//tools/bzl:genrule2.bzl", "genrule2") - def prolog_cafe_library( name, srcs, deps = [], **kwargs): - genrule2( + native.genrule( name = name + '__pl2j', cmd = '$(location //lib/prolog:compiler_bin) ' + '$$(dirname $@) $@ ' +
diff --git a/plugins/download-commands b/plugins/download-commands index 8357e94..6ee2462 160000 --- a/plugins/download-commands +++ b/plugins/download-commands
@@ -1 +1 @@ -Subproject commit 8357e942dd9da82884a4e1b6e4697479153d0496 +Subproject commit 6ee246245b9200062e753d1c6943d5782cb7fee0
diff --git a/plugins/hooks b/plugins/hooks index 0e4e41a..18edefa 160000 --- a/plugins/hooks +++ b/plugins/hooks
@@ -1 +1 @@ -Subproject commit 0e4e41a160afb3553ea0a895500770b290ab569b +Subproject commit 18edefac123218ef61af7e4926b2c56824eef03a
diff --git a/plugins/replication b/plugins/replication index dc18cb6..fae5360 160000 --- a/plugins/replication +++ b/plugins/replication
@@ -1 +1 @@ -Subproject commit dc18cb665eb452d71602e1a980d7669a67265dfc +Subproject commit fae5360380023e8351f39be3d4effd4bb2cd8906
diff --git a/plugins/reviewnotes b/plugins/reviewnotes index 7070ce2..be803eb 160000 --- a/plugins/reviewnotes +++ b/plugins/reviewnotes
@@ -1 +1 @@ -Subproject commit 7070ce225267cd024d0a2f06d0c03f8011774def +Subproject commit be803eb40fdcd5bfa11d9a808863585f86228e06
diff --git a/plugins/singleusergroup b/plugins/singleusergroup index 570b6e2..a4c9e7e 160000 --- a/plugins/singleusergroup +++ b/plugins/singleusergroup
@@ -1 +1 @@ -Subproject commit 570b6e287a74750d37d2a94e2cf66297c004dce4 +Subproject commit a4c9e7eb2e3165c981f518457e323d1154d6efc4
diff --git a/polygerrit-ui/.gitignore b/polygerrit-ui/.gitignore index b3b74a6..7f06bef 100644 --- a/polygerrit-ui/.gitignore +++ b/polygerrit-ui/.gitignore
@@ -4,3 +4,4 @@ fonts bower_components .tmp +.vscode \ No newline at end of file
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD index 1f11cde..d4d2322 100644 --- a/polygerrit-ui/BUILD +++ b/polygerrit-ui/BUILD
@@ -21,6 +21,7 @@ "//lib/js:moment", "//lib/js:page", "//lib/js:polymer", + "//lib/js:polymer-resin", "//lib/js:promise-polyfill", ], ) @@ -28,7 +29,7 @@ genrule2( name = "fonts", srcs = [ - "//lib/fonts:sourcecodepro", + "//lib/fonts:robotomono", ], outs = ["fonts.zip"], cmd = " && ".join([
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md index 90bc0ed..01f61d9 100644 --- a/polygerrit-ui/README.md +++ b/polygerrit-ui/README.md
@@ -70,9 +70,10 @@ that serves PolyGerrit: ```sh -bazel build polygerrit && \ -java -jar bazel-bin/polygerrit.war daemon --polygerrit-dev \ --d ../gerrit_testsite --console-log --show-stack-trace +bazel build polygerrit && + $(bazel info output_base)/external/local_jdk/bin/java \ + -jar bazel-bin/polygerrit.war daemon --polygerrit-dev \ + -d ../gerrit_testsite --console-log --show-stack-trace ``` ## Running Tests @@ -96,6 +97,11 @@ ./polygerrit-ui/app/run_test.sh ``` +To allow the tests to run in Safari: + +* In the Advanced preferences tab, check "Show Develop menu in menu bar". +* In the Develop menu, enable the "Allow Remote Automation" option. + If you need to pass additional arguments to `wct`: ```sh @@ -114,3 +120,45 @@ We follow the [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml) with a few exceptions. When in doubt, remain consistent with the code around you. + +In addition, we encourage the use of [ESLint](http://eslint.org/). +It is available as a command line utility, as well as a plugin for most editors +and IDEs. It, along with a few dependencies, can also be installed through NPM: + +```sh +sudo npm install -g eslint eslint-config-google eslint-plugin-html +``` + +`eslint-config-google` is a port of the Google JS Style Guide to an ESLint +config module, and `eslint-plugin-html` allows ESLint to lint scripts inside +HTML. +We have an .eslintrc.json config file in the polygerrit-ui/ directory configured +to enforce the preferred style of the PolyGerrit project. +After installing, you can use `eslint` on any new file you create. +In addition, you can supply the `--fix` flag to apply some suggested fixes for +simple style issues. +If you modify JS inside of `<script>` tags, like for test suites, you may have +to supply the `--ext .html` flag. + +Some useful commands: + +* To run ESLint on the whole app, less some dependency code: +`eslint --ignore-pattern 'bower_components/' --ignore-pattern 'gr-linked-text' --ignore-pattern 'scripts/vendor' --ext .html,.js polygerrit-ui/app` +* To run ESLint on just the subdirectory you modified: +`eslint --ext .html,.js polygerrit-ui/app/$YOUR_DIR_HERE` +* To run the linter on all of your local changes: +`git diff --name-only master | xargs eslint --ext .html,.js` + +We also use the polylint tool to lint use of Polymer. To install polylint, +execute the following command. + +```sh +npm install -g polylint +``` + +To run polylint, execute the following command. + +```sh +bazel test //polygerrit-ui/app:polylint_test +``` +
diff --git a/polygerrit-ui/app/.eslintrc.json b/polygerrit-ui/app/.eslintrc.json new file mode 100644 index 0000000..2f77b42 --- /dev/null +++ b/polygerrit-ui/app/.eslintrc.json
@@ -0,0 +1,62 @@ +{ + "extends": ["eslint:recommended", "google"], + "env": { + "browser": true, + "es6": true + }, + "globals": { + "__dirname": false, + "app": false, + "page": false, + "Polymer": false, + "process": false, + "require": false, + "Gerrit": false, + "Promise": false, + "assert": false, + "test": false, + "flushAsynchronousOperations": false + }, + "rules": { + "arrow-parens": ["error", "as-needed"], + "brace-style": ["error", "1tbs", { "allowSingleLine": true }], + "camelcase": "off", + "comma-dangle": ["error", "always-multiline"], + "eol-last": "off", + "indent": ["error", 2, { + "MemberExpression": 2, + "FunctionDeclaration": {"body": 1, "parameters": 2}, + "FunctionExpression": {"body": 1, "parameters": 2}, + "CallExpression": {"arguments": 2}, + "ArrayExpression": 1, + "ObjectExpression": 1, + "SwitchCase": 1 + }], + "keyword-spacing": ["error", { "after": true, "before": true }], + "max-len": [ + "error", + 80, + 2, + {"ignoreComments": true} + ], + "new-cap": ["error", { "capIsNewExceptions": ["Polymer"] }], + "no-console": "off", + "no-undef": "off", + "no-var": "error", + "object-shorthand": ["error", "always"], + "prefer-arrow-callback": "error", + "prefer-const": "error", + "prefer-spread": "error", + "quote-props": ["error", "consistent-as-needed"], + "require-jsdoc": "off", + "semi": [2, "always"], + "template-curly-spacing": "error", + "valid-jsdoc": "off" + }, + "plugins": [ + "html" + ], + "settings": { + "html/report-bad-indent": "error" + } +}
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD index abfb8f8..04a1d69 100644 --- a/polygerrit-ui/app/BUILD +++ b/polygerrit-ui/app/BUILD
@@ -2,6 +2,7 @@ default_visibility = ["//visibility:public"], ) +load(":rules.bzl", "polygerrit_bundle") load("//tools/bzl:genrule2.bzl", "genrule2") load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library", "closure_js_binary") load( @@ -12,8 +13,8 @@ "js_component", ) -vulcanize( - name = "gr-app", +polygerrit_bundle( + name = "polygerrit_ui", srcs = glob( [ "**/*.html", @@ -27,82 +28,7 @@ ], ), app = "elements/gr-app.html", - deps = ["//polygerrit-ui:polygerrit_components.bower_components"], -) - -closure_js_library( - name = "closure_lib", - srcs = ["gr-app.js"], - convention = "GOOGLE", - # TODO(davido): Clean up these issues: http://paste.openstack.org/show/608548 - # and remove this supression - suppress = ["JSC_UNUSED_LOCAL_ASSIGNMENT"], - deps = [ - "//lib/polymer_externs:polymer_closure", - "@io_bazel_rules_closure//closure/library", - ], -) - -closure_js_binary( - name = "closure_bin", - # Known issue: Closure compilation not compatible with Polymer behaviors. - # See: https://github.com/google/closure-compiler/issues/2042 - compilation_level = "WHITESPACE_ONLY", - defs = [ - "--polymer_pass", - "--jscomp_off=duplicate", - "--force_inject_library=es6_runtime", - ], - language = "ECMASCRIPT5", - deps = [":closure_lib"], -) - -filegroup( - name = "top_sources", - srcs = [ - "favicon.ico", - "index.html", - ], -) - -filegroup( - name = "css_sources", - srcs = glob(["styles/**/*.css"]), -) - -filegroup( - name = "app_sources", - srcs = [ - "closure_bin.js", - "gr-app.html", - ], -) - -genrule2( - name = "polygerrit_ui", - srcs = [ - "//lib/fonts:sourcecodepro", - "//lib/js:highlightjs_files", - ":top_sources", - ":css_sources", - ":app_sources", - # we extract from the zip, but depend on the component for license checking. - "@webcomponentsjs//:zipfile", - "//lib/js:webcomponentsjs", - ], outs = ["polygerrit_ui.zip"], - cmd = " && ".join([ - "mkdir -p $$TMP/polygerrit_ui/{styles,fonts,bower_components/{highlightjs,webcomponentsjs},elements}", - "for f in $(locations :app_sources); do ext=$${f##*.}; cp -p $$f $$TMP/polygerrit_ui/elements/gr-app.$$ext; done", - "cp $(locations //lib/fonts:sourcecodepro) $$TMP/polygerrit_ui/fonts/", - "for f in $(locations :top_sources); do cp $$f $$TMP/polygerrit_ui/; done", - "for f in $(locations :css_sources); do cp $$f $$TMP/polygerrit_ui/styles; done", - "for f in $(locations //lib/js:highlightjs_files); do cp $$f $$TMP/polygerrit_ui/bower_components/highlightjs/ ; done", - "unzip -qd $$TMP/polygerrit_ui/bower_components $(location @webcomponentsjs//:zipfile) webcomponentsjs/webcomponents-lite.js", - "cd $$TMP", - "find . -exec touch -t 198001010000 '{}' ';'", - "zip -qr $$ROOT/$@ *", - ]), ) bower_component_bundle( @@ -129,6 +55,16 @@ ), ) +filegroup( + name = "bower_components", + srcs = glob( + [ + "bower_components/**/*.html", + "bower_components/**/*.js", + ], + ), +) + genrule2( name = "pg_code_zip", srcs = [":pg_code"], @@ -147,6 +83,7 @@ size = "large", srcs = ["wct_test.sh"], data = [ + "test/common-test-setup.html", "test/index.html", ":pg_code.zip", ":test_components.zip", @@ -157,3 +94,79 @@ "manual", ], ) + +sh_test( + name = "lint_test", + size = "large", + srcs = ["lint_test.sh"], + data = [ + ".eslintrc.json", + ":pg_code", + ], + # Should not run sandboxed. + tags = [ + "local", + "manual", + ], +) + +sh_test( + name = "polylint_test", + size = "large", + srcs = ["polylint_test.sh"], + data = [ + ":pg_code", + "//polygerrit-ui:polygerrit_components.bower_components.zip", + ], + # Should not run sandboxed. + tags = [ + "local", + "manual", + ], +) + +# Embed bundle +polygerrit_bundle( + name = "polygerrit_embed_ui", + srcs = glob( + [ + "**/*.html", + "**/*.js", + ], + exclude = [ + "bower_components/**", + "index.html", + "test/**", + "**/*_test.html", + ], + ), + app = "embed/change-diff-views.html", + outs = ["polygerrit_embed_ui.zip"], +) + +filegroup( + name = "embed_test_files", + srcs = glob( + [ + "embed/**/*_test.html", + ], + ), +) + +sh_test( + name = "embed_test", + size = "large", + srcs = ["embed_test.sh"], + data = [ + "test/common-test-setup.html", + "embed/test.html", + ":embed_test_files", + ":polygerrit_embed_ui.zip", + ":test_components.zip", + ], + # Should not run sandboxed. + tags = [ + "local", + "manual", + ], +)
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html index 2ec8538..2174fc6 100644 --- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html +++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html
@@ -19,14 +19,14 @@ 'use strict'; /** @polymerBehavior Gerrit.BaseUrlBehavior */ - var BaseUrlBehavior = { - getBaseUrl: function() { + const BaseUrlBehavior = { + getBaseUrl() { return window.CANONICAL_PATH || ''; }, - computeGwtUrl: function(path) { - var base = this.getBaseUrl(); - var clientPath = path.substring(base.length); + computeGwtUrl(path) { + const base = this.getBaseUrl(); + const clientPath = path.substring(base.length); return base + '/?polygerrit=0#' + clientPath; }, };
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html index 1e277bc..99efede 100644 --- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html +++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
@@ -20,9 +20,9 @@ <script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script> <script src="../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../test/common-test-setup.html"/> <script> + /** @type {String} */ window.CANONICAL_PATH = '/r'; </script> <link rel="import" href="base-url-behavior.html"> @@ -42,11 +42,12 @@ </test-fixture> <script> - suite('base-url-behavior tests', function() { - var element; - var overlay; + suite('base-url-behavior tests', () => { + let element; + // eslint-disable-next-line no-unused-vars + let overlay; - suiteSetup(function() { + suiteSetup(() => { // Define a Polymer element that uses this behavior. Polymer({ is: 'test-element', @@ -56,20 +57,20 @@ }); }); - setup(function() { + setup(() => { element = fixture('basic'); overlay = fixture('within-overlay'); }); - test('getBaseUrl', function() { + test('getBaseUrl', () => { assert.deepEqual(element.getBaseUrl(), '/r'); }); - test('computeGwtUrl', function() { + test('computeGwtUrl', () => { assert.deepEqual( - element.computeGwtUrl('/r/c/1/'), - '/r/?polygerrit=0#/c/1/' + element.computeGwtUrl('/r/c/1/'), + '/r/?polygerrit=0#/c/1/' ); }); }); -</script> \ No newline at end of file +</script>
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html index ca955b3..178dd7c 100644 --- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior.html
@@ -18,7 +18,7 @@ 'use strict'; /** @polymerBehavior Gerrit.ChangeTableBehavior */ - var ChangeTableBehavior = { + const ChangeTableBehavior = { properties: { columnNames: { type: Array, @@ -32,21 +32,21 @@ 'Size', ], readOnly: true, - } + }, }, /** * Returns the complement to the given column array * @param {Array} columns */ - getComplementColumns: function(columns) { - return this.columnNames.filter(function(column) { - return columns.indexOf(column) === -1; + getComplementColumns(columns) { + return this.columnNames.filter(column => { + return !columns.includes(column); }); }, - isColumnHidden: function(columnToCheck, columnsToDisplay) { - return columnsToDisplay.indexOf(columnToCheck) === -1; + isColumnHidden(columnToCheck, columnsToDisplay) { + return !columnsToDisplay.includes(columnToCheck); }, };
diff --git a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html index 42ea615..19837cc 100644 --- a/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-change-table-behavior/gr-change-table-behavior_test.html
@@ -20,8 +20,7 @@ <script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script> <script src="../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../test/common-test-setup.html"/> <link rel="import" href="gr-change-table-behavior.html"> <test-fixture id="basic"> @@ -39,11 +38,12 @@ </test-fixture> <script> - suite('gr-change-table-behavior tests', function() { - var element; - var overlay; + suite('gr-change-table-behavior tests', () => { + let element; + // eslint-disable-next-line no-unused-vars + let overlay; - suiteSetup(function() { + suiteSetup(() => { // Define a Polymer element that uses this behavior. Polymer({ is: 'test-element', @@ -51,13 +51,13 @@ }); }); - setup(function() { + setup(() => { element = fixture('basic'); overlay = fixture('within-overlay'); }); - test('getComplementColumns', function() { - var columns = [ + test('getComplementColumns', () => { + let columns = [ 'Subject', 'Status', 'Owner', @@ -79,9 +79,9 @@ ['Owner', 'Updated']); }); - test('isColumnHidden', function() { - var columnToCheck = 'Project'; - var columnsToDisplay = [ + test('isColumnHidden', () => { + const columnToCheck = 'Project'; + let columnsToDisplay = [ 'Subject', 'Status', 'Owner', @@ -92,7 +92,7 @@ ]; assert.isFalse(element.isColumnHidden(columnToCheck, columnsToDisplay)); - var columnsToDisplay = [ + columnsToDisplay = [ 'Subject', 'Status', 'Owner',
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.html b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.html new file mode 100644 index 0000000..f2b12ee --- /dev/null +++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior.html
@@ -0,0 +1,55 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<link rel="import" href="../base-url-behavior/base-url-behavior.html"> +<link rel="import" href="../gr-url-encoding-behavior.html"> +<script> +(function(window) { + 'use strict'; + + /** @polymerBehavior Gerrit.ListViewBehavior */ + const ListViewBehavior = { + computeLoadingClass(loading) { + return loading ? 'loading' : ''; + }, + + computeShownItems(items) { + return items.slice(0, 25); + }, + + getUrl(path, item) { + return this.getBaseUrl() + path + this.encodeURL(item, true); + }, + + getFilterValue(params) { + if (!params) { return null; } + return this._filter = params.filter || null; + }, + + getOffsetValue(params) { + if (params && params.offset) { + return params.offset; + } + return 0; + }, + }; + + window.Gerrit = window.Gerrit || {}; + window.Gerrit.ListViewBehavior = [ + ListViewBehavior, + window.Gerrit.BaseUrlBehavior, + window.Gerrit.URLEncodingBehavior]; +})(window); +</script>
diff --git a/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html new file mode 100644 index 0000000..599f691 --- /dev/null +++ b/polygerrit-ui/app/behaviors/gr-list-view-behavior/gr-list-view-behavior_test.html
@@ -0,0 +1,89 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>keyboard-shortcut-behavior</title> + +<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../test/common-test-setup.html"/> +<link rel="import" href="gr-list-view-behavior.html"> + +<test-fixture id="basic"> + <template> + <test-element></test-element> + </template> +</test-fixture> + +<script> + suite('gr-list-view-behavior tests', () => { + let element; + // eslint-disable-next-line no-unused-vars + let overlay; + + suiteSetup(() => { + // Define a Polymer element that uses this behavior. + Polymer({ + is: 'test-element', + behaviors: [Gerrit.ListViewBehavior], + }); + }); + + setup(() => { + element = fixture('basic'); + }); + + test('computeLoadingClass', () => { + assert.equal(element.computeLoadingClass(true), 'loading'); + assert.equal(element.computeLoadingClass(false), ''); + }); + + test('computeShownItems', () => { + const myArr = new Array(26); + assert.equal(element.computeShownItems(myArr).length, 25); + }); + + test('getUrl', () => { + assert.equal(element.getUrl('/path/to/something/', 'item'), + '/path/to/something/item'); + assert.equal(element.getUrl('/path/to/something/', 'item%test'), + '/path/to/something/item%2525test'); + }); + + test('getFilterValue', () => { + let params; + assert.equal(element.getFilterValue(params), null); + + params = {filter: null}; + assert.equal(element.getFilterValue(params), null); + + params = {filter: 'test'}; + assert.equal(element.getFilterValue(params), 'test'); + }); + + test('getOffsetValue', () => { + let params; + assert.equal(element.getOffsetValue(params), 0); + + params = {offset: null}; + assert.equal(element.getOffsetValue(params), 0); + + params = {offset: 1}; + assert.equal(element.getOffsetValue(params), 1); + }); + }); +</script>
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html index acf3a62..b0f40a9d 100644 --- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -17,8 +17,19 @@ (function(window) { 'use strict'; + // Tags identifying ChangeMessages that move change into WIP state. + const WIP_TAGS = [ + 'autogenerated:gerrit:newWipPatchSet', + 'autogenerated:gerrit:setWorkInProgress', + ]; + + // Tags identifying ChangeMessages that move change out of WIP state. + const READY_TAGS = [ + 'autogenerated:gerrit:setReadyForReview', + ]; + /** @polymerBehavior Gerrit.PatchSetBehavior */ - var PatchSetBehavior = { + const PatchSetBehavior = { /** * Given an object of revisions, get a particular revision based on patch * num. @@ -27,15 +38,100 @@ * @param {number|string} patchNum The number index of the revision * @return {Object} The correspondent revision obj from {revisions} */ - getRevisionByPatchNum: function(revisions, patchNum) { + getRevisionByPatchNum(revisions, patchNum) { patchNum = parseInt(patchNum, 10); - for (var rev in revisions) { + for (const rev in revisions) { if (revisions.hasOwnProperty(rev) && revisions[rev]._number === patchNum) { return revisions[rev]; } } }, + + /** + * Construct a chronological list of patch sets derived from change details. + * Each element of this list is an object with the following properties: + * + * * num {number} The number identifying the patch set + * * desc {!string} Optional patch set description + * * wip {boolean} If true, this patch set was never subject to review. + * + * The wip property is determined by the change's current work_in_progress + * property and its log of change messages. + * + * @param {Object} change The change details + * @return {Array<Object>} Sorted list of patch set objects, as described + * above + */ + computeAllPatchSets(change) { + if (!change) { return []; } + const patchNums = []; + for (const commit in change.revisions) { + if (change.revisions.hasOwnProperty(commit)) { + patchNums.push({ + num: change.revisions[commit]._number, + desc: change.revisions[commit].description, + }); + } + } + patchNums.sort((a, b) => { return a.num - b.num; }); + return this._computeWipForPatchSets(change, patchNums); + }, + + /** + * Populate the wip properties of the given list of patch sets. + * + * @param {Object} change The change details + * @param {Array<Object>} patchNums Sorted list of patch set objects, as + * generated by computeAllPatchSets + * @return {Array<Object>} The given list of patch set objects, with the + * wip property set on each of them + */ + _computeWipForPatchSets(change, patchNums) { + if (!change.messages || !change.messages.length) { + return patchNums; + } + const psWip = {}; + let wip = change.work_in_progress; + for (let i = 0; i < change.messages.length; i++) { + const msg = change.messages[i]; + if (WIP_TAGS.includes(msg.tag)) { + wip = true; + } else if (READY_TAGS.includes(msg.tag)) { + wip = false; + } + if (psWip[msg._revision_number] !== false) { + psWip[msg._revision_number] = wip; + } + } + + for (let i = 0; i < patchNums.length; i++) { + patchNums[i].wip = psWip[patchNums[i].num]; + } + return patchNums; + }, + + computeLatestPatchNum(allPatchSets) { + if (!allPatchSets || !allPatchSets.length) { return undefined; } + return allPatchSets[allPatchSets.length - 1].num; + }, + + /** + * Check whether there is no newer patch than the latest patch that was + * available when this change was loaded. + * @return {Promise} A promise that yields true if the latest patch has been + * loaded, and false if a newer patch has been uploaded in the meantime. + */ + fetchIsLatestKnown(change, restAPI) { + const knownLatest = this.computeLatestPatchNum( + this.computeAllPatchSets(change)); + return restAPI.getChangeDetail(change._number) + .then(detail => { + const actualLatest = this.computeLatestPatchNum( + this.computeAllPatchSets(detail)); + return actualLatest <= knownLatest; + }); + }, }; window.Gerrit = window.Gerrit || {};
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html index 862c734..0cf7bb4 100644 --- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -16,16 +16,16 @@ <!-- Polymer included for the html import polyfill. --> <script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script> <script src="../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../test/common-test-setup.html"/> <title>gr-patch-set-behavior</title> -<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-patch-set-behavior.html"> <script> - suite('gr-path-list-behavior tests', function() { - test('getRevisionByPatchNum', function() { - var get = Gerrit.PatchSetBehavior.getRevisionByPatchNum; - var revisions = [ + suite('gr-path-list-behavior tests', () => { + test('getRevisionByPatchNum', () => { + const get = Gerrit.PatchSetBehavior.getRevisionByPatchNum; + const revisions = [ {_number: 0}, {_number: 1}, {_number: 2}, @@ -34,5 +34,132 @@ assert.deepEqual(get(revisions, 2), revisions[2]); assert.equal(get(revisions, '3'), undefined); }); + + test('fetchIsLatestKnown on latest', done => { + const knownChange = { + revisions: { + sha1: {description: 'patch 1', _number: 1}, + sha2: {description: 'patch 2', _number: 2}, + }, + }; + const mockRestApi = { + getChangeDetail() { + return Promise.resolve(knownChange); + }, + }; + Gerrit.PatchSetBehavior.fetchIsLatestKnown(knownChange, mockRestApi) + .then(isLatest => { + assert.isTrue(isLatest); + done(); + }); + }); + + test('fetchIsLatestKnown not on latest', done => { + const knownChange = { + revisions: { + sha1: {description: 'patch 1', _number: 1}, + sha2: {description: 'patch 2', _number: 2}, + }, + }; + const actualChange = { + revisions: { + sha1: {description: 'patch 1', _number: 1}, + sha2: {description: 'patch 2', _number: 2}, + sha3: {description: 'patch 3', _number: 3}, + }, + }; + const mockRestApi = { + getChangeDetail() { + return Promise.resolve(actualChange); + }, + }; + Gerrit.PatchSetBehavior.fetchIsLatestKnown(knownChange, mockRestApi) + .then(isLatest => { + assert.isFalse(isLatest); + done(); + }); + }); + + test('_computeWipForPatchSets', () => { + // Compute patch sets for a given timeline on a change. The initial WIP + // property of the change can be true or false. The map of tags by + // revision is keyed by patch set number. Each value is a list of change + // message tags in the order that they occurred in the timeline. These + // indicate actions that modify the WIP property of the change and/or + // create new patch sets. + // + // Returns the actual results with an assertWip method that can be used + // to compare against an expected value for a particular patch set. + const compute = (initialWip, tagsByRevision) => { + const change = { + messages: [], + work_in_progress: initialWip, + }; + const revs = Object.keys(tagsByRevision).sort((a, b) => { + return a - b; + }); + for (const rev of revs) { + for (const tag of tagsByRevision[rev]) { + change.messages.push({ + tag, + _revision_number: rev, + }); + } + } + let patchNums = revs.map(rev => { return {num: rev}; }); + patchNums = Gerrit.PatchSetBehavior._computeWipForPatchSets( + change, patchNums); + const actualWipsByRevision = {}; + for (const patchNum of patchNums) { + actualWipsByRevision[patchNum.num] = patchNum.wip; + } + const verifier = { + assertWip(revision, expectedWip) { + const patchNum = patchNums.find(patchNum => { + return patchNum.num == revision; + }); + if (!patchNum) { + assert.fail('revision ' + revision + ' not found'); + } + assert.equal(patchNum.wip, expectedWip, + 'wip state for ' + revision + ' is ' + + patchNum.wip + '; expected ' + expectedWip); + return verifier; + }, + }; + return verifier; + }; + + compute(false, {1: ['upload']}).assertWip(1, false); + compute(true, {1: ['upload']}).assertWip(1, true); + + const setWip = 'autogenerated:gerrit:setWorkInProgress'; + const uploadInWip = 'autogenerated:gerrit:newWipPatchSet'; + const clearWip = 'autogenerated:gerrit:setReadyForReview'; + + compute(false, { + 1: ['upload', setWip], + 2: ['upload'], + 3: ['upload', clearWip], + 4: ['upload', setWip], + }).assertWip(1, false) // Change was created with PS1 ready for review + .assertWip(2, true) // PS2 was uploaded during WIP + .assertWip(3, false) // PS3 was marked ready for review after upload + .assertWip(4, false); // PS4 was uploaded ready for review + + compute(false, { + 1: [uploadInWip, null, 'addReviewer'], + 2: ['upload'], + 3: ['upload', clearWip, setWip], + 4: ['upload'], + 5: ['upload', clearWip], + 6: [uploadInWip], + }).assertWip(1, true) // Change was created in WIP + .assertWip(2, true) // PS2 was uploaded during WIP + .assertWip(3, false) // PS3 was marked ready for review + .assertWip(4, true) // PS4 was uploaded during WIP + .assertWip(5, false) // PS5 was marked ready for review + .assertWip(6, true); // PS6 was uploaded with WIP option + }); }); </script>
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html index f9c4a80..2ccae2c 100644 --- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
@@ -18,10 +18,10 @@ 'use strict'; /** @polymerBehavior Gerrit.PathListBehavior */ - var PathListBehavior = { - specialFilePathCompare: function(a, b) { + const PathListBehavior = { + specialFilePathCompare(a, b) { // The commit message always goes first. - var COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; + const COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; if (a === COMMIT_MESSAGE_PATH) { return -1; } @@ -30,7 +30,7 @@ } // The merge list always comes next. - var MERGE_LIST_PATH = '/MERGE_LIST'; + const MERGE_LIST_PATH = '/MERGE_LIST'; if (a === MERGE_LIST_PATH) { return -1; } @@ -38,25 +38,24 @@ return 1; } - var aLastDotIndex = a.lastIndexOf('.'); - var aExt = a.substr(aLastDotIndex + 1); - var aFile = a.substr(0, aLastDotIndex) || a; + const aLastDotIndex = a.lastIndexOf('.'); + const aExt = a.substr(aLastDotIndex + 1); + const aFile = a.substr(0, aLastDotIndex) || a; - var bLastDotIndex = b.lastIndexOf('.'); - var bExt = b.substr(bLastDotIndex + 1); - var bFile = b.substr(0, bLastDotIndex) || b; + const bLastDotIndex = b.lastIndexOf('.'); + const bExt = b.substr(bLastDotIndex + 1); + const bFile = b.substr(0, bLastDotIndex) || b; // Sort header files above others with the same base name. - var headerExts = ['h', 'hxx', 'hpp']; + const headerExts = ['h', 'hxx', 'hpp']; if (aFile.length > 0 && aFile === bFile) { - if (headerExts.indexOf(aExt) !== -1 && - headerExts.indexOf(bExt) !== -1) { + if (headerExts.includes(aExt) && headerExts.includes(bExt)) { return a.localeCompare(b); } - if (headerExts.indexOf(aExt) !== -1) { + if (headerExts.includes(aExt)) { return -1; } - if (headerExts.indexOf(bExt) !== -1) { + if (headerExts.includes(bExt)) { return 1; } }
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html index 2b42587..37772d1 100644 --- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -16,25 +16,33 @@ <!-- Polymer included for the html import polyfill. --> <script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script> <script src="../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../test/common-test-setup.html"/> <title>gr-path-list-behavior</title> -<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-path-list-behavior.html"> <script> - suite('gr-path-list-behavior tests', function() { - test('special sort', function() { - var sort = Gerrit.PathListBehavior.specialFilePathCompare; - var testFiles = [ + suite('gr-path-list-behavior tests', () => { + test('special sort', () => { + const sort = Gerrit.PathListBehavior.specialFilePathCompare; + const testFiles = [ '/a.h', '/MERGE_LIST', '/a.cpp', '/COMMIT_MSG', '/asdasd', - '/mrPeanutbutter.py' + '/mrPeanutbutter.py', ]; - assert.deepEqual(testFiles.sort(sort), - ['/COMMIT_MSG', '/MERGE_LIST', '/a.h', '/a.cpp', '/asdasd', '/mrPeanutbutter.py']); + assert.deepEqual( + testFiles.sort(sort), + [ + '/COMMIT_MSG', + '/MERGE_LIST', + '/a.h', + '/a.cpp', + '/asdasd', + '/mrPeanutbutter.py', + ]); }); }); </script>
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html index 6b35328..3e9e19e 100644 --- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html
@@ -15,4 +15,6 @@ --> <link rel="import" href="../../bower_components/polymer/polymer.html"> <link rel="import" href="../../elements/shared/gr-tooltip/gr-tooltip.html"> +<script src="../../scripts/rootElement.js"></script> + <script src="gr-tooltip-behavior.js"></script>
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js index e4c4d11..54a59ca 100644 --- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js +++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -14,40 +14,47 @@ (function(window) { 'use strict'; - var BOTTOM_OFFSET = 7.2; // Height of the arrow in tooltip. + const BOTTOM_OFFSET = 7.2; // Height of the arrow in tooltip. /** @polymerBehavior Gerrit.TooltipBehavior */ - var TooltipBehavior = { + const TooltipBehavior = { properties: { - hasTooltip: Boolean, + hasTooltip: { + type: Boolean, + observer: '_setupTooltipListeners', + }, _isTouchDevice: { type: Boolean, - value: function() { + value() { return 'ontouchstart' in document.documentElement; }, }, _tooltip: Element, _titleText: String, + _hasSetupTooltipListeners: { + type: Boolean, + value: false, + }, }, - attached: function() { - if (!this.hasTooltip) { return; } - - this.addEventListener('mouseenter', this._handleShowTooltip.bind(this)); - this.addEventListener('mouseleave', this._handleHideTooltip.bind(this)); - this.addEventListener('tap', this._handleHideTooltip.bind(this)); - - this.listen(window, 'scroll', '_handleWindowScroll'); - }, - - detached: function() { + detached() { this._handleHideTooltip(); this.unlisten(window, 'scroll', '_handleWindowScroll'); }, - _handleShowTooltip: function(e) { + _setupTooltipListeners() { + if (this._hasSetupTooltipListeners || !this.hasTooltip) { return; } + this._hasSetupTooltipListeners = true; + + this.addEventListener('mouseenter', this._handleShowTooltip.bind(this)); + this.addEventListener('mouseleave', this._handleHideTooltip.bind(this)); + this.addEventListener('tap', this._handleHideTooltip.bind(this)); + this.listen(window, 'scroll', '_handleWindowScroll'); + }, + + _handleShowTooltip(e) { if (this._isTouchDevice) { return; } if (!this.hasAttribute('title') || @@ -61,21 +68,21 @@ this._titleText = this.getAttribute('title'); this.setAttribute('title', ''); - var tooltip = document.createElement('gr-tooltip'); + const tooltip = document.createElement('gr-tooltip'); tooltip.text = this._titleText; tooltip.maxWidth = this.getAttribute('max-width'); // 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'; - Polymer.dom(document.body).appendChild(tooltip); + Gerrit.getRootElement().appendChild(tooltip); this._positionTooltip(tooltip); tooltip.style.visibility = null; this._tooltip = tooltip; }, - _handleHideTooltip: function(e) { + _handleHideTooltip(e) { if (this._isTouchDevice) { return; } if (!this.hasAttribute('title') || this._titleText == null) { @@ -89,19 +96,20 @@ this._tooltip = null; }, - _handleWindowScroll: function(e) { + _handleWindowScroll(e) { if (!this._tooltip) { return; } this._positionTooltip(this._tooltip); }, - _positionTooltip: function(tooltip) { - var rect = this.getBoundingClientRect(); - var boxRect = tooltip.getBoundingClientRect(); - var parentRect = tooltip.parentElement.getBoundingClientRect(); - var top = rect.top - parentRect.top; - var left = rect.left - parentRect.left + (rect.width - boxRect.width) / 2; - var right = parentRect.width - left - boxRect.width; + _positionTooltip(tooltip) { + const rect = this.getBoundingClientRect(); + const boxRect = tooltip.getBoundingClientRect(); + 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.updateStyles({ '--gr-tooltip-arrow-center-offset': left + 'px',
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html index 99bfc03..f442c43 100644 --- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html +++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior_test.html
@@ -19,8 +19,7 @@ <script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script> <script src="../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../test/common-test-setup.html"/> <link rel="import" href="gr-tooltip-behavior.html"> <script>void(0);</script> @@ -32,22 +31,22 @@ </test-fixture> <script> - suite('gr-tooltip-behavior tests', function() { - var element; - var sandbox; + suite('gr-tooltip-behavior tests', () => { + let element; + let sandbox; function makeTooltip(tooltipRect, parentRect) { return { - getBoundingClientRect: function() { return tooltipRect; }, + getBoundingClientRect() { return tooltipRect; }, updateStyles: sinon.stub(), style: {left: 0, top: 0}, parentElement: { - getBoundingClientRect: function() { return parentRect; }, + getBoundingClientRect() { return parentRect; }, }, }; } - suiteSetup(function() { + suiteSetup(() => { // Define a Polymer element that uses this behavior. Polymer({ is: 'tooltip-behavior-element', @@ -55,20 +54,20 @@ }); }); - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); element = fixture('basic'); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('normal position', function() { - sandbox.stub(element, 'getBoundingClientRect', function() { + test('normal position', () => { + sandbox.stub(element, 'getBoundingClientRect', () => { return {top: 100, left: 100, width: 200}; }); - var tooltip = makeTooltip( + const tooltip = makeTooltip( {height: 30, width: 50}, {top: 0, left: 0, width: 1000}); @@ -78,45 +77,51 @@ assert.equal(tooltip.style.top, '100px'); }); - test('left side position', function() { - sandbox.stub(element, 'getBoundingClientRect', function() { + test('left side position', () => { + sandbox.stub(element, 'getBoundingClientRect', () => { return {top: 100, left: 10, width: 50}; }); - var tooltip = makeTooltip( + const tooltip = makeTooltip( {height: 30, width: 120}, {top: 0, left: 0, width: 1000}); element._positionTooltip(tooltip); assert.isTrue(tooltip.updateStyles.called); - var offset = tooltip.updateStyles + const offset = tooltip.updateStyles .lastCall.args[0]['--gr-tooltip-arrow-center-offset']; assert.isBelow(parseFloat(offset.replace(/px$/, '')), 0); assert.equal(tooltip.style.left, '0px'); assert.equal(tooltip.style.top, '100px'); }); - test('right side position', function() { - sandbox.stub(element, 'getBoundingClientRect', function() { + test('right side position', () => { + sandbox.stub(element, 'getBoundingClientRect', () => { return {top: 100, left: 950, width: 50}; }); - var tooltip = makeTooltip( + const tooltip = makeTooltip( {height: 30, width: 120}, {top: 0, left: 0, width: 1000}); element._positionTooltip(tooltip); assert.isTrue(tooltip.updateStyles.called); - var offset = tooltip.updateStyles + const offset = tooltip.updateStyles .lastCall.args[0]['--gr-tooltip-arrow-center-offset']; assert.isAbove(parseFloat(offset.replace(/px$/, '')), 0); assert.equal(tooltip.style.left, '915px'); assert.equal(tooltip.style.top, '100px'); }); - test('hides tooltip when detached', function() { + test('hides tooltip when detached', () => { sandbox.stub(element, '_handleHideTooltip'); element.remove(); flushAsynchronousOperations(); assert.isTrue(element._handleHideTooltip.called); }); + + test('sets up listeners when has-tooltip is changed', () => { + const addListenerStub = sandbox.stub(element, 'addEventListener'); + element.hasTooltip = true; + assert.isTrue(addListenerStub.called); + }); }); </script>
diff --git a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior.html b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior.html index b7d71fc..25d9c27 100644 --- a/polygerrit-ui/app/behaviors/gr-url-encoding-behavior.html +++ b/polygerrit-ui/app/behaviors/gr-url-encoding-behavior.html
@@ -18,14 +18,14 @@ 'use strict'; /** @polymerBehavior Gerrit.URLEncodingBehavior */ - var URLEncodingBehavior = { + const URLEncodingBehavior = { /** * Pretty-encodes a URL. Double-encodes the string, and then replaces * benevolent characters for legibility. */ - encodeURL: function(url, replaceSlashes) { + encodeURL(url, replaceSlashes) { // @see Issue 4255 regarding double-encoding. - var output = encodeURIComponent(encodeURIComponent(url)); + let output = encodeURIComponent(encodeURIComponent(url)); // @see Issue 4577 regarding more readable URLs. output = output.replace(/%253A/g, ':'); output = output.replace(/%2520/g, '+');
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html index 3d99cec..ca94495 100644 --- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html +++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html
@@ -20,28 +20,46 @@ (function(window) { 'use strict'; - var getKeyboardEvent = function(e) { - return Polymer.dom(e.detail ? e.detail.keyboardEvent : e); + // Must be declared outside behavior implementation to be accessed inside + // behavior functions. + const getKeyboardEvent = function(e) { + e = Polymer.dom(e.detail ? e.detail.keyboardEvent : e); + // When e is a keyboardEvent, e.event is not null. + if (e.event) { e = e.event; } + return e; }; - var KeyboardShortcutBehaviorImpl = { - modifierPressed: function(e) { + /** @polymerBehavior KeyboardShortcutBehaviorImpl */ + const KeyboardShortcutBehaviorImpl = { + modifierPressed(e) { e = getKeyboardEvent(e); - // When e is a keyboardEvent, e.event is not null. - if (e.event) { e = e.event; } return e.altKey || e.ctrlKey || e.metaKey || e.shiftKey; }, - shouldSuppressKeyboardShortcut: function(e) { + isModifierPressed(e, modifier) { + return getKeyboardEvent(e)[modifier]; + }, + + shouldSuppressKeyboardShortcut(e) { e = getKeyboardEvent(e); - if (e.path[0].tagName === 'INPUT' || e.path[0].tagName === 'TEXTAREA') { + const tagName = Polymer.dom(event).rootTarget.tagName; + if (tagName === 'INPUT' || tagName === 'TEXTAREA') { return true; } - for (var i = 0; i < e.path.length; i++) { + for (let i = 0; e.path && i < e.path.length; i++) { if (e.path[i].tagName === 'GR-OVERLAY') { return true; } } return false; }, + + // Alias for getKeyboardEvent. + getKeyboardEvent(e) { + return getKeyboardEvent(e); + }, + + getRootTarget(e) { + return Polymer.dom(getKeyboardEvent(e)).rootTarget; + }, }; window.Gerrit = window.Gerrit || {};
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html index 9ede5d9..6906ea2 100644 --- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html +++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html
@@ -20,8 +20,7 @@ <script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script> <script src="../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../test/common-test-setup.html"/> <link rel="import" href="keyboard-shortcut-behavior.html"> <test-fixture id="basic"> @@ -39,77 +38,77 @@ </test-fixture> <script> - suite('keyboard-shortcut-behavior tests', function() { - var element; - var overlay; - var sandbox; + suite('keyboard-shortcut-behavior tests', () => { + let element; + let overlay; + let sandbox; - suiteSetup(function() { + suiteSetup(() => { // Define a Polymer element that uses this behavior. Polymer({ is: 'test-element', behaviors: [Gerrit.KeyboardShortcutBehavior], keyBindings: { - 'k': '_handleKey' + k: '_handleKey', }, - _handleKey: function() {}, + _handleKey() {}, }); }); - setup(function() { + setup(() => { element = fixture('basic'); overlay = fixture('within-overlay'); sandbox = sinon.sandbox.create(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('doesn’t block kb shortcuts for non-whitelisted els', function(done) { - var divEl = document.createElement('div'); + test('doesn’t block kb shortcuts for non-whitelisted els', done => { + const divEl = document.createElement('div'); element.appendChild(divEl); - element._handleKey = function(e) { + element._handleKey = e => { assert.isFalse(element.shouldSuppressKeyboardShortcut(e)); done(); }; MockInteractions.keyDownOn(divEl, 75, null, 'k'); }); - test('blocks kb shortcuts for input els', function(done) { - var inputEl = document.createElement('input'); + test('blocks kb shortcuts for input els', done => { + const inputEl = document.createElement('input'); element.appendChild(inputEl); - element._handleKey = function(e) { + element._handleKey = e => { assert.isTrue(element.shouldSuppressKeyboardShortcut(e)); done(); }; MockInteractions.keyDownOn(inputEl, 75, null, 'k'); }); - test('blocks kb shortcuts for textarea els', function(done) { - var textareaEl = document.createElement('textarea'); + test('blocks kb shortcuts for textarea els', done => { + const textareaEl = document.createElement('textarea'); element.appendChild(textareaEl); - element._handleKey = function(e) { + element._handleKey = e => { assert.isTrue(element.shouldSuppressKeyboardShortcut(e)); done(); }; MockInteractions.keyDownOn(textareaEl, 75, null, 'k'); }); - test('blocks kb shortcuts for anything in a gr-overlay', function(done) { - var divEl = document.createElement('div'); - var element = overlay.querySelector('test-element'); + test('blocks kb shortcuts for anything in a gr-overlay', done => { + const divEl = document.createElement('div'); + const element = overlay.querySelector('test-element'); element.appendChild(divEl); - element._handleKey = function(e) { + element._handleKey = e => { assert.isTrue(element.shouldSuppressKeyboardShortcut(e)); done(); }; MockInteractions.keyDownOn(divEl, 75, null, 'k'); }); - test('modifierPressed returns accurate values', function() { - var spy = sandbox.spy(element, 'modifierPressed'); - element._handleKey = function(e) { + test('modifierPressed returns accurate values', () => { + const spy = sandbox.spy(element, 'modifierPressed'); + element._handleKey = e => { element.modifierPressed(e); }; MockInteractions.keyDownOn(element, 75, 'shift', 'k'); @@ -127,5 +126,26 @@ MockInteractions.keyDownOn(element, 75, 'alt', 'k'); assert.isTrue(spy.lastCall.returnValue); }); + + test('isModifierPressed returns accurate value', () => { + const spy = sandbox.spy(element, 'isModifierPressed'); + element._handleKey = e => { + element.isModifierPressed(e, 'shiftKey'); + }; + MockInteractions.keyDownOn(element, 75, 'shift', 'k'); + assert.isTrue(spy.lastCall.returnValue); + MockInteractions.keyDownOn(element, 75, null, 'k'); + assert.isFalse(spy.lastCall.returnValue); + MockInteractions.keyDownOn(element, 75, 'ctrl', 'k'); + assert.isFalse(spy.lastCall.returnValue); + MockInteractions.keyDownOn(element, 75, null, 'k'); + assert.isFalse(spy.lastCall.returnValue); + MockInteractions.keyDownOn(element, 75, 'meta', 'k'); + assert.isFalse(spy.lastCall.returnValue); + MockInteractions.keyDownOn(element, 75, null, 'k'); + assert.isFalse(spy.lastCall.returnValue); + MockInteractions.keyDownOn(element, 75, 'alt', 'k'); + assert.isFalse(spy.lastCall.returnValue); + }); }); </script>
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html index f71fe8f..3764628 100644 --- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html +++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
@@ -20,7 +20,7 @@ 'use strict'; /** @polymerBehavior Gerrit.RESTClientBehavior */ - var RESTClientBehavior = { + const RESTClientBehavior = { ChangeDiffType: { ADDED: 'ADDED', COPIED: 'COPIED', @@ -88,35 +88,35 @@ REVIEWER_UPDATES: 19, // Set the submittable boolean. - SUBMITTABLE: 20 + SUBMITTABLE: 20, }, - listChangesOptionsToHex: function() { - var v = 0; - for (var i = 0; i < arguments.length; i++) { - v |= 1 << arguments[i]; + listChangesOptionsToHex(...args) { + let v = 0; + for (let i = 0; i < args.length; i++) { + v |= 1 << args[i]; } return v.toString(16); }, - changeBaseURL: function(changeNum, patchNum) { - var v = this.getBaseUrl() + '/changes/' + changeNum; + changeBaseURL(changeNum, patchNum) { + let v = this.getBaseUrl() + '/changes/' + changeNum; if (patchNum) { v += '/revisions/' + patchNum; } return v; }, - changePath: function(changeNum) { + changePath(changeNum) { return this.getBaseUrl() + '/c/' + changeNum; }, - changeIsOpen: function(status) { + changeIsOpen(status) { return status === this.ChangeStatus.NEW || status === this.ChangeStatus.DRAFT; }, - changeStatusString: function(change) { + changeStatusString(change) { // "Closed" states should take precedence over "open" ones. if (change.status === this.ChangeStatus.MERGED) { return 'Merged'; @@ -137,7 +137,7 @@ window.Gerrit = window.Gerrit || {}; window.Gerrit.RESTClientBehavior = [ Gerrit.BaseUrlBehavior, - RESTClientBehavior + RESTClientBehavior, ]; })(window); </script>
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html index 2b3e858..87fe67b 100644 --- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html +++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
@@ -20,11 +20,12 @@ <script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script> <script src="../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../test/common-test-setup.html"/> <script> + /** @type {String} */ window.CANONICAL_PATH = '/r'; </script> -<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="../base-url-behavior/base-url-behavior.html"> <link rel="import" href="rest-client-behavior.html"> @@ -43,11 +44,12 @@ </test-fixture> <script> - suite('rest-client-behavior tests', function() { - var element; - var overlay; + suite('rest-client-behavior tests', () => { + let element; + // eslint-disable-next-line no-unused-vars + let overlay; - suiteSetup(function() { + suiteSetup(() => { // Define a Polymer element that uses this behavior. Polymer({ is: 'test-element', @@ -58,20 +60,20 @@ }); }); - setup(function() { + setup(() => { element = fixture('basic'); overlay = fixture('within-overlay'); }); - test('changeBaseURL', function() { + test('changeBaseURL', () => { assert.deepEqual( - element.changeBaseURL('1', '1'), - '/r/changes/1/revisions/1' + element.changeBaseURL('1', '1'), + '/r/changes/1/revisions/1' ); }); - test('changePath', function() { + test('changePath', () => { assert.deepEqual(element.changePath('1'), '/r/c/1'); }); }); -</script> \ No newline at end of file +</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project.html b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project.html new file mode 100644 index 0000000..9fc7523 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project.html
@@ -0,0 +1,106 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> +<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> +<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> + +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../shared/gr-select/gr-select.html"> +<link rel="import" href="../../../styles/gr-form-styles.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-admin-create-project"> + <template> + <style include="shared-styles"> + :host { + display: inline-block; + } + main { + margin: 2em 1em; + } + gr-autocomplete { + display: flex + border-radius: 2px 0 0 2px; + outline: none; + overflow: hidden; + } + </style> + <style include="gr-form-styles"></style> + <main class="gr-form-styles"> + <h1 id="Title"> + Create Project + </h1> + <a id="redirect" href$="[[_redirect(_projectConfig.name)]]" + hidden$="[[!_projectCreated]]" hidden></a> + <br> + <div id="form"> + <fieldset> + <section> + <span class="title">Project name</span> + <iron-autogrow-textarea + id="projectNameInput" + autocomplete="on" + bind-value="{{_projectConfig.name}}"> + </iron-autogrow-textarea> + </section> + <section> + <span class="title">Rights inherit from</span> + <gr-autocomplete + id="rightsInheritFromInput" + text="{{_projectConfig.parent}}" + query="[[_query]]" + placeholder="Optional, defaults to 'All-Projects'"> + </gr-autocomplete> + </section> + <section> + <span class="title">Create initial empty commit</span> + <span class="value"> + <select + id="initalCommit" + is="gr-select" + bind-value="{{_projectConfig.create_empty_commit}}"> + <option value="false">False</option> + <option value="true">True</option> + </select> + </span> + </section> + <section> + <span class="title">Only serve as parent for other projects</span> + <span class="value"> + <select + id="parentProject" + is="gr-select" + bind-value="{{_projectConfig.permissions_only}}"> + <option value="false">False</option> + <option value="true">True</option> + </select> + </span> + </section> + </fieldset> + <gr-button + id="submitBtn" + on-tap="_handleCreateProject" + disabled$="[[!_projectConfig.name]]">Save changes</gr-button> + </div> + </main> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-admin-create-project.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project.js b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project.js new file mode 100644 index 0000000..2376e0c --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project.js
@@ -0,0 +1,95 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-admin-create-project', + + properties: { + params: Object, + + _projectConfig: Object, + _projectCreated: { + type: Object, + value: false, + }, + + _query: { + type: Function, + value() { + return this._getProjectSuggestions.bind(this); + }, + }, + }, + + behaviors: [ + Gerrit.BaseUrlBehavior, + Gerrit.URLEncodingBehavior, + ], + + attached() { + this._createProject(); + }, + + _createProject() { + this._projectConfig = []; + }, + + _formatProjectConfigForSave(p) { + const configInputObj = {}; + for (const key in p) { + if (p.hasOwnProperty(key)) { + if (typeof p[key] === 'object') { + configInputObj[key] = p[key].configured_value; + } else { + configInputObj[key] = p[key]; + } + } + } + return configInputObj; + }, + + _redirect(projectName) { + return this.getBaseUrl() + '/admin/projects/' + + this.encodeURL(projectName, true); + }, + + _handleCreateProject() { + const config = this._formatProjectConfigForSave(this._projectConfig); + return this.$.restAPI.createProject(config) + .then(projectRegistered => { + if (projectRegistered.status === 201) { + this._projectCreated = true; + this.$.redirect.click(); + } + }); + }, + + _getProjectSuggestions(input) { + return this.$.restAPI.getSuggestedProjects(input) + .then(response => { + const projects = []; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + projects.push({ + name: key, + value: response[key], + }); + } + return projects; + }); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project_test.html b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project_test.html new file mode 100644 index 0000000..3a1cc1d --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-create-project/gr-admin-create-project_test.html
@@ -0,0 +1,107 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-admin-create-project</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-admin-create-project.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-admin-create-project></gr-admin-create-project> + </template> +</test-fixture> + +<script> + suite('gr-admin-create-project tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, + }); + element = fixture('basic'); + element._projectConfig = { + name: 'test-project', + create_empty_commit: true, + parent: 'All-Project', + permissions_only: false, + }; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('project created', done => { + const configInputObj = { + name: 'test-project', + create_empty_commit: true, + parent: 'All-Project', + permissions_only: false, + }; + + const saveStub = sandbox.stub(element.$.restAPI, + 'createProject', () => { + return Promise.resolve({}); + }); + + const button = element.$.submitBtn; + element.$.projectNameInput.bindValue = configInputObj.name; + element.$.rightsInheritFromInput.bindValue = configInputObj.parent; + element.$.initalCommit.bindValue = + configInputObj.create_empty_commit; + element.$.parentProject.bindValue = + configInputObj.permissions_only; + + assert.isFalse(button.hasAttribute('disabled')); + + assert.deepEqual(element._projectConfig, configInputObj); + + element._handleCreateProject().then(() => { + assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj)); + done(); + }); + }); + + test('test for button being called', done => { + const button = element.$.submitBtn; + + flushAsynchronousOperations(); + sandbox.stub(element, '_handleCreateProject'); + element._handleCreateProject(); + MockInteractions.tap(button); + assert.isTrue(element._handleCreateProject.called); + done(); + }); + + test('test for _handleCreateProject being called', done => { + const button = element.$.submitBtn; + + flushAsynchronousOperations(); + MockInteractions.tap(button); + done(); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html new file mode 100644 index 0000000..22a2abc --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.html
@@ -0,0 +1,59 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html"> +<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-list-view/gr-list-view.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> + +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-admin-group-list"> + <template> + <style include="shared-styles"></style> + <gr-list-view + filter="[[_filter]]" + items="[[_groups]]" + items-per-page="[[_groupsPerPage]]" + loading="[[_loading]]" + offset="[[_offset]]" + path="[[_path]]"> + <table id="list"> + <tr class="headerRow"> + <th class="name topHeader">Group Name</th> + <th class="description topHeader">Group Description</th> + <th class="visibleToAll topHeader">Visible To All</th> + </tr> + <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]"> + <td>Loading...</td> + </tr> + <template is="dom-repeat" items="[[_shownGroups]]" + class$="[[computeLoadingClass(_loading)]]"> + <tr class="table"> + <td class="name"> + <a href$="[[_computeGroupUrl(item.group_id)]]">[[item.name]]</a> + </td> + <td class="description">[[item.description]]</td> + <td class="visibleToAll">[[_visibleToAll(item)]]</td> + </tr> + </template> + </table> + </gr-list-view> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-admin-group-list.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js new file mode 100644 index 0000000..645b89b --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -0,0 +1,104 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-admin-group-list', + + properties: { + /** + * URL params passed from the router. + */ + params: { + type: Object, + observer: '_paramsChanged', + }, + + /** + * Offset of currently visible query results. + */ + _offset: Number, + _path: { + type: String, + readOnly: true, + value: '/admin/groups', + }, + _groups: Array, + + /** + * Because we request one more than the groupsPerPage, _shownGroups + * may be one less than _groups. + * */ + _shownGroups: { + type: Array, + computed: 'computeShownItems(_groups)', + }, + + _groupsPerPage: { + type: Number, + value: 25, + }, + + _loading: { + type: Boolean, + value: true, + }, + _filter: String, + }, + + behaviors: [ + Gerrit.ListViewBehavior, + ], + + listeners: { + 'next-page': '_handleNextPage', + 'previous-page': '_handlePreviousPage', + }, + + _paramsChanged(params) { + this._loading = true; + this._filter = this.getFilterValue(params); + this._offset = this.getOffsetValue(params); + + return this._getGroups(this._filter, this._groupsPerPage, + this._offset); + }, + + _computeGroupUrl(id) { + return this.getUrl(this._path + '/', id); + }, + + _getGroups(filter, groupsPerPage, offset) { + this._groups = []; + return this.$.restAPI.getGroups(filter, groupsPerPage, offset) + .then(groups => { + if (!groups) { + return; + } + this._groups = Object.keys(groups) + .map(key => { + const group = groups[key]; + group.name = key; + return group; + }); + this._loading = false; + }); + }, + + _visibleToAll(item) { + return item.options.visible_to_all === true ? 'Y' : 'N'; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html new file mode 100644 index 0000000..a6834e2 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
@@ -0,0 +1,145 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-admin-group-list</title> + +<script src="../../../bower_components/page/page.js"></script> +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="../../../test/common-test-setup.html"/> + +<link rel="import" href="gr-admin-group-list.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-admin-group-list></gr-admin-group-list> + </template> +</test-fixture> + +<script> + let counter = 0; + const groupGenerator = () => { + return { + name: `test${++counter}`, + id: '59b92f35489e62c80d1ab1bf0c2d17843038df8b', + url: '#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b', + options: { + visible_to_all: false, + }, + description: 'Gerrit Site Administrators', + group_id: 1, + owner: 'Administrators', + owner_id: '7ca042f4d5847936fcb90ca91057673157fd06fc', + }; + }; + + suite('gr-admin-group-list tests', () => { + let element; + let groups; + let sandbox; + let value; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('list with groups', () => { + setup(done => { + groups = _.times(26, groupGenerator); + + stub('gr-rest-api-interface', { + getGroups(num, offset) { + return Promise.resolve(groups); + }, + }); + + element._paramsChanged(value).then(() => { flush(done); }); + }); + + test('test for test group in the list', done => { + flush(() => { + assert.equal(element._groups[1].name, '1'); + assert.equal(element._groups[1].options.visible_to_all, false); + done(); + }); + }); + + test('_shownGroups', () => { + assert.equal(element._shownGroups.length, 25); + }); + }); + + suite('test with less then 25 groups', () => { + setup(done => { + groups = _.times(25, groupGenerator); + + stub('gr-rest-api-interface', { + getGroups(num, offset) { + return Promise.resolve(groups); + }, + }); + + element._paramsChanged(value).then(() => { flush(done); }); + }); + + test('_shownGroups', () => { + assert.equal(element._shownGroups.length, 25); + }); + }); + + suite('filter', () => { + test('_paramsChanged', done => { + sandbox.stub(element.$.restAPI, 'getGroups', () => { + return Promise.resolve(groups); + }); + const value = { + filter: 'test', + offset: 25, + }; + element._paramsChanged(value).then(() => { + assert.isTrue(element.$.restAPI.getGroups.lastCall + .calledWithExactly('test', 25, 25)); + done(); + }); + }); + }); + + suite('loading', () => { + test('correct contents are displayed', () => { + assert.isTrue(element._loading); + assert.equal(element.computeLoadingClass(element._loading), 'loading'); + assert.equal(getComputedStyle(element.$.loading).display, 'block'); + + element._loading = false; + element._groups = _.times(25, groupGenerator); + + flushAsynchronousOperations(); + assert.equal(element.computeLoadingClass(element._loading), ''); + assert.equal(getComputedStyle(element.$.loading).display, 'none'); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-plugin-list/gr-admin-plugin-list.html b/polygerrit-ui/app/elements/admin/gr-admin-plugin-list/gr-admin-plugin-list.html new file mode 100644 index 0000000..67a89d4 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-plugin-list/gr-admin-plugin-list.html
@@ -0,0 +1,52 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-list-view/gr-styled-table.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> + +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-admin-plugin-list"> + <template> + <style include="shared-styles"></style> + <gr-styled-table> + <table id="list"> + <tr class="headerRow"> + <th class="name topHeader">Plugin Name</th> + <th class="version topHeader">Version</th> + <th class="status topHeader">Status</th> + </tr> + <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]"> + <td>Loading...</td> + </tr> + <template is="dom-repeat" items="[[_plugins]]" + class$="[[computeLoadingClass(_loading)]]"> + <tr class="table"> + <td class="name"> + <a href$="[[_computePluginUrl(item.index_url)]]">[[item.id]]</a> + </td> + <td class="version">[[item.version]]</td> + <td class="status">[[_status(item)]]</td> + </tr> + </template> + </table> + </gr-styled-table> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-admin-plugin-list.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-plugin-list/gr-admin-plugin-list.js b/polygerrit-ui/app/elements/admin/gr-admin-plugin-list/gr-admin-plugin-list.js new file mode 100644 index 0000000..00401a50 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-plugin-list/gr-admin-plugin-list.js
@@ -0,0 +1,61 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-admin-plugin-list', + + properties: { + _plugins: Array, + _loading: { + type: Boolean, + value: true, + }, + }, + + behaviors: [ + Gerrit.ListViewBehavior, + ], + + ready() { + this._getPlugins(); + }, + + _getPlugins() { + return this.$.restAPI.getPlugins() + .then(plugins => { + if (!plugins) { + this._plugins = []; + return; + } + this._plugins = Object.keys(plugins) + .map(key => { + const plugin = plugins[key]; + plugin.name = key; + return plugin; + }); + this._loading = false; + }); + }, + + _status(item) { + return item.disabled === true ? 'Disabled' : 'Enabled'; + }, + + _computePluginUrl(id) { + return this.getUrl('/', id); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-plugin-list/gr-admin-plugin-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-plugin-list/gr-admin-plugin-list_test.html new file mode 100644 index 0000000..0969b84 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-plugin-list/gr-admin-plugin-list_test.html
@@ -0,0 +1,73 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-admin-plugin-list</title> + +<script src="../../../bower_components/page/page.js"></script> +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-admin-plugin-list.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-admin-plugin-list></gr-admin-plugin-list> + </template> +</test-fixture> + +<script> + let counter = 0; + const pluginGenerator = () => { + return { + id: `test${++counter}`, + index_url: `plugins/test${counter}/`, + version: '3.0-SNAPSHOT', + disabled: false, + }; + }; + + suite('gr-admin-plugin-list tests', () => { + let element; + let plugins; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + plugins = _.times(26, pluginGenerator); + stub('gr-rest-api-interface', { + getPlugins() { return Promise.resolve(plugins); }, + }); + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('_plugins item is formatted correctly', () => { + return element._getPlugins().then(() => { + assert.equal(element._plugins[2].id, 'test3'); + assert.equal(element._plugins[2].index_url, 'plugins/test3/'); + assert.equal(element._plugins[2].version, '3.0-SNAPSHOT'); + assert.equal(element._plugins[2].disabled, false); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html new file mode 100644 index 0000000..45a6e4a --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.html
@@ -0,0 +1,72 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html"> +<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-list-view/gr-list-view.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> + +<link rel="import" href="../../../styles/shared-styles.html"> + + +<dom-module id="gr-admin-project-list"> + <template> + <style include="shared-styles"></style> + <gr-list-view + filter="[[_filter]]" + items-per-page="[[_projectsPerPage]]" + items="[[_projects]]" + loading="[[_loading]]" + offset="[[_offset]]" + path="[[_path]]"> + <table id="list"> + <tr class="headerRow"> + <th class="name topHeader">Project Name</th> + <th class="description topHeader">Project Description</th> + <th class="repositoryBrowser topHeader">Repository Browser</th> + <th class="readOnly topHeader">Read only</th> + </tr> + <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]"> + <td>Loading...</td> + </tr> + <template is="dom-repeat" items="[[_shownProjects]]" + class$="[[computeLoadingClass(_loading)]]"> + <tr class="table"> + <td class="name"> + <a href$="[[_computeProjectUrl(item.name)]]">[[item.name]]</a> + </td> + <td class="description">[[item.description]]</td> + <td class="repositoryBrowser"> + <template is="dom-repeat" + items="[[_computeWeblink(item)]]" as="link"> + <a href$="[[link.url]]" + class="webLink" + rel="noopener" + target="_blank"> + ([[link.name]]) + </a> + </template> + </td> + <td class="readOnly">[[_readOnly(item)]]</td> + </tr> + </template> + </table> + </gr-list-view> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-admin-project-list.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js new file mode 100644 index 0000000..8cd804f --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list.js
@@ -0,0 +1,107 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-admin-project-list', + + properties: { + /** + * URL params passed from the router. + */ + params: { + type: Object, + observer: '_paramsChanged', + }, + + /** + * Offset of currently visible query results. + */ + _offset: Number, + _path: { + type: String, + readOnly: true, + value: '/admin/projects', + }, + _projects: Array, + + /** + * Because we request one more than the projectsPerPage, _shownProjects + * maybe one less than _projects. + * */ + _shownProjects: { + type: Array, + computed: 'computeShownItems(_projects)', + }, + + _projectsPerPage: { + type: Number, + value: 25, + }, + + _loading: { + type: Boolean, + value: true, + }, + _filter: String, + }, + + behaviors: [ + Gerrit.ListViewBehavior, + ], + + _paramsChanged(params) { + this._loading = true; + this._filter = this.getFilterValue(params); + this._offset = this.getOffsetValue(params); + + return this._getProjects(this._filter, this._projectsPerPage, + this._offset); + }, + + _computeProjectUrl(name) { + return this.getUrl(this._path + '/', name); + }, + + _getProjects(filter, projectsPerPage, offset) { + this._projects = []; + return this.$.restAPI.getProjects(filter, projectsPerPage, offset) + .then(projects => { + if (!projects) { + return; + } + this._projects = Object.keys(projects) + .map(key => { + const project = projects[key]; + project.name = key; + return project; + }); + this._loading = false; + }); + }, + + _readOnly(item) { + return item.state === 'READ_ONLY' ? 'Y' : 'N'; + }, + + _computeWeblink(project) { + if (!project.web_links) { + return ''; + } + const webLinks = project.web_links; + return webLinks.length ? webLinks : null; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html new file mode 100644 index 0000000..1292bc5 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-project-list/gr-admin-project-list_test.html
@@ -0,0 +1,141 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-admin-project-list</title> + +<script src="../../../bower_components/page/page.js"></script> +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-admin-project-list.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-admin-project-list></gr-admin-project-list> + </template> +</test-fixture> + +<script> + let counter; + const projectGenerator = () => { + return { + id: `test${++counter}`, + state: 'ACTIVE', + web_links: [ + { + name: 'diffusion', + url: `https://phabricator.example.org/r/project/test${counter}`, + }, + ], + }; + }; + + suite('gr-admin-project-list tests', () => { + let element; + let projects; + let sandbox; + let value; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + counter = 0; + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('list with projects', () => { + setup(done => { + projects = _.times(26, projectGenerator); + + stub('gr-rest-api-interface', { + getProjects(num, offset) { + return Promise.resolve(projects); + }, + }); + + element._paramsChanged(value).then(() => { flush(done); }); + }); + + test('test for test project in the list', done => { + flush(() => { + assert.equal(element._projects[1].id, 'test2'); + done(); + }); + }); + + test('_shownProjects', () => { + assert.equal(element._shownProjects.length, 25); + }); + }); + + suite('list with less then 25 projects', () => { + setup(done => { + projects = _.times(25, projectGenerator); + + stub('gr-rest-api-interface', { + getProjects(num, offset) { + return Promise.resolve(projects); + }, + }); + + element._paramsChanged(value).then(() => { flush(done); }); + }); + + test('_shownProjects', () => { + assert.equal(element._shownProjects.length, 25); + }); + }); + + suite('filter', () => { + test('_paramsChanged', done => { + sandbox.stub(element.$.restAPI, 'getProjects', () => { + return Promise.resolve(projects); + }); + const value = { + filter: 'test', + offset: 25, + }; + element._paramsChanged(value).then(() => { + assert.isTrue(element.$.restAPI.getProjects.lastCall + .calledWithExactly('test', 25, 25)); + done(); + }); + }); + }); + + suite('loading', () => { + test('correct contents are displayed', () => { + assert.isTrue(element._loading); + assert.equal(element.computeLoadingClass(element._loading), 'loading'); + assert.equal(getComputedStyle(element.$.loading).display, 'block'); + + element._loading = false; + element._projects = _.times(25, projectGenerator); + + flushAsynchronousOperations(); + assert.equal(element.computeLoadingClass(element._loading), ''); + assert.equal(getComputedStyle(element.$.loading).display, 'none'); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.html b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.html new file mode 100644 index 0000000..e149594 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.html
@@ -0,0 +1,242 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> +<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> + +<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../shared/gr-select/gr-select.html"> +<link rel="import" href="../../../styles/gr-form-styles.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-admin-project"> + <template> + <style include="shared-styles"> + main { + margin: 2em 1em; + } + h2.edited:after { + color: #444; + content: ' *'; + } + .loading, + .hideDownload { + display: none; + } + #loading.loading { + display: block; + } + #loading:not(.loading) { + display: none; + } + </style> + <style include="gr-form-styles"></style> + <main class="gr-form-styles read-only"> + <h1 id="Title">[[project]]</h1> + <div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div> + <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]"> + <div id="downloadContent" class$="[[_computeDownloadClass(_schemes)]]"> + <h2 id="download">Download</h2> + <fieldset> + <gr-download-commands + id="downloadCommands" + commands="[[_computeCommands(project, _schemesObj, _selectedScheme)]]" + schemes="[[_schemes]]" + selected-scheme="{{_selectedScheme}}"></gr-download-commands> + </fieldset> + </div> + <h2 id="configurations" + class$="[[_computeHeaderClass(_configChanged)]]">Configurations</h2> + <div id="form"> + <fieldset> + <h3 id="Description">Description</h3> + <fieldset> + <iron-autogrow-textarea + id="descriptionInput" + class="description" + autocomplete="on" + placeholder="<Insert project description here>" + bind-value="{{_projectConfig.description}}" + disabled$="[[_readOnly]]"></iron-autogrow-textarea> + </fieldset> + <h3 id="Options">Project Options</h3> + <fieldset id="options"> + <section> + <span class="title">State</span> + <span class="value"> + <select + id="stateSelect" + is="gr-select" + bind-value="{{_projectConfig.state}}" + disabled$="[[_readOnly]]"> + <template is="dom-repeat" items=[[_states]]> + <option value="[[item.value]]">[[item.label]]</option> + </template> + </select> + </span> + </section> + <section> + <span class="title">Submit type</span> + <span class="value"> + <select + id="submitTypeSelect" + is="gr-select" + bind-value="{{_projectConfig.submit_type}}" + disabled$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_submitTypes]]"> + <option value="[[item.value]]">[[item.label]]</option> + </template> + </select> + </span> + </section> + <section> + <span class="title">Allow content merges</span> + <span class="value"> + <select + id="contentMergeSelect" + is="gr-select" + bind-value="{{_projectConfig.use_content_merge.configured_value}}" + disabled$="[[_readOnly]]"> + <template is="dom-repeat" + items="[[_formatBooleanSelect(_projectConfig.use_content_merge)]]"> + <option value="[[item.value]]">[[item.label]]</option> + </template> + </select> + </span> + </section> + <section> + <span class="title"> + Create a new change for every commit not in the target branch + </span> + <span class="value"> + <select + id="newChangeSelect" + is="gr-select" + bind-value="{{_projectConfig.create_new_change_for_all_not_in_target.configured_value}}" + disabled$="[[_readOnly]]"> + <template is="dom-repeat" + items="[[_formatBooleanSelect(_projectConfig.create_new_change_for_all_not_in_target)]]"> + <option value="[[item.value]]">[[item.label]]</option> + </template> + </select> + </span> + </section> + <section> + <span class="title">Require Change-Id in commit message</span> + <span class="value"> + <select + id="requireChangeIdSelect" + is="gr-select" + bind-value="{{_projectConfig.require_change_id.configured_value}}" + disabled$="[[_readOnly]]"> + <template is="dom-repeat" + items="[[_formatBooleanSelect(_projectConfig.require_change_id)]]"> + <option value="[[item.value]]">[[item.label]]</option> + </template> + </select> + </span> + </section> + <section> + <span class="title"> + Reject implicit merges when changes are pushed for review</span> + <span class="value"> + <select + id="rejectImplicitMergesSelect" + is="gr-select" + bind-value="{{_projectConfig.reject_implicit_merges.configured_value}}" + disabled$="[[_readOnly]]"> + <template is="dom-repeat" + items="[[_formatBooleanSelect(_projectConfig.reject_implicit_merges)]]"> + <option value="[[item.value]]">[[item.label]]</option> + </template> + </select> + </span> + </section> + <section> + <span class="title">Maximum Git object size limit</span> + <span class="value"> + <input + id="maxGitObjSizeInput" + bind-value="{{_projectConfig.max_object_size_limit.configured_value}}" + is="iron-input" + type="text" + disabled$="[[_readOnly]]"> + </span> + </section> + <section> + <span class="title">Match authored date with committer date upon submit</span> + <span class="value"> + <select + id="matchAuthoredDateWithCommitterDateSelect" + is="gr-select" + bind-value="{{_projectConfig.match_author_to_committer_date.configured_value}}" + disabled$="[[_readOnly]]"> + <template is="dom-repeat" + items="[[_formatBooleanSelect(_projectConfig.match_author_to_committer_date)]]"> + <option value="[[item.value]]">[[item.label]]</option> + </template> + </select> + </span> + </section> + </fieldset> + <h3 id="Options">Contributor Agreements</h3> + <fieldset id="agreements"> + <section> + <span class="title"> + Require a valid contributor agreement to upload</span> + <span class="value"> + <select + id="contributorAgreementSelect" + is="gr-select" + bind-value="{{_projectConfig.use_contributor_agreements.configured_value}}" + disabled$="[[_readOnly]]"> + <template is="dom-repeat" + items="[[_formatBooleanSelect(_projectConfig.use_contributor_agreements)]]"> + <option value="[[item.value]]">[[item.label]]</option> + </template> + </select> + </span> + </section> + <section> + <span class="title">Require Signed-off-by in commit message</span> + <span class="value"> + <select + id="useSignedOffBySelect" + is="gr-select" + bind-value="{{_projectConfig.use_signed_off_by.configured_value}}" + disabled$="[[_readOnly]]"> + <template is="dom-repeat" + items="[[_formatBooleanSelect(_projectConfig.use_signed_off_by)]]"> + <option value="[[item.value]]">[[item.label]]</option> + </template> + </select> + </span> + </section> + </fieldset> + <!-- TODO @beckysiegel add plugin config widgets --> + <gr-button + on-tap="_handleSaveProjectConfig" + disabled$="[[_computeButtonDisabled(_readOnly, _configChanged)]]">Save changes</gr-button> + </fieldset> + </div> + </div> + </main> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-admin-project.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.js b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.js new file mode 100644 index 0000000..3422f17 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project.js
@@ -0,0 +1,250 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + const STATES = { + active: {value: 'ACTIVE', label: 'Active'}, + readOnly: {value: 'READ_ONLY', label: 'Read Only'}, + hidden: {value: 'HIDDEN', label: 'Hidden'}, + }; + + const SUBMIT_TYPES = { + mergeIfNecessary: { + value: 'MERGE_IF_NECESSARY', + label: 'Merge if necessary', + }, + fastForwardOnly: { + value: 'FAST_FORWARD_ONLY', + label: 'Fast forward only', + }, + rebaseAlways: { + value: 'REBASE_ALWAYS', + label: 'Rebase Always', + }, + rebaseIfNecessary: { + value: 'REBASE_IF_NECESSARY', + label: 'Rebase if necessary', + }, + mergeAlways: { + value: 'MERGE_ALWAYS', + label: 'Merge always', + }, + cherryPick: { + value: 'CHERRY_PICK', + label: 'Cherry pick', + }, + }; + + Polymer({ + is: 'gr-admin-project', + + properties: { + params: Object, + project: String, + + _configChanged: { + type: Boolean, + value: false, + }, + _loading: { + type: Boolean, + value: true, + }, + _loggedIn: { + type: Boolean, + value: false, + observer: '_loggedInChanged', + }, + _projectConfig: Object, + _readOnly: { + type: Boolean, + value: true, + }, + _states: { + type: Array, + value() { + return Object.values(STATES); + }, + }, + _submitTypes: { + type: Array, + value() { + return Object.values(SUBMIT_TYPES); + }, + }, + _schemes: { + type: Array, + value() { return []; }, + computed: '_computeSchemes(_schemesObj)', + observer: '_schemesChanged', + }, + _selectedCommand: { + type: String, + value: 'Clone', + }, + _selectedScheme: String, + _schemesObj: Object, + }, + + observers: [ + '_handleConfigChanged(_projectConfig.*)', + ], + + attached() { + this._loadProject(); + }, + + _loadProject() { + if (!this.project) { return Promise.resolve(); } + + const promises = []; + promises.push(this._getLoggedIn().then(loggedIn => { + this._loggedIn = loggedIn; + if (loggedIn) { + this.$.restAPI.getProjectAccess(this.project).then(access => { + // If the user is not an owner, is_owner is not a property. + this._readOnly = !access[this.project].is_owner; + }); + } + })); + + promises.push(this.$.restAPI.getProjectConfig(this.project).then( + config => { + this._projectConfig = config; + if (!this._projectConfig.state) { + this._projectConfig.state = STATES.active.value; + } + this._loading = false; + })); + + promises.push(this.$.restAPI.getConfig().then(config => { + this._schemesObj = config.download.schemes; + })); + + return Promise.all(promises); + }, + + _computeLoadingClass(loading) { + return loading ? 'loading' : ''; + }, + + _computeDownloadClass(schemes) { + return !schemes || !schemes.length ? 'hideDownload' : ''; + }, + + _loggedInChanged(_loggedIn) { + if (!_loggedIn) { return; } + this.$.restAPI.getPreferences().then(prefs => { + if (prefs.download_scheme) { + // Note (issue 5180): normalize the download scheme with lower-case. + this._selectedScheme = prefs.download_scheme.toLowerCase(); + } + }); + }, + + _formatBooleanSelect(item) { + if (!item) { return; } + let inheritLabel = 'Inherit'; + if (item.inherited_value) { + inheritLabel = `Inherit (${item.inherited_value})`; + } + return [ + { + label: inheritLabel, + value: 'INHERIT', + }, + { + label: 'True', + value: 'TRUE', + }, { + label: 'False', + value: 'FALSE', + }, + ]; + }, + + _isLoading() { + return this._loading || this._loading === undefined; + }, + + _getLoggedIn() { + return this.$.restAPI.getLoggedIn(); + }, + + _formatProjectConfigForSave(p) { + const configInputObj = {}; + for (const key in p) { + if (p.hasOwnProperty(key)) { + if (typeof p[key] === 'object') { + configInputObj[key] = p[key].configured_value; + } else { + configInputObj[key] = p[key]; + } + } + } + return configInputObj; + }, + + _handleSaveProjectConfig() { + return this.$.restAPI.saveProjectConfig(this.project, + this._formatProjectConfigForSave(this._projectConfig)).then(() => { + this._configChanged = false; + }); + }, + + _handleConfigChanged() { + if (this._isLoading()) { return; } + this._configChanged = true; + }, + + _computeButtonDisabled(readOnly, configChanged) { + return readOnly || !configChanged; + }, + + _computeHeaderClass(configChanged) { + return configChanged ? 'edited' : ''; + }, + + _computeSchemes(schemesObj) { + return Object.keys(schemesObj); + }, + + _schemesChanged(schemes) { + if (schemes.length === 0) { return; } + if (!schemes.includes(this._selectedScheme)) { + this._selectedScheme = schemes.sort()[0]; + } + }, + + _computeCommands(project, schemesObj, _selectedScheme) { + const commands = []; + let commandObj; + if (schemesObj.hasOwnProperty(_selectedScheme)) { + commandObj = schemesObj[_selectedScheme].clone_commands; + } + for (const title in commandObj) { + if (!commandObj.hasOwnProperty(title)) { continue; } + commands.push({ + title, + command: commandObj[title] + .replace('${project}', project) + .replace('${project-base-name}', + project.substring(project.lastIndexOf('/') + 1)), + }); + } + return commands; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project_test.html b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project_test.html new file mode 100644 index 0000000..e3b809d --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-project/gr-admin-project_test.html
@@ -0,0 +1,286 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-admin-project</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-admin-project.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-admin-project></gr-admin-project> + </template> +</test-fixture> + +<script> + suite('gr-admin-project tests', () => { + let element; + let sandbox; + const PROJECT = 'test-project'; + const SCHEMES = {http: {}, repo: {}, ssh: {}}; + + function getFormFields() { + const selects = Polymer.dom(element.root).querySelectorAll('select'); + const textareas = + Polymer.dom(element.root).querySelectorAll('iron-autogrow-textarea'); + const inputs = Polymer.dom(element.root).querySelectorAll('input'); + return inputs.concat(textareas).concat(selects); + } + + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(false); }, + getProjectConfig() { + return Promise.resolve({ + description: 'Access inherited by all other projects.', + use_contributor_agreements: { + value: false, + configured_value: 'FALSE', + }, + use_content_merge: { + value: false, + configured_value: 'FALSE', + }, + use_signed_off_by: { + value: false, + configured_value: 'FALSE', + }, + create_new_change_for_all_not_in_target: { + value: false, + configured_value: 'FALSE', + }, + require_change_id: { + value: false, + configured_value: 'FALSE', + }, + reject_implicit_merges: { + value: false, + configured_value: 'FALSE', + }, + match_author_to_committer_date: { + value: false, + configured_value: 'FALSE', + }, + max_object_size_limit: {}, + submit_type: 'MERGE_IF_NECESSARY', + }); + }, + getConfig() { + return Promise.resolve({download: {}}); + }, + }); + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('loading displays before project config is loaded', () => { + assert.isTrue(element.$.loading.classList.contains('loading')); + assert.isFalse(getComputedStyle(element.$.loading).display === 'none'); + assert.isTrue(element.$.loadedContent.classList.contains('loading')); + assert.isTrue(getComputedStyle(element.$.loadedContent) + .display === 'none'); + }); + + test('download commands visibility', () => { + element._loading = false; + flushAsynchronousOperations(); + assert.isTrue(element.$.downloadContent.classList + .contains('hideDownload')); + assert.isTrue(getComputedStyle(element.$.downloadContent) + .display == 'none'); + element._schemesObj = SCHEMES; + flushAsynchronousOperations(); + assert.isFalse(element.$.downloadContent.classList + .contains('hideDownload')); + assert.isFalse(getComputedStyle(element.$.downloadContent) + .display == 'none'); + }); + + test('form defaults to read only', () => { + assert.isTrue(element._readOnly); + }); + + test('form defaults to read only when not logged in', done => { + element.project = PROJECT; + element._loadProject().then(() => { + assert.isTrue(element._readOnly); + done(); + }); + }); + + test('form defaults to read only when logged in and not admin', done => { + element.project = PROJECT; + sandbox.stub(element, '_getLoggedIn', () => { + return Promise.resolve(true); + }); + sandbox.stub(element.$.restAPI, 'getProjectAccess', () => { + return Promise.resolve({'test-project': {}}); + }); + element._loadProject().then(() => { + assert.isTrue(element._readOnly); + done(); + }); + }); + + test('all form elements are disabled when not admin', done => { + element.project = PROJECT; + element._loadProject().then(() => { + flushAsynchronousOperations(); + const formFields = getFormFields(); + for (const field of formFields) { + assert.isTrue(field.hasAttribute('disabled')); + } + done(); + }); + }); + + test('_formatBooleanSelect', () => { + let item = {inherited_value: 'true'}; + assert.deepEqual(element._formatBooleanSelect(item), [ + { + label: 'Inherit (true)', + value: 'INHERIT', + }, + { + label: 'True', + value: 'TRUE', + }, { + label: 'False', + value: 'FALSE', + }, + ]); + + // For items without inherited values + item = {}; + assert.deepEqual(element._formatBooleanSelect(item), [ + { + label: 'Inherit', + value: 'INHERIT', + }, + { + label: 'True', + value: 'TRUE', + }, { + label: 'False', + value: 'FALSE', + }, + ]); + }); + + suite('admin', () => { + setup(() => { + element.project = PROJECT; + sandbox.stub(element, '_getLoggedIn', () => { + return Promise.resolve(true); + }); + sandbox.stub(element.$.restAPI, 'getProjectAccess', () => { + return Promise.resolve({'test-project': {is_owner: true}}); + }); + }); + + test('all form elements are enabled', done => { + element._loadProject().then(() => { + flushAsynchronousOperations(); + const formFields = getFormFields(); + for (const field of formFields) { + assert.isFalse(field.hasAttribute('disabled')); + } + assert.isFalse(element._loading); + done(); + }); + }); + + test('state gets set correctly', done => { + element._loadProject().then(() => { + assert.equal(element._projectConfig.state, 'ACTIVE'); + done(); + }); + }); + + test('fields update and save correctly', done => { + const configInputObj = { + description: 'new description', + use_contributor_agreements: 'TRUE', + use_content_merge: 'TRUE', + use_signed_off_by: 'TRUE', + create_new_change_for_all_not_in_target: 'TRUE', + require_change_id: 'TRUE', + reject_implicit_merges: 'TRUE', + match_author_to_committer_date: 'TRUE', + max_object_size_limit: 10, + submit_type: 'FAST_FORWARD_ONLY', + state: 'READ_ONLY', + }; + + const saveStub = sandbox.stub(element.$.restAPI, 'saveProjectConfig' + , () => { + return Promise.resolve({}); + }); + + const button = Polymer.dom(element.root).querySelector('gr-button'); + + element._loadProject().then(() => { + assert.isTrue(button.hasAttribute('disabled')); + assert.isFalse(element.$.Title.classList.contains('edited')); + element.$.descriptionInput.bindValue = configInputObj.description; + element.$.stateSelect.bindValue = configInputObj.state; + element.$.submitTypeSelect.bindValue = configInputObj.submit_type; + element.$.contentMergeSelect.bindValue = + configInputObj.use_content_merge; + element.$.newChangeSelect.bindValue = + configInputObj.create_new_change_for_all_not_in_target; + element.$.requireChangeIdSelect.bindValue = + configInputObj.require_change_id; + element.$.rejectImplicitMergesSelect.bindValue = + configInputObj.reject_implicit_merges; + element.$.matchAuthoredDateWithCommitterDateSelect.bindValue = + configInputObj.match_author_to_committer_date; + element.$.maxGitObjSizeInput.bindValue = + configInputObj.max_object_size_limit; + element.$.contributorAgreementSelect.bindValue = + configInputObj.use_contributor_agreements; + element.$.useSignedOffBySelect.bindValue = + configInputObj.use_signed_off_by; + + assert.isFalse(button.hasAttribute('disabled')); + assert.isTrue(element.$.configurations.classList.contains('edited')); + + const formattedObj = + element._formatProjectConfigForSave(element._projectConfig); + assert.deepEqual(formattedObj, configInputObj); + + element._handleSaveProjectConfig().then(() => { + assert.isTrue(button.hasAttribute('disabled')); + assert.isFalse(element.$.Title.classList.contains('edited')); + assert.isTrue(saveStub.lastCall.calledWithExactly(PROJECT, + configInputObj)); + done(); + }); + }); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html index 527485d..458418a 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html +++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -15,11 +15,97 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> + +<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> +<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html"> +<link rel="import" href="../../../styles/gr-menu-page-styles.html"> +<link rel="import" href="../../../styles/shared-styles.html"> +<link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html"> <link rel="import" href="../../shared/gr-placeholder/gr-placeholder.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../gr-admin-create-project/gr-admin-create-project.html"> +<link rel="import" href="../gr-admin-group-list/gr-admin-group-list.html"> +<link rel="import" href="../gr-admin-plugin-list/gr-admin-plugin-list.html"> +<link rel="import" href="../gr-admin-project-list/gr-admin-project-list.html"> +<link rel="import" href="../gr-admin-project/gr-admin-project.html"> +<link rel="import" href="../gr-project-detail-list/gr-project-detail-list.html"> <dom-module id="gr-admin-view"> <template> - <gr-placeholder title="Admin" path="[[path]]"></gr-placeholder> + <style include="shared-styles"></style> + <style include="gr-menu-page-styles"></style> + <gr-page-nav class$="[[_computeLoadingClass(_loading)]]"> + <ul class="sectionContent"> + <template id="adminNav" is="dom-repeat" items="[[_filteredLinks]]"> + <li class$="sectionTitle [[_computeSelectedClass(item.view, params)]]"> + <a class="title" href="[[_computeLinkURL(item)]]" + rel="noopener">[[item.name]]</a> + </li> + <template is="dom-repeat" items="[[item.children]]" as="child"> + <li class$="[[_computeSelectedClass(child.view, params)]]"> + <a href$="[[_computeLinkURL(child)]]" + rel="noopener">[[child.name]]</a> + </li> + </template> + <template is="dom-if" if="[[item.subsection]]"> + <!--If a section has a subsection, render that.--> + <li class$="[[_computeSelectedClass(item.subsection.view, params)]]"> + <a class="title" href$="[[_computeLinkURL(item.subsection)]]" + rel="noopener"> + [[item.subsection.name]]</a> + </li> + <!--Loop through the links in the sub-section.--> + <template is="dom-repeat" + items="[[item.subsection.children]]" as="child"> + <li class$="subsectionItem [[_computeSelectedClass(child.view, params, child.detailType)]]"> + <a href$="[[_computeLinkURL(child)]]">[[child.name]]</a> + </li> + </template> + </template> + </template> + </ul> + </gr-page-nav> + <template is="dom-if" if="[[_showProjectList]]" restamp="true"> + <main class="table"> + <gr-admin-project-list class="table" params="[[params]]"> + </gr-admin-project-list> + </main> + </template> + <template is="dom-if" if="[[_showProjectMain]]" restamp="true"> + <main> + <gr-admin-project project="[[params.project]]"></gr-admin-project> + </main> + </template> + <template is="dom-if" if="[[_showGroupList]]" restamp="true"> + <main class="table"> + <gr-admin-group-list class="table" params="[[params]]"> + </gr-admin-group-list> + </main> + </template> + <template is="dom-if" if="[[_showPluginList]]" restamp="true"> + <main class="table"> + <gr-admin-plugin-list class="table"></gr-admin-plugin-list> + </main> + </template> + <template is="dom-if" if="[[_showCreateProject]]" restamp="true"> + <main class="table"> + <gr-admin-create-project + params="[[params]]" + id="createProject"></gr-admin-create-project> + </main> + </template> + <template is="dom-if" if="[[_showProjectDetailList]]" restamp="true"> + <main class="table"> + <gr-project-detail-list + params="[[params]]" + class="table" + detail-type="[[params.detailType]]"></gr-project-detail-list> + </main> + </template> + <template is="dom-if" if="[[params.placeholder]]" restamp="true"> + <gr-placeholder title="Admin" path="[[path]]"></gr-placeholder> + </template> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template> <script src="gr-admin-view.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js index cb248e1..3be1a2b 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js +++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -14,11 +14,172 @@ (function() { 'use strict'; + const ADMIN_LINKS = [{ + name: 'Projects', + url: '/admin/projects', + view: 'gr-admin-project-list', + viewableToAll: true, + children: [{ + name: 'Create Project', + capability: 'createProject', + section: 'Projects', + url: '/admin/create-project', + view: 'gr-admin-create-project', + }], + }, { + name: 'Groups', + section: 'Groups', + url: '/admin/groups', + view: 'gr-admin-group-list', + children: [{ + name: 'Create Group', + capability: 'createGroup', + url: '/admin/create-group', + view: 'gr-admin-create-group', + }], + }, { + name: 'Plugins', + capability: 'viewPlugins', + section: 'Plugins', + url: '/admin/plugins', + view: 'gr-admin-plugin-list', + }]; + + const ACCOUNT_CAPABILITIES = ['createProject', 'createGroup', 'viewPlugins']; + Polymer({ is: 'gr-admin-view', properties: { + params: Object, path: String, + adminView: String, + + _project: String, + _filteredLinks: Array, + _showDownload: { + type: Boolean, + value: false, + }, + _showCreateProject: Boolean, + _showProjectMain: Boolean, + _showProjectList: Boolean, + _showProjectBranches: Boolean, + _showGroupList: Boolean, + _showPluginList: Boolean, + }, + + behaviors: [ + Gerrit.BaseUrlBehavior, + Gerrit.URLEncodingBehavior, + ], + + observers: [ + '_paramsChanged(params)', + ], + + attached() { + this.reload(); + }, + + reload() { + return this.$.restAPI.getAccount().then(account => { + this._account = account; + if (!account) { + // Return so that account capabilities don't load with no account. + return this._filteredLinks = this._filterLinks(link => { + return link.viewableToAll; + }); + } + this._loadAccountCapabilities(); + }); + }, + + _filterLinks(filterFn) { + const links = ADMIN_LINKS.filter(filterFn); + const filteredLinks = []; + for (const link of links) { + const linkCopy = Object.assign({}, link); + linkCopy.children = linkCopy.children ? + linkCopy.children.filter(filterFn) : []; + if (linkCopy.name === 'Projects' && this._project) { + linkCopy.subsection = { + name: `${this._project}`, + view: 'gr-admin-project', + url: `/admin/projects/${this.encodeURL(this._project, true)}`, + children: [{ + name: 'Branches', + detailType: 'branches', + view: 'gr-project-detail-list', + url: `/admin/projects/${this.encodeURL(this._project, true)}` + + ',branches', + }, + { + name: 'Tags', + detailType: 'tags', + view: 'gr-project-detail-list', + url: `/admin/projects/${this.encodeURL(this._project, true)}` + + ',tags', + }], + }; + } + filteredLinks.push(linkCopy); + } + return filteredLinks; + }, + + _loadAccountCapabilities() { + return this.$.restAPI.getAccountCapabilities(ACCOUNT_CAPABILITIES) + .then(capabilities => { + this._filteredLinks = this._filterLinks(link => { + return !link.capability || + capabilities.hasOwnProperty(link.capability); + }); + }); + }, + + _paramsChanged(params) { + this.set('_showCreateProject', + params.adminView === 'gr-admin-create-project'); + this.set('_showProjectMain', params.adminView === 'gr-admin-project'); + this.set('_showProjectList', + params.adminView === 'gr-admin-project-list'); + this.set('_showProjectDetailList', + params.adminView === 'gr-project-detail-list'); + this.set('_showGroupList', params.adminView === 'gr-admin-group-list'); + this.set('_showPluginList', params.adminView === 'gr-admin-plugin-list'); + if (params.project !== this._project) { + this._project = params.project || ''; + // Reloads the admin menu. + this.reload(); + } + }, + + // TODO (beckysiegel): Update these functions after router abstraction is + // updated. They are currently copied from gr-dropdown (and should be + // updated there as well once complete). + _computeURLHelper(host, path) { + return '//' + host + this.getBaseUrl() + path; + }, + + _computeRelativeURL(path) { + const host = window.location.host; + return this._computeURLHelper(host, path); + }, + + _computeLinkURL(link) { + if (!link || typeof link.url === 'undefined') { return ''; } + if (link.target) { + return link.url; + } + return this._computeRelativeURL(link.url); + }, + + _computeSelectedClass(itemView, params, opt_detailType) { + if (params.detailType && params.detailType !== opt_detailType) { + return ''; + } + return itemView === params.adminView ? 'selected' : ''; }, }); -})(); +})(); \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html new file mode 100644 index 0000000..acb3ba4 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -0,0 +1,190 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-admin-view</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-admin-view.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-admin-view></gr-admin-view> + </template> +</test-fixture> + +<script> + suite('gr-admin-view tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + stub('gr-rest-api-interface', { + getProjectConfig() { + return Promise.resolve({}); + }, + }); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('_computeURLHelper', () => { + const path = '/test'; + const host = 'http://www.testsite.com'; + const computedPath = element._computeURLHelper(host, path); + assert.equal(computedPath, '//http://www.testsite.com/test'); + }); + + test('link URLs', () => { + assert.equal( + element._computeLinkURL({url: '/test'}), + '//' + window.location.host + '/test'); + assert.equal( + element._computeLinkURL({url: '/test', target: '_blank'}), + '/test'); + }); + + test('current page gets selected and is displayed', () => { + element._filteredLinks = [{ + name: 'Projects', + url: '/admin/projects', + view: 'gr-admin-project-list', + children: [{ + url: '/admin/create-project', + name: 'Create Project', + section: 'Projects', + view: 'gr-admin-create-project', + viewableToAll: true, + }], + }]; + + element.params = { + adminView: 'gr-admin-project-list', + }; + + flushAsynchronousOperations(); + assert.equal(Polymer.dom(element.root).querySelectorAll( + '.selected').length, 1); + assert.ok(element.$$('gr-admin-project-list')); + assert.isNotOk(element.$$('gr-admin-create-project')); + }); + + test('_filteredLinks admin', done => { + sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => { + return Promise.resolve({ + createGroup: true, + createProject: true, + viewPlugins: true, + }); + }); + element._loadAccountCapabilities().then(() => { + assert.equal(element._filteredLinks.length, 3); + + // Projects + assert.equal(element._filteredLinks[0].children.length, 1); + assert.isNotOk(element._filteredLinks[0].subsection); + + // Groups + assert.equal(element._filteredLinks[1].children.length, 1); + + // Plugins + assert.equal(element._filteredLinks[2].children.length, 0); + done(); + }); + }); + + test('_filteredLinks non admin authenticated', done => { + sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => { + return Promise.resolve({}); + }); + element._loadAccountCapabilities().then(() => { + assert.equal(element._filteredLinks.length, 2); + + // Projects + assert.equal(element._filteredLinks[0].children.length, 0); + assert.isNotOk(element._filteredLinks[0].subsection); + + // Groups + assert.equal(element._filteredLinks[1].children.length, 0); + done(); + }); + }); + + test('_filteredLinks non admin unathenticated', done => { + element.reload().then(() => { + assert.equal(element._filteredLinks.length, 1); + + // Projects + assert.equal(element._filteredLinks[0].children.length, 0); + assert.isNotOk(element._filteredLinks[0].subsection); + done(); + }); + }); + + test('Project shows up in nav', done => { + element._project = 'Test Project'; + sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => { + return Promise.resolve({ + createGroup: true, + createProject: true, + viewPlugins: true, + }); + }); + element._loadAccountCapabilities().then(() => { + assert.equal(element._filteredLinks.length, 3); + + // Projects + assert.equal(element._filteredLinks[0].children.length, 1); + assert.equal(element._filteredLinks[0].subsection.name, 'Test Project'); + + // Groups + assert.equal(element._filteredLinks[1].children.length, 1); + + // Plugins + assert.equal(element._filteredLinks[2].children.length, 0); + done(); + }); + }); + + test('Nav is reloaded when project changes', () => { + sandbox.stub(element.$.restAPI, 'getAccountCapabilities', () => { + return Promise.resolve({ + createGroup: true, + createProject: true, + viewPlugins: true, + }); + }); + sandbox.stub(element.$.restAPI, 'getAccount', () => { + return Promise.resolve({_id: 1}); + }); + sandbox.stub(element, 'reload'); + element.params = {project: 'Test Project', adminView: 'gr-admin-project'}; + assert.equal(element.reload.callCount, 1); + element.params = {project: 'Test Project 2', + adminView: 'gr-admin-project'}; + assert.equal(element.reload.callCount, 2); + }); + }); +</script> \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html new file mode 100644 index 0000000..ddae595 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.html
@@ -0,0 +1,77 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html"> +<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html"> +<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-list-view/gr-list-view.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> + +<link rel="import" href="../../../styles/shared-styles.html"> + + +<dom-module id="gr-project-detail-list"> + <template> + <style include="shared-styles"> + .repositoryBrowser { + display: none; + } + .repositoryBrowser.show{ + display: table-cell; + } + </style> + <gr-list-view + filter="[[_filter]]" + items-per-page="[[_itemsPerPage]]" + items="[[_items]]" + loading="[[_loading]]" + offset="[[_offset]]" + path="[[_getPath(_project)]]"> + <table id="list"> + <tr class="headerRow"> + <th class="name topHeader">Name</th> + <th class="description topHeader">Revision</th> + <th class$="repositoryBrowser topHeader [[computeBrowserClass(detailType)]]"> + Repository Browser</th> + </tr> + <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]"> + <td>Loading...</td> + </tr> + <template is="dom-repeat" items="[[_shownItems]]" + class$="[[computeLoadingClass(_loading)]]"> + <tr class="table"> + <td class="name">[[_stripRefs(item.ref, detailType)]]</td> + <td class="description">[[item.revision]]</td> + <td class$="repositoryBrowser [[computeBrowserClass(detailType)]]"> + <template is="dom-repeat" + items="[[_computeWeblink(item)]]" as="link"> + <a href$="[[link.url]]" + class="webLink" + rel="noopener" + target="_blank"> + ([[link.name]]) + </a> + </template> + </td> + </tr> + </template> + </table> + </gr-list-view> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-project-detail-list.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.js b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.js new file mode 100644 index 0000000..704380a --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list.js
@@ -0,0 +1,128 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + const DETAIL_TYPES = { + BRANCHES: 'branches', + TAGS: 'tags', + }; + + Polymer({ + is: 'gr-project-detail-list', + + properties: { + /** + * URL params passed from the router. + */ + params: { + type: Object, + observer: '_paramsChanged', + }, + /** + * The kind of detail we are displaying, possibilities are determined by + * the const DETAIL_TYPES. + */ + detailType: String, + + /** + * Offset of currently visible query results. + */ + _offset: Number, + _project: Object, + _items: Array, + /** + * Because we request one more than the projectsPerPage, _shownProjects + * maybe one less than _projects. + * */ + _shownItems: { + type: Array, + computed: 'computeShownItems(_items)', + }, + _itemsPerPage: { + type: Number, + value: 25, + }, + _loading: { + type: Boolean, + value: true, + }, + _filter: String, + }, + + behaviors: [ + Gerrit.ListViewBehavior, + Gerrit.URLEncodingBehavior, + ], + + _paramsChanged(params) { + this._loading = true; + if (!params || !params.project) { return; } + + this._project = params.project; + + this._filter = this.getFilterValue(params); + this._offset = this.getOffsetValue(params); + + return this._getItems(this._filter, this._project, + this._itemsPerPage, this._offset, this.detailType); + }, + + _getItems(filter, project, itemsPerPage, offset, detailType) { + this._items = []; + Polymer.dom.flush(); + if (detailType === DETAIL_TYPES.BRANCHES) { + return this.$.restAPI.getProjectBranches( + filter, project, itemsPerPage, offset) .then(items => { + if (!items) { return; } + this._items = items; + this._loading = false; + }); + } else if (detailType === DETAIL_TYPES.TAGS) { + return this.$.restAPI.getProjectTags( + filter, project, itemsPerPage, offset) .then(items => { + if (!items) { return; } + this._items = items; + this._loading = false; + }); + } + }, + + _getPath(project) { + return `/admin/projects/${this.encodeURL(project, true)},` + + `${this.detailType}`; + }, + + _computeWeblink(project) { + if (!project.web_links) { return ''; } + const webLinks = project.web_links; + return webLinks.length ? webLinks : null; + }, + + computeBrowserClass(detailType) { + if (detailType === DETAIL_TYPES.BRANCHES) { + return 'show'; + } + return ''; + }, + + _stripRefs(item, detailType) { + if (detailType === DETAIL_TYPES.BRANCHES) { + return item.replace('refs/heads/', ''); + } else if (detailType === DETAIL_TYPES.TAGS) { + return item.replace('refs/tags/', ''); + } + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list_test.html b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list_test.html new file mode 100644 index 0000000..a452091 --- /dev/null +++ b/polygerrit-ui/app/elements/admin/gr-project-detail-list/gr-project-detail-list_test.html
@@ -0,0 +1,251 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-project-detail-list</title> + +<script src="../../../bower_components/page/page.js"></script> +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-project-detail-list.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-project-detail-list></gr-project-detail-list> + </template> +</test-fixture> + +<script> + let counter; + const branchGenerator = () => { + return { + ref: `refs/heads/test${++counter}`, + revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d', + web_links: [ + { + name: 'diffusion', + url: `https://git.example.org/branch/test;refs/heads/test${counter}`, + }, + ], + }; + }; + const tagGenerator = () => { + return { + ref: `refs/tags/test${++counter}`, + revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d', + }; + }; + + suite('Branches', () => { + let element; + let branches; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.detailType = 'branches'; + counter = 0; + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('list of project branches', () => { + setup(done => { + branches = _.times(26, branchGenerator); + + stub('gr-rest-api-interface', { + getProjectBranches(num, project, offset) { + return Promise.resolve(branches); + }, + }); + + const params = { + project: 'test', + }; + + element._paramsChanged(params).then(() => { flush(done); }); + }); + + test('test for test branch in the list', done => { + flush(() => { + assert.equal(element._items[1].ref, 'refs/heads/test2'); + done(); + }); + }); + + test('test for test web links in the branches list', done => { + flush(() => { + assert.equal(element._items[1].web_links[0].url, + 'https://git.example.org/branch/test;refs/heads/test2'); + done(); + }); + }); + + test('test for refs/heads/ being striped from ref', done => { + flush(() => { + assert.equal(element._stripRefs(element._items[1].ref, + element.detailType), 'test2'); + done(); + }); + }); + + test('_shownItems', () => { + assert.equal(element._shownItems.length, 25); + }); + }); + + suite('list with less then 25 branches', () => { + setup(done => { + branches = _.times(25, branchGenerator); + + stub('gr-rest-api-interface', { + getProjectBranches(num, project, offset) { + return Promise.resolve(branches); + }, + }); + + const params = { + project: 'test', + }; + + element._paramsChanged(params).then(() => { flush(done); }); + }); + + test('_shownProjectsBranches', () => { + assert.equal(element._shownItems.length, 25); + }); + }); + + suite('filter', () => { + test('_paramsChanged', done => { + sandbox.stub(element.$.restAPI, 'getProjectBranches', () => { + return Promise.resolve(branches); + }); + const params = { + project: 'test', + filter: 'test', + offset: 25, + }; + element._paramsChanged(params).then(() => { + assert.isTrue(element.$.restAPI.getProjectBranches.lastCall + .calledWithExactly('test', 'test', 25, 25)); + done(); + }); + }); + }); + }); + + suite('Tags', () => { + let element; + let tags; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.detailType = 'tags'; + counter = 0; + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('list of project tags', () => { + setup(done => { + tags = _.times(26, tagGenerator); + + stub('gr-rest-api-interface', { + getProjectTags(num, project, offset) { + return Promise.resolve(tags); + }, + }); + + const params = { + project: 'test', + }; + + element._paramsChanged(params).then(() => { flush(done); }); + }); + + test('test for test tag in the list', done => { + flush(() => { + assert.equal(element._items[1].ref, 'refs/tags/test2'); + done(); + }); + }); + + test('test for refs/tags/ being striped from ref', done => { + flush(() => { + assert.equal(element._stripRefs(element._items[1].ref, + element.detailType), 'test2'); + done(); + }); + }); + + test('_shownItems', () => { + assert.equal(element._shownItems.length, 25); + }); + }); + + suite('list with less then 25 tags', () => { + setup(done => { + tags = _.times(25, tagGenerator); + + stub('gr-rest-api-interface', { + getProjectTags(num, project, offset) { + return Promise.resolve(tags); + }, + }); + + const params = { + project: 'test', + }; + + element._paramsChanged(params).then(() => { flush(done); }); + }); + + test('_shownItems', () => { + assert.equal(element._shownItems.length, 25); + }); + }); + + suite('filter', () => { + test('_paramsChanged', done => { + sandbox.stub(element.$.restAPI, 'getProjectTags', () => { + return Promise.resolve(tags); + }); + const params = { + project: 'test', + filter: 'test', + offset: 25, + }; + element._paramsChanged(params).then(() => { + assert.isTrue(element.$.restAPI.getProjectTags.lastCall + .calledWithExactly('test', 'test', 25, 25)); + done(); + }); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html index d50e0b3..9181f5d 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -22,14 +22,18 @@ <link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> <link rel="import" href="../../shared/gr-change-star/gr-change-star.html"> <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-change-list-item"> <template> - <style> + <style include="shared-styles"> :host { display: table-row; border-bottom: 1px solid #eee; } + :host(:hover) { + background-color: #f5fafd; + } :host([selected]) { background-color: #ebf5fb; } @@ -136,8 +140,8 @@ </td> <td class="cell branch" hidden$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]"> - <a href$="[[_computeProjectBranchURL(change.project, change.branch)]]"> - [[change.branch]] + <a href$="[[_computeProjectBranchURL(change)]]"> + [[_computeBranchText(change)]] </a> </td> <td class="cell updated"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js index 566dfe0..189a942 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -40,15 +40,15 @@ Gerrit.URLEncodingBehavior, ], - _computeChangeURL: function(changeNum) { + _computeChangeURL(changeNum) { if (!changeNum) { return ''; } return this.getBaseUrl() + '/c/' + changeNum + '/'; }, - _computeLabelTitle: function(change, labelName) { - var label = change.labels[labelName]; + _computeLabelTitle(change, labelName) { + const label = change.labels[labelName]; if (!label) { return 'Label not applicable'; } - var significantLabel = label.rejected || label.approved || + const significantLabel = label.rejected || label.approved || label.disliked || label.recommended; if (significantLabel && significantLabel.name) { return labelName + '\nby ' + significantLabel.name; @@ -56,12 +56,12 @@ return labelName; }, - _computeLabelClass: function(change, labelName) { - var label = change.labels[labelName]; + _computeLabelClass(change, labelName) { + const label = change.labels[labelName]; // Mimic a Set. - var classes = { - 'cell': true, - 'label': true, + const classes = { + cell: true, + label: true, }; if (label) { if (label.approved) { @@ -83,8 +83,8 @@ return Object.keys(classes).sort().join(' '); }, - _computeLabelValue: function(change, labelName) { - var label = change.labels[labelName]; + _computeLabelValue(change, labelName) { + const label = change.labels[labelName]; if (!label) { return ''; } if (label.approved) { return '✓'; @@ -101,15 +101,25 @@ return ''; }, - _computeProjectURL: function(project) { + _computeProjectURL(project) { return this.getBaseUrl() + '/q/status:open+project:' + this.encodeURL(project, false); }, - _computeProjectBranchURL: function(project, branch) { - // @see Issue 4255. - return this._computeProjectURL(project) + - '+branch:' + this.encodeURL(branch, false); + _computeProjectBranchURL(change) { + // @see Issue 4255, Issue 6195. + let output = this._computeProjectURL(change.project); + output += '+branch:' + this.encodeURL(change.branch, false); + if (change.topic) { + output += '+topic:' + this.encodeURL(change.topic, false); + } + return output; + }, + + _computeBranchText(change) { + let output = change.branch; + if (change.topic) { output += ` (${change.topic})`; } + return output; }, }); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html index ad76f10..0483d23 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-change-list-item.html"> <script>void(0);</script> @@ -34,19 +34,19 @@ </test-fixture> <script> - suite('gr-change-list-item tests', function() { - var element; + suite('gr-change-list-item tests', () => { + let element; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, - getLoggedIn: function() { return Promise.resolve(false); }, + getConfig() { return Promise.resolve({}); }, + getLoggedIn() { return Promise.resolve(false); }, }); element = fixture('basic'); }); - test('change status', function() { - var getStatusForChange = function(change) { + test('change status', () => { + const getStatusForChange = function(change) { element.change = change; return element.$$('.cell.status').textContent.trim(); }; @@ -59,7 +59,7 @@ assert.equal(getStatusForChange({status: 'DRAFT'}), 'Draft'); }); - test('computed fields', function() { + test('computed fields', () => { assert.equal(element._computeLabelClass({labels: {}}), 'cell label u-gray-background'); assert.equal(element._computeLabelClass( @@ -99,19 +99,19 @@ 'Code-Review'), 'Code-Review\nby Diffy'); assert.equal(element._computeLabelTitle( {labels: {'Code-Review': {recommended: {name: 'Diffy'}, - rejected: {name: 'Admin'}}}}, 'Code-Review'), + rejected: {name: 'Admin'}}}}, 'Code-Review'), 'Code-Review\nby Admin'); assert.equal(element._computeLabelTitle( {labels: {'Code-Review': {approved: {name: 'Diffy'}, - rejected: {name: 'Admin'}}}}, 'Code-Review'), + rejected: {name: 'Admin'}}}}, 'Code-Review'), 'Code-Review\nby Admin'); assert.equal(element._computeLabelTitle( {labels: {'Code-Review': {recommended: {name: 'Diffy'}, - disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'), + disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'), 'Code-Review\nby Admin'); assert.equal(element._computeLabelTitle( {labels: {'Code-Review': {approved: {name: 'Diffy'}, - disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'), + disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'), 'Code-Review\nby Diffy'); assert.equal(element._computeLabelValue({labels: {}}), ''); @@ -130,17 +130,22 @@ assert.equal(element._computeProjectURL('combustible/stuff'), '/q/status:open+project:combustible%252Fstuff'); - assert.equal(element._computeProjectBranchURL( - 'combustible-stuff', 'le/mons'), + const change = {project: 'combustible-stuff', branch: 'le/mons'}; + assert.equal(element._computeProjectBranchURL(change), '/q/status:open+project:combustible-stuff+branch:le%252Fmons'); + change.topic = 'test/test'; + assert.equal(element._computeProjectBranchURL(change), + '/q/status:open+project:combustible-stuff+branch:le%252Fmons' + + '+topic:test%252Ftest'); + element.change = {_number: 42}; assert.equal(element.changeURL, '/c/42/'); element.change = {_number: 43}; assert.equal(element.changeURL, '/c/43/'); }); - test('no hidden columns', function() { + test('no hidden columns', () => { element.visibleChangeTableColumns = [ 'Subject', 'Status', @@ -153,13 +158,13 @@ flushAsynchronousOperations(); - element.columnNames.forEach(function(column) { - var elementClass = '.' + column.toLowerCase(); + for (const column of element.columnNames) { + const elementClass = '.' + column.toLowerCase(); assert.isFalse(element.$$(elementClass).hidden); - }); + } }); - test('no hidden columns', function() { + test('no hidden columns', () => { element.visibleChangeTableColumns = [ 'Subject', 'Status', @@ -172,13 +177,13 @@ flushAsynchronousOperations(); - element.columnNames.forEach(function(column) { - var elementClass = '.' + column.toLowerCase(); + for (const column of element.columnNames) { + const elementClass = '.' + column.toLowerCase(); assert.isFalse(element.$$(elementClass).hidden); - }); + } }); - test('project column hidden', function() { + test('project column hidden', () => { element.visibleChangeTableColumns = [ 'Subject', 'Status', @@ -190,25 +195,24 @@ flushAsynchronousOperations(); - element.columnNames.forEach(function(column) { - var elementClass = '.' + column.toLowerCase(); + for (const column of element.columnNames) { + const elementClass = '.' + column.toLowerCase(); if (column === 'Project') { assert.isTrue(element.$$(elementClass).hidden); } else { assert.isFalse(element.$$(elementClass).hidden); } - }); + } }); - test('random column does not exist', function() { + test('random column does not exist', () => { element.visibleChangeTableColumns = [ 'Bad', ]; flushAsynchronousOperations(); - var elementClass = '.bad'; + const elementClass = '.bad'; assert.isNotOk(element.$$(elementClass)); }); - }); </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html index afe0e38..0c5db71 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -19,10 +19,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../gr-change-list/gr-change-list.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-change-list-view"> <template> - <style> + <style include="shared-styles"> :host { background-color: var(--view-background-color); display: block;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js index 82d85ac..a606203 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -14,7 +14,7 @@ (function() { 'use strict'; - var LookupQueryPatterns = { + const LookupQueryPatterns = { CHANGE_ID: /^\s*i?[0-9a-f]{8,40}\s*$/i, CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g, }; @@ -56,7 +56,7 @@ viewState: { type: Object, notify: true, - value: function() { return {}; }, + value() { return {}; }, }, _changesPerPage: Number, @@ -93,11 +93,11 @@ 'previous-page': '_handlePreviousPage', }, - attached: function() { + attached() { this.fire('title-change', {title: this._query}); }, - _paramsChanged: function(value) { + _paramsChanged(value) { if (value.view != this.tagName.toLowerCase()) { return; } this._loading = true; @@ -112,12 +112,12 @@ this.fire('title-change', {title: this._query}); - this._getPreferences().then(function(prefs) { + this._getPreferences().then(prefs => { this._changesPerPage = prefs.changes_per_page; return this._getChanges(); - }.bind(this)).then(function(changes) { + }).then(changes => { if (this._query && changes.length === 1) { - for (var query in LookupQueryPatterns) { + for (const query in LookupQueryPatterns) { if (LookupQueryPatterns.hasOwnProperty(query) && this._query.match(LookupQueryPatterns[query])) { page.show('/c/' + changes[0]._number); @@ -127,46 +127,46 @@ } this._changes = changes; this._loading = false; - }.bind(this)); + }); }, - _getChanges: function() { + _getChanges() { return this.$.restAPI.getChanges(this._changesPerPage, this._query, this._offset); }, - _getPreferences: function() { + _getPreferences() { return this.$.restAPI.getPreferences(); }, - _computeNavLink: function(query, offset, direction, changesPerPage) { + _computeNavLink(query, offset, direction, changesPerPage) { // Offset could be a string when passed from the router. offset = +(offset || 0); - var newOffset = Math.max(0, offset + (changesPerPage * direction)); + const newOffset = Math.max(0, offset + (changesPerPage * direction)); // Double encode URI component. - var href = this.getBaseUrl() + '/q/' + this.encodeURL(query, false); + let href = this.getBaseUrl() + '/q/' + this.encodeURL(query, false); if (newOffset > 0) { href += ',' + newOffset; } return href; }, - _hidePrevArrow: function(offset) { + _hidePrevArrow(offset) { return offset === 0; }, - _hideNextArrow: function(loading) { + _hideNextArrow(loading) { return loading || !this._changes || !this._changes.length || !this._changes[this._changes.length - 1]._more_changes; }, - _handleNextPage: function() { + _handleNextPage() { if (this.$.nextArrow.hidden) { return; } page.show(this._computeNavLink( this._query, this._offset, 1, this._changesPerPage)); }, - _handlePreviousPage: function() { + _handlePreviousPage() { if (this.$.prevArrow.hidden) { return; } page.show(this._computeNavLink( this._query, this._offset, -1, this._changesPerPage));
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html index 661dd2c..e60907b 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -21,8 +21,7 @@ <script src="../../../bower_components/page/page.js"></script> <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-change-list-view.html"> <script>void(0);</script> @@ -34,17 +33,17 @@ </test-fixture> <script> - var CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127'; - var COMMIT_HASH = '12345678'; + const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127'; + const COMMIT_HASH = '12345678'; - suite('gr-change-list-view tests', function() { - var element; - var sandbox; + suite('gr-change-list-view tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(false); }, - getChanges: function(num, query) { + getLoggedIn() { return Promise.resolve(false); }, + getChanges(num, query) { return Promise.resolve([]); }, }); @@ -52,14 +51,14 @@ sandbox = sinon.sandbox.create(); }); - teardown(function(done) { - flush(function() { + teardown(done => { + flush(() => { sandbox.restore(); done(); }); }); - test('url is properly encoded', function() { + test('url is properly encoded', () => { assert.equal(element._computeNavLink( 'status:open project:platform/frameworks/base', 0, -1, 25), '/q/status:open+project:platform%252Fframeworks%252Fbase' @@ -70,11 +69,11 @@ ); }); - test('_computeNavLink', function() { - var query = 'status:open'; - var offset = 0; - var direction = 1; - var changesPerPage = 5; + test('_computeNavLink', () => { + const query = 'status:open'; + let offset = 0; + let direction = 1; + const changesPerPage = 5; assert.equal( element._computeNavLink(query, offset, direction, changesPerPage), '/q/status:open,5'); @@ -89,12 +88,12 @@ '/q/status:open,10'); }); - test('_computeNavLink with path', function() { + test('_computeNavLink with path', () => { window.CANONICAL_PATH = '/r'; - var query = 'status:open'; - var offset = 0; - var direction = 1; - var changesPerPage = 5; + const query = 'status:open'; + let offset = 0; + let direction = 1; + const changesPerPage = 5; assert.equal( element._computeNavLink(query, offset, direction, changesPerPage), '/r/q/status:open,5'); @@ -109,34 +108,34 @@ '/r/q/status:open,10'); }); - test('_hidePrevArrow', function() { - var offset = 0; + test('_hidePrevArrow', () => { + let offset = 0; assert.isTrue(element._hidePrevArrow(offset)); offset = 5; assert.isFalse(element._hidePrevArrow(offset)); }); - test('_hideNextArrow', function() { - var loading = true; + test('_hideNextArrow', () => { + let loading = true; assert.isTrue(element._hideNextArrow(loading)); loading = false; assert.isTrue(element._hideNextArrow(loading)); element._changes = []; assert.isTrue(element._hideNextArrow(loading)); element._changes = - Array.apply(null, Array(5)).map(Object.prototype.valueOf, {}); + Array(...Array(5)).map(Object.prototype.valueOf, {}); assert.isTrue(element._hideNextArrow(loading)); element._changes = - Array.apply(null, Array(25)).map(Object.prototype.valueOf, + Array(...Array(25)).map(Object.prototype.valueOf, {_more_changes: true}); assert.isFalse(element._hideNextArrow(loading)); element._changes = - Array.apply(null, Array(25)).map(Object.prototype.valueOf, {}); + Array(...Array(25)).map(Object.prototype.valueOf, {}); assert.isTrue(element._hideNextArrow(loading)); }); - test('_handleNextPage', function() { - var showStub = sandbox.stub(page, 'show'); + test('_handleNextPage', () => { + const showStub = sandbox.stub(page, 'show'); element.$.nextArrow.hidden = true; element._handleNextPage(); assert.isFalse(showStub.called); @@ -145,8 +144,8 @@ assert.isTrue(showStub.called); }); - test('_handlePreviousPage', function() { - var showStub = sandbox.stub(page, 'show'); + test('_handlePreviousPage', () => { + const showStub = sandbox.stub(page, 'show'); element.$.prevArrow.hidden = true; element._handlePreviousPage(); assert.isFalse(showStub.called); @@ -155,11 +154,11 @@ assert.isTrue(showStub.called); }); - suite('query based navigation', function() { - test('Searching for a change ID redirects to change', function(done) { + suite('query based navigation', () => { + test('Searching for a change ID redirects to change', done => { sandbox.stub(element, '_getChanges') .returns(Promise.resolve([{_number: 1}])); - sandbox.stub(page, 'show', function(url) { + sandbox.stub(page, 'show', url => { assert.equal(url, '/c/1'); done(); }); @@ -167,10 +166,10 @@ element.params = {view: 'gr-change-list-view', query: CHANGE_ID}; }); - test('Searching for a change num redirects to change', function(done) { + test('Searching for a change num redirects to change', done => { sandbox.stub(element, '_getChanges') .returns(Promise.resolve([{_number: 1}])); - sandbox.stub(page, 'show', function(url) { + sandbox.stub(page, 'show', url => { assert.equal(url, '/c/1'); done(); }); @@ -178,10 +177,10 @@ element.params = {view: 'gr-change-list-view', query: '1'}; }); - test('Commit hash redirects to change', function(done) { + test('Commit hash redirects to change', done => { sandbox.stub(element, '_getChanges') .returns(Promise.resolve([{_number: 1}])); - sandbox.stub(page, 'show', function(url) { + sandbox.stub(page, 'show', url => { assert.equal(url, '/c/1'); done(); }); @@ -189,10 +188,10 @@ element.params = {view: 'gr-change-list-view', query: COMMIT_HASH}; }); - test('Searching for an invalid change ID searches', function() { + test('Searching for an invalid change ID searches', () => { sandbox.stub(element, '_getChanges') .returns(Promise.resolve([])); - var stub = sandbox.stub(page, 'show'); + const stub = sandbox.stub(page, 'show'); element.params = {view: 'gr-change-list-view', query: CHANGE_ID}; flushAsynchronousOperations(); @@ -200,10 +199,10 @@ assert.isFalse(stub.called); }); - test('Change ID with multiple search results searches', function() { + test('Change ID with multiple search results searches', () => { sandbox.stub(element, '_getChanges') .returns(Promise.resolve([{}, {}])); - var stub = sandbox.stub(page, 'show'); + const stub = sandbox.stub(page, 'show'); element.params = {view: 'gr-change-list-view', query: CHANGE_ID}; flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html index 8a95fa8..7277fd2 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -14,17 +14,20 @@ limitations under the License. --> +<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> <link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html"> +<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html"> <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../styles/gr-change-list-styles.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../gr-change-list-item/gr-change-list-item.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-change-list"> <template> - <style> + <style include="shared-styles"> #changeList { border-collapse: collapse; width: 100%; @@ -35,6 +38,20 @@ th { text-align: left; } + .groupHeader { + background-color: #eee; + border-top: 1em solid #fff; + } + .groupHeader a { + color: #000; + text-decoration: none; + } + .groupHeader a:hover { + text-decoration: underline; + } + .headerRow + tr { + border: none; + } </style> <style include="gr-change-list-styles"></style> <table id="changeList"> @@ -54,17 +71,20 @@ </th> </template> </tr> - <template is="dom-repeat" items="[[groups]]" as="changeGroup" - index-as="groupIndex"> - <template is="dom-if" if="[[_groupTitle(groupIndex)]]"> + <template is="dom-repeat" items="[[sections]]" as="changeSection" + index-as="sectionIndex"> + <template is="dom-if" if="[[_sectionTitle(sectionIndex)]]"> <tr class="groupHeader"> <td class="cell" colspan$="[[_computeColspan(changeTableColumns, labelNames)]]"> - [[_groupTitle(groupIndex)]] + <a + href$="[[_sectionHref(sectionIndex)]]"> + [[_sectionTitle(sectionIndex)]] + </a> </td> </tr> </template> - <template is="dom-if" if="[[!changeGroup.length]]"> + <template is="dom-if" if="[[!changeSection.length]]"> <tr class="noChanges"> <td class="cell" colspan$="[[_computeColspan(changeTableColumns, labelNames)]]"> @@ -72,9 +92,9 @@ </td> </tr> </template> - <template is="dom-repeat" items="[[changeGroup]]" as="change"> + <template is="dom-repeat" items="[[changeSection]]" as="change"> <gr-change-list-item - selected$="[[_computeItemSelected(index, groupIndex, selectedIndex)]]" + selected$="[[_computeItemSelected(index, sectionIndex, selectedIndex)]]" assigned$="[[_computeItemAssigned(account, change)]]" needs-review$="[[_computeItemNeedsReview(account, change, showReviewedState)]]" change="[[change]]"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js index 425ea76..e7b15af 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -14,7 +14,7 @@ (function() { 'use strict'; - var NUMBER_FIXED_COLUMNS = 3; + const NUMBER_FIXED_COLUMNS = 3; Polymer({ is: 'gr-change-list', @@ -42,7 +42,7 @@ */ account: { type: Object, - value: function() { return {}; }, + value() { return {}; }, }, /** * An array of ChangeInfo objects to render. @@ -53,20 +53,20 @@ observer: '_changesChanged', }, /** - * ChangeInfo objects grouped into arrays. The groups and changes + * ChangeInfo objects grouped into arrays. The sections and changes * properties should not be used together. */ - groups: { + sections: { type: Array, - value: function() { return []; }, + value() { return []; }, }, - groupTitles: { + sectionMetadata: { type: Array, - value: function() { return []; }, + value() { return []; }, }, labelNames: { type: Array, - computed: '_computeLabelNames(groups)', + computed: '_computeLabelNames(sections)', }, selectedIndex: { type: Number, @@ -83,14 +83,17 @@ }, keyEventTarget: { type: Object, - value: function() { return document.body; }, + value() { return document.body; }, }, + changeTableColumns: Array, }, behaviors: [ + Gerrit.BaseUrlBehavior, Gerrit.ChangeTableBehavior, Gerrit.KeyboardShortcutBehavior, Gerrit.RESTClientBehavior, + Gerrit.URLEncodingBehavior, ], keyBindings: { @@ -99,18 +102,19 @@ 'n ]': '_handleNKey', 'o enter': '_handleEnterKey', 'p [': '_handlePKey', + 'shift+r': '_handleRKey', }, - attached: function() { + attached() { this._loadPreferences(); }, - _lowerCase: function(column) { + _lowerCase(column) { return column.toLowerCase(); }, - _loadPreferences: function() { - return this._getLoggedIn().then(function(loggedIn) { + _loadPreferences() { + return this._getLoggedIn().then(loggedIn => { this.changeTableColumns = this.columnNames; if (!loggedIn) { @@ -118,99 +122,105 @@ this.visibleChangeTableColumns = this.columnNames; return; } - return this._getPreferences().then(function(preferences) { + return this._getPreferences().then(preferences => { this.showNumber = !!(preferences && preferences.legacycid_in_change_table); this.visibleChangeTableColumns = preferences.change_table.length > 0 ? preferences.change_table : this.columnNames; - }.bind(this)); - }.bind(this)); + }); + }); }, - _getLoggedIn: function() { + _getLoggedIn() { return this.$.restAPI.getLoggedIn(); }, - _getPreferences: function() { + _getPreferences() { return this.$.restAPI.getPreferences(); }, - _computeColspan: function(changeTableColumns, labelNames) { + _computeColspan(changeTableColumns, labelNames) { return changeTableColumns.length + labelNames.length + NUMBER_FIXED_COLUMNS; }, - _computeLabelNames: function(groups) { - if (!groups) { return []; } - var labels = []; - var nonExistingLabel = function(item) { - return labels.indexOf(item) < 0; + _computeLabelNames(sections) { + if (!sections) { return []; } + let labels = []; + const nonExistingLabel = function(item) { + return !labels.includes(item); }; - for (var i = 0; i < groups.length; i++) { - var group = groups[i]; - for (var j = 0; j < group.length; j++) { - var change = group[j]; + for (let i = 0; i < sections.length; i++) { + const section = sections[i]; + for (let j = 0; j < section.length; j++) { + const change = section[j]; if (!change.labels) { continue; } - var currentLabels = Object.keys(change.labels); + const currentLabels = Object.keys(change.labels); labels = labels.concat(currentLabels.filter(nonExistingLabel)); } } return labels.sort(); }, - _computeLabelShortcut: function(labelName) { + _computeLabelShortcut(labelName) { return labelName.replace(/[a-z-]/g, ''); }, - _changesChanged: function(changes) { - this.groups = changes ? [changes] : []; + _changesChanged(changes) { + this.sections = changes ? [changes] : []; }, - _groupTitle: function(groupIndex) { - if (groupIndex > this.groupTitles.length - 1) { return null; } - return this.groupTitles[groupIndex]; + _sectionTitle(sectionIndex) { + if (sectionIndex > this.sectionMetadata.length - 1) { return null; } + return this.sectionMetadata[sectionIndex].name; }, - _computeItemSelected: function(index, groupIndex, selectedIndex) { - var idx = 0; - for (var i = 0; i < groupIndex; i++) { - idx += this.groups[i].length; + _sectionHref(sectionIndex) { + if (sectionIndex > this.sectionMetadata.length - 1) { return null; } + const query = this.sectionMetadata[sectionIndex].query; + return `${this.getBaseUrl()}/q/${this.encodeURL(query, true)}`; + }, + + _computeItemSelected(index, sectionIndex, selectedIndex) { + let idx = 0; + for (let i = 0; i < sectionIndex; i++) { + idx += this.sections[i].length; } idx += index; return idx == selectedIndex; }, - _computeItemNeedsReview: function(account, change, showReviewedState) { + _computeItemNeedsReview(account, change, showReviewedState) { return showReviewedState && !change.reviewed && this.changeIsOpen(change.status) && account._account_id != change.owner._account_id; }, - _computeItemAssigned: function(account, change) { + _computeItemAssigned(account, change) { if (!change.assignee) { return false; } return account._account_id === change.assignee._account_id; }, - _getAggregateGroupsLen: function(groups) { - groups = groups || []; - var len = 0; - this.groups.forEach(function(group) { - len += group.length; - }); + _getAggregatesectionsLen(sections) { + sections = sections || []; + let len = 0; + for (const section of this.sections) { + len += section.length; + } return len; }, - _handleJKey: function(e) { + _handleJKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } e.preventDefault(); - var len = this._getAggregateGroupsLen(this.groups); + const len = this._getAggregatesectionsLen(this.sections); if (this.selectedIndex === len - 1) { return; } this.selectedIndex += 1; }, - _handleKKey: function(e) { + _handleKKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -219,7 +229,7 @@ this.selectedIndex -= 1; }, - _handleEnterKey: function(e) { + _handleEnterKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -227,29 +237,38 @@ page.show(this._changeURLForIndex(this.selectedIndex)); }, - _handleNKey: function(e) { + _handleNKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); this.fire('next-page'); }, - _handlePKey: function(e) { + _handlePKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); this.fire('previous-page'); }, - _changeURLForIndex: function(index) { - var changeEls = this._getListItems(); + _handleRKey(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { + return; + } + + e.preventDefault(); + window.location.reload(); + }, + + _changeURLForIndex(index) { + const changeEls = this._getListItems(); if (index < changeEls.length && changeEls[index]) { return changeEls[index].changeURL; } return ''; }, - _getListItems: function() { + _getListItems() { return Polymer.dom(this.root).querySelectorAll('gr-change-list-item'); }, });
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html index 1a1abff..aba32b03 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../bower_components/page/page.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-change-list.html"> <script>void(0);</script> @@ -40,16 +40,16 @@ </test-fixture> <script> - suite('gr-change-list basic tests', function() { - var element; + suite('gr-change-list basic tests', () => { + let element; - setup(function() { + setup(() => { element = fixture('basic'); }); function stubRestAPI(preferences) { - var loggedInPromise = Promise.resolve(preferences !== null); - var preferencesPromise = Promise.resolve(preferences); + const loggedInPromise = Promise.resolve(preferences !== null); + const preferencesPromise = Promise.resolve(preferences); stub('gr-rest-api-interface', { getLoggedIn: sinon.stub().returns(loggedInPromise), getPreferences: sinon.stub().returns(preferencesPromise), @@ -57,60 +57,60 @@ return Promise.all([loggedInPromise, preferencesPromise]); } - suite('test show change number not logged in', function() { - setup(function() { - return stubRestAPI(null).then(function() { + suite('test show change number not logged in', () => { + setup(() => { + return stubRestAPI(null).then(() => { element = fixture('basic'); return element._loadPreferences(); }); }); - test('show number disabled', function() { + test('show number disabled', () => { assert.isFalse(element.showNumber); }); }); - suite('test show change number preference enabled', function() { - setup(function() { + suite('test show change number preference enabled', () => { + setup(() => { return stubRestAPI({legacycid_in_change_table: true, - time_format: 'HHMM_12', - change_table: [], - }).then(function() { + time_format: 'HHMM_12', + change_table: [], + }).then(() => { element = fixture('basic'); return element._loadPreferences(); }); }); - test('show number enabled', function() { + test('show number enabled', () => { assert.isTrue(element.showNumber); }); }); - suite('test show change number preference disabled', function() { - setup(function() { + suite('test show change number preference disabled', () => { + setup(() => { // legacycid_in_change_table is not set when false. return stubRestAPI({time_format: 'HHMM_12', change_table: []}).then( - function() { - element = fixture('basic'); - return element._loadPreferences(); - }); + () => { + element = fixture('basic'); + return element._loadPreferences(); + }); }); - test('show number disabled', function() { + test('show number disabled', () => { assert.isFalse(element.showNumber); }); }); - test('computed fields', function() { + test('computed fields', () => { assert.equal(element._computeLabelNames( [[{_number: 0, labels: {}}]]).length, 0); assert.equal(element._computeLabelNames([[ {_number: 0, labels: {Verified: {approved: {}}}}, - {_number: 1, labels: { - Verified: {approved: {}}, 'Code-Review': {approved: {}}}}, - {_number: 2, labels: { - Verified: {approved: {}}, 'Library-Compliance': {approved: {}}}}, - ]]).length, 3); + {_number: 1, labels: { + 'Verified': {approved: {}}, 'Code-Review': {approved: {}}}}, + {_number: 2, labels: { + 'Verified': {approved: {}}, 'Library-Compliance': {approved: {}}}}, + ]]).length, 3); assert.equal(element._computeLabelShortcut('Code-Review'), 'CR'); assert.equal(element._computeLabelShortcut('Verified'), 'V'); @@ -119,17 +119,17 @@ 'Some-Special-Label-7'), 'SSL7'); }); - test('colspans', function() { - var thItemCount = Polymer.dom(element.root).querySelectorAll( + test('colspans', () => { + const thItemCount = Polymer.dom(element.root).querySelectorAll( 'th').length; - var changeTableColumns = []; - var labelNames = []; + const changeTableColumns = []; + const labelNames = []; assert.equal(thItemCount, element._computeColspan( changeTableColumns, labelNames)); }); - test('keyboard shortcuts', function(done) { + test('keyboard shortcuts', done => { element.selectedIndex = 0; element.changes = [ {_number: 0}, @@ -137,17 +137,17 @@ {_number: 2}, ]; flushAsynchronousOperations(); - var elementItems = Polymer.dom(element.root).querySelectorAll( + const elementItems = Polymer.dom(element.root).querySelectorAll( 'gr-change-list-item'); assert.equal(elementItems.length, 3); - flush(function() { + flush(() => { assert.isTrue(elementItems[0].hasAttribute('selected')); MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); assert.equal(element.selectedIndex, 1); MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); - var showStub = sinon.stub(page, 'show'); + const showStub = sinon.stub(page, 'show'); assert.equal(element.selectedIndex, 2); MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter'); assert(showStub.lastCall.calledWithExactly('/c/2/'), @@ -169,7 +169,7 @@ }); }); - test('changes needing review', function() { + test('changes needing review', () => { element.changes = [ { _number: 0, @@ -196,13 +196,13 @@ _number: 4, status: 'ABANDONED', owner: {_account_id: 0}, - } + }, ]; flushAsynchronousOperations(); - var elementItems = Polymer.dom(element.root).querySelectorAll( + let elementItems = Polymer.dom(element.root).querySelectorAll( 'gr-change-list-item'); assert.equal(elementItems.length, 5); - for (var i = 0; i < elementItems.length; i++) { + for (let i = 0; i < elementItems.length; i++) { assert.isFalse(elementItems[i].hasAttribute('needs-review')); } @@ -227,152 +227,152 @@ assert.isFalse(elementItems[4].hasAttribute('needs-review')); }); - test('no changes', function() { + test('no changes', () => { element.changes = []; flushAsynchronousOperations(); - var listItems = Polymer.dom(element.root).querySelectorAll( + const listItems = Polymer.dom(element.root).querySelectorAll( 'gr-change-list-item'); assert.equal(listItems.length, 0); - var noChangesMsg = Polymer.dom(element.root).querySelector('.noChanges'); + const noChangesMsg = + Polymer.dom(element.root).querySelector('.noChanges'); assert.ok(noChangesMsg); }); - test('empty groups', function() { - element.groups = [[], []]; + test('empty sections', () => { + element.sections = [[], []]; flushAsynchronousOperations(); - var listItems = Polymer.dom(element.root).querySelectorAll( + const listItems = Polymer.dom(element.root).querySelectorAll( 'gr-change-list-item'); assert.equal(listItems.length, 0); - var noChangesMsg = Polymer.dom(element.root).querySelectorAll( + const noChangesMsg = Polymer.dom(element.root).querySelectorAll( '.noChanges'); assert.equal(noChangesMsg.length, 2); }); - suite('empty column preference', function() { - var element; + suite('empty column preference', () => { + let element; - setup(function() { - return stubRestAPI({ + setup(() => + stubRestAPI({ legacycid_in_change_table: true, - time_format: 'HHMM_12', - change_table: [], - } - ).then(function() { + time_format: 'HHMM_12', + change_table: [], + }).then(() => { element = fixture('basic'); return element._loadPreferences(); - }); - }); + }) + ); - test('show number enabled', function() { + test('show number enabled', () => { assert.isTrue(element.showNumber); }); - test('all columns visible', function() { - element.columnNames.forEach(function(column) { - var elementClass = '.' + element._lowerCase(column); + test('all columns visible', () => { + for (const column of element.columnNames) { + const elementClass = '.' + element._lowerCase(column); assert.isFalse(element.$$(elementClass).hidden); - }); + } }); }); - suite('full column preference', function() { - var element; + suite('full column preference', () => { + let element; - setup(function() { + setup(() => { return stubRestAPI({ - legacycid_in_change_table: true, - time_format: 'HHMM_12', - change_table: [ - 'Subject', - 'Status', - 'Owner', - 'Project', - 'Branch', - 'Updated', - 'Size', - ], - }).then(function() { + legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [ + 'Subject', + 'Status', + 'Owner', + 'Project', + 'Branch', + 'Updated', + 'Size', + ], + }).then(() => { element = fixture('basic'); return element._loadPreferences(); }); }); - test('all columns visible', function() { - element.changeTableColumns.forEach(function(column) { - var elementClass = '.' + element._lowerCase(column); + test('all columns visible', () => { + for (const column of element.changeTableColumns) { + const elementClass = '.' + element._lowerCase(column); assert.isFalse(element.$$(elementClass).hidden); - }); + } }); }); - suite('partial column preference', function() { - var element; + suite('partial column preference', () => { + let element; - setup(function() { + setup(() => { return stubRestAPI({ - legacycid_in_change_table: true, - time_format: 'HHMM_12', - change_table: [ - 'Subject', - 'Status', - 'Owner', - 'Branch', - 'Updated', - 'Size', - ], - }).then(function() { + legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [ + 'Subject', + 'Status', + 'Owner', + 'Branch', + 'Updated', + 'Size', + ], + }).then(() => { element = fixture('basic'); return element._loadPreferences(); }); }); - test('all columns except project visible', function() { - element.changeTableColumns.forEach(function(column) { - var elementClass = '.' + column.toLowerCase(); + test('all columns except project visible', () => { + for (const column of element.changeTableColumns) { + const elementClass = '.' + column.toLowerCase(); if (column === 'Project') { assert.isTrue(element.$$(elementClass).hidden); } else { assert.isFalse(element.$$(elementClass).hidden); } - }); + } }); }); - suite('random column does not exist', function() { - var element; + suite('random column does not exist', () => { + let element; /* This would only exist if somebody manually updated the config file. */ - setup(function() { + setup(() => { return stubRestAPI({ - legacycid_in_change_table: true, - time_format: 'HHMM_12', - change_table: [ - 'Bad', - ], - }).then(function() { + legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [ + 'Bad', + ], + }).then(() => { element = fixture('basic'); return element._loadPreferences(); }); }); - test('bad column does not exist', function() { - var elementClass = '.bad'; + test('bad column does not exist', () => { + const elementClass = '.bad'; assert.isNotOk(element.$$(elementClass)); }); }); }); - suite('gr-change-list groups', function() { - var element; + suite('gr-change-list sections', () => { + let element; - setup(function() { + setup(() => { element = fixture('basic'); }); - test('keyboard shortcuts', function() { + test('keyboard shortcuts', () => { element.selectedIndex = 0; - element.groups = [ + element.sections = [ [ {_number: 0}, {_number: 1}, @@ -387,11 +387,21 @@ {_number: 6}, {_number: 7}, {_number: 8}, - ] + ], ]; - element.groupTitles = ['Group 1', 'Group 2', 'Group 3']; + element.sectionMetadata = [ + { + name: 'Group 1', + }, + { + name: 'Group 2', + }, + { + name: 'Group 3', + }, + ]; flushAsynchronousOperations(); - var elementItems = Polymer.dom(element.root).querySelectorAll( + const elementItems = Polymer.dom(element.root).querySelectorAll( 'gr-change-list-item'); assert.equal(elementItems.length, 9); @@ -399,7 +409,7 @@ assert.equal(element.selectedIndex, 1); MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' - var showStub = sinon.stub(page, 'show'); + const showStub = sinon.stub(page, 'show'); assert.equal(element.selectedIndex, 2); MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' assert(showStub.lastCall.calledWithExactly('/c/2/'), @@ -421,7 +431,7 @@ showStub.restore(); }); - test('assigned attribute set in each item', function() { + test('assigned attribute set in each item', () => { element.changes = [ { _number: 0, @@ -441,12 +451,24 @@ ]; element.account = {_account_id: 42}; flushAsynchronousOperations(); - var items = element._getListItems(); + const items = element._getListItems(); assert.equal(items.length, 3); - for (var i = 0; i < items.length; i++) { + for (let i = 0; i < items.length; i++) { assert.equal(items[i].hasAttribute('assigned'), items[i]._account_id === element.account._account_id); } }); + + test('_sectionHref', () => { + element.sectionMetadata = [ + {query: 'is:open owner:self'}, + {query: 'is:open ((reviewer:self -is:ignored) OR assignee:self)'}, + ]; + + assert.equal(element._sectionHref(10), null); + assert.equal(element._sectionHref(0), '/q/is:open+owner:self'); + assert.equal(element._sectionHref(1), + '/q/is:open+((reviewer:self+-is:ignored)+OR+assignee:self)'); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html index ce413ca..63a6e0b 100644 --- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html +++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -14,12 +14,14 @@ limitations under the License. --> +<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-dashboard-view"> <template> - <style> + <style include="shared-styles"> :host { background-color: var(--view-background-color); display: block; @@ -44,8 +46,8 @@ show-reviewed-state account="[[account]]" selected-index="{{viewState.selectedChangeIndex}}" - groups="{{_results}}" - group-titles="[[_groupTitles]]"></gr-change-list> + sections="{{_results}}" + section-metadata="[[sectionMetadata]]"></gr-change-list> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js index 977552a..be955f6 100644 --- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js +++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -14,6 +14,23 @@ (function() { 'use strict'; + const DEFAULT_SECTIONS = [ + { + name: 'Outgoing reviews', + query: 'is:open owner:self', + }, + { + name: 'Incoming reviews', + query: 'is:open ((reviewer:self -owner:self -is:ignored) OR ' + + 'assignee:self)', + }, + { + name: 'Recently closed', + query: 'is:closed (owner:self OR reviewer:self OR assignee:self) ' + + '-age:4w limit:10', + }, + ]; + Polymer({ is: 'gr-dashboard-view', @@ -26,7 +43,7 @@ properties: { account: { type: Object, - value: function() { return {}; }, + value() { return {}; }, }, viewState: Object, params: { @@ -35,13 +52,9 @@ }, _results: Array, - _groupTitles: { + sectionMetadata: { type: Array, - value: [ - 'Outgoing reviews', - 'Incoming reviews', - 'Recently closed', - ], + value() { return DEFAULT_SECTIONS; }, }, /** @@ -53,26 +66,42 @@ }, }, - attached: function() { + behaviors: [ + Gerrit.RESTClientBehavior, + ], + + get options() { + return this.listChangesOptionsToHex( + this.ListChangesOption.LABELS, + this.ListChangesOption.DETAILED_ACCOUNTS, + this.ListChangesOption.REVIEWED + ); + }, + + attached() { this.fire('title-change', {title: 'My Reviews'}); }, /** * Allows a refresh if menu item is selected again. */ - _paramsChanged: function() { + _paramsChanged() { this._loading = true; - this._getDashboardChanges().then(function(results) { + this._getChanges().then(results => { this._results = results; this._loading = false; - }.bind(this)).catch(function(err) { + }).catch(err => { this._loading = false; console.warn(err.message); - }.bind(this)); + }); }, - _getDashboardChanges: function() { - return this.$.restAPI.getDashboardChanges(); + _getChanges() { + return this.$.restAPI.getChanges( + null, + this.sectionMetadata.map(section => section.query), + null, + this.options); }, }); })();
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html index 718e59c..c67a50b 100644 --- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -20,7 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-dashboard-view.html"> <script>void(0);</script> @@ -32,18 +32,22 @@ </test-fixture> <script> - suite('gr-dashboard-view tests', function() { - var element; + suite('gr-dashboard-view tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { element = fixture('basic'); + sandbox = sinon.sandbox.create(); }); - test('content is refreshed with same dropdown selected twice', function() { - var getChangesStub = sinon.stub(element, '_getDashboardChanges', - function() { - return Promise.resolve(); - }); + teardown(() => { + sandbox.restore(); + }); + + test('content is refreshed with same dropdown selected twice', () => { + const getChangesStub = sandbox.stub(element, '_getChanges', + () => Promise.resolve()); element.params = {view: 'gr-dashboard-view'};
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html index bb4a520..096794a 100644 --- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html +++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.html
@@ -17,10 +17,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-account-entry"> <template> - <style> + <style include="shared-styles"> gr-autocomplete { display: inline-block; flex: 1; @@ -34,7 +35,8 @@ threshold="[[suggestFrom]]" query="[[query]]" on-commit="_handleInputCommit" - clear-on-commit> + clear-on-commit + warn-uncommitted> </gr-autocomplete> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template>
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js index 46f2ed2..a1aaac4 100644 --- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js +++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
@@ -22,7 +22,6 @@ * * @event add */ - properties: { borderless: Boolean, change: Object, @@ -39,14 +38,15 @@ */ allowAnyUser: Boolean, + // suggestFrom = 0 to enable default suggestions. suggestFrom: { type: Number, - value: 3, + value: 0, }, query: { type: Function, - value: function() { + value() { return this._getReviewerSuggestions.bind(this); }, }, @@ -56,22 +56,30 @@ return this.$.input.focusStart; }, - focus: function() { + focus() { this.$.input.focus(); }, - clear: function() { + clear() { this.$.input.clear(); }, - _handleInputCommit: function(e) { + setText(text) { + this.$.input.setText(text); + }, + + getText() { + return this.$.input.text; + }, + + _handleInputCommit(e) { this.fire('add', {value: e.detail.value}); }, - _makeSuggestion: function(reviewer) { - var name; - var value; - var generateStatusStr = function(account) { + _makeSuggestion(reviewer) { + let name; + let value; + const generateStatusStr = function(account) { return account.status ? ' (' + account.status + ')' : ''; }; if (reviewer.account) { @@ -89,22 +97,23 @@ generateStatusStr(reviewer); value = {account: reviewer, count: 1}; } - return {name: name, value: value}; + return {name, value}; }, - _getReviewerSuggestions: function(input) { - var api = this.$.restAPI; - var xhr = this.allowAnyUser ? + _getReviewerSuggestions(input) { + if (!this.change) { return Promise.resolve([]); } + const api = this.$.restAPI; + const xhr = this.allowAnyUser ? api.getSuggestedAccounts(input) : api.getChangeSuggestedReviewers(this.change._number, input); - return xhr.then(function(reviewers) { + return xhr.then(reviewers => { if (!reviewers) { return []; } if (!this.filter) { return reviewers.map(this._makeSuggestion); } return reviewers .filter(this.filter) .map(this._makeSuggestion.bind(this)); - }.bind(this)); + }); }, }); })();
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html index 2948b4b..55213c1 100644 --- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html +++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-account-entry.html"> <script>void(0);</script> @@ -34,11 +34,11 @@ </test-fixture> <script> - suite('gr-account-entry tests', function() { - var sandbox; - var _nextAccountId = 0; - var makeAccount = function(opt_status) { - var accountId = ++_nextAccountId; + suite('gr-account-entry tests', () => { + let sandbox; + let _nextAccountId = 0; + const makeAccount = function(opt_status) { + const accountId = ++_nextAccountId; return { _account_id: accountId, name: 'name ' + accountId, @@ -47,15 +47,15 @@ }; }; - var owner; - var existingReviewer1; - var existingReviewer2; - var suggestion1; - var suggestion2; - var suggestion3; - var element; + let owner; + let existingReviewer1; + let existingReviewer2; + let suggestion1; + let suggestion2; + let suggestion3; + let element; - setup(function() { + setup(() => { owner = makeAccount(); existingReviewer1 = makeAccount(); existingReviewer2 = makeAccount(); @@ -70,7 +70,7 @@ element = fixture('basic'); element.change = { - owner: owner, + owner, reviewers: { CC: [existingReviewer1], REVIEWER: [existingReviewer2], @@ -79,97 +79,107 @@ sandbox = sinon.sandbox.create(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - suite('stubbed values for _getReviewerSuggestions', function() { - setup(function() { + suite('stubbed values for _getReviewerSuggestions', () => { + setup(() => { stub('gr-rest-api-interface', { - getChangeSuggestedReviewers: function() { - var redundantSuggestion1 = {account: existingReviewer1}; - var redundantSuggestion2 = {account: existingReviewer2}; - var redundantSuggestion3 = {account: owner}; + getChangeSuggestedReviewers() { + const redundantSuggestion1 = {account: existingReviewer1}; + const redundantSuggestion2 = {account: existingReviewer2}; + const redundantSuggestion3 = {account: owner}; return Promise.resolve([redundantSuggestion1, redundantSuggestion2, - redundantSuggestion3, suggestion1, suggestion2, suggestion3]); + redundantSuggestion3, suggestion1, suggestion2, suggestion3]); }, }); }); - test('_makeSuggestion formats account or group accordingly', function() { - var account = makeAccount(); - var suggestion = element._makeSuggestion({account: account}); + test('_makeSuggestion formats account or group accordingly', () => { + let account = makeAccount(); + let suggestion = element._makeSuggestion({account}); assert.deepEqual(suggestion, { name: account.name + ' <' + account.email + '>', - value: {account: account}, + value: {account}, }); - var group = {name: 'test'}; - suggestion = element._makeSuggestion({group: group}); + const group = {name: 'test'}; + suggestion = element._makeSuggestion({group}); assert.deepEqual(suggestion, { name: group.name + ' (group)', - value: {group: group}, + value: {group}, }); suggestion = element._makeSuggestion(account); assert.deepEqual(suggestion, { name: account.name + ' <' + account.email + '>', - value: {account: account, count: 1}, + value: {account, count: 1}, }); account = makeAccount('OOO'); - suggestion = element._makeSuggestion({account: account}); + suggestion = element._makeSuggestion({account}); assert.deepEqual(suggestion, { name: account.name + ' <' + account.email + '> (OOO)', - value: {account: account}, + value: {account}, }); suggestion = element._makeSuggestion(account); assert.deepEqual(suggestion, { name: account.name + ' <' + account.email + '> (OOO)', - value: {account: account, count: 1}, + value: {account, count: 1}, }); }); - test('_getReviewerSuggestions excludes owner+reviewers', function(done) { - element._getReviewerSuggestions().then(function(reviewers) { + test('_getReviewerSuggestions excludes owner+reviewers', done => { + element._getReviewerSuggestions().then(reviewers => { // Default is no filtering. assert.equal(reviewers.length, 6); // Set up filter that only accepts suggestion1. - var accountId = suggestion1.account._account_id; + const accountId = suggestion1.account._account_id; element.filter = function(suggestion) { return suggestion.account && suggestion.account._account_id === accountId; }; - element._getReviewerSuggestions().then(function(reviewers) { + element._getReviewerSuggestions().then(reviewers => { assert.deepEqual(reviewers, [element._makeSuggestion(suggestion1)]); }).then(done); }); }); }); - test('allowAnyUser', function(done) { - var suggestReviewerStub = + test('allowAnyUser', done => { + const suggestReviewerStub = sandbox.stub(element.$.restAPI, 'getChangeSuggestedReviewers') .returns(Promise.resolve([])); - var suggestAccountStub = + const suggestAccountStub = sandbox.stub(element.$.restAPI, 'getSuggestedAccounts') .returns(Promise.resolve([])); - element._getReviewerSuggestions('').then(function() { + element._getReviewerSuggestions('').then(() => { assert.isTrue(suggestReviewerStub.calledOnce); assert.isFalse(suggestAccountStub.called); element.allowAnyUser = true; - element._getReviewerSuggestions('').then(function() { + element._getReviewerSuggestions('').then(() => { assert.isTrue(suggestReviewerStub.calledOnce); assert.isTrue(suggestAccountStub.calledOnce); done(); }); }); }); + + test('setText', () => { + // Spy on query, as that is called when _updateSuggestions proceeds. + const suggestSpy = sandbox.spy(element.$.input, 'query'); + element.setText('test text'); + flushAsynchronousOperations(); + + assert.equal(element.$.input.$.input.value, 'test text'); + assert.isFalse(suggestSpy.called); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html index 810658c..1dcae68 100644 --- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html +++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.html
@@ -17,10 +17,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html"> <link rel="import" href="../gr-account-entry/gr-account-entry.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-account-list"> <template> - <style> + <style include="shared-styles"> gr-account-chip { display: inline-block; margin: 0 .2em .2em 0; @@ -44,7 +45,7 @@ data-account-id$="[[account._account_id]]" removable="[[_computeRemovable(account)]]" on-keydown="_handleChipKeydown" - tabindex$="[[index]]"> + tabindex="-1"> </gr-account-chip> </template> <gr-account-entry
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js index 35311f9..4e403e6 100644 --- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js +++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
@@ -14,13 +14,21 @@ (function() { 'use strict'; + const VALID_EMAIL_ALERT = 'Please input a valid email.'; + Polymer({ is: 'gr-account-list', + /** + * Fired when user inputs an invalid email address. + * + * @event show-alert + */ + properties: { accounts: { type: Array, - value: function() { return []; }, + value() { return []; }, notify: true, }, change: Object, @@ -35,6 +43,7 @@ type: Boolean, value: false, }, + /** * When true, the account-entry autocomplete uses the account suggest API * endpoint, which suggests any account in that Gerrit instance (and does @@ -48,6 +57,15 @@ type: Boolean, value: false, }, + + /** + * When true, allows for non-suggested inputs to be added. + */ + allowAnyInput: { + type: Boolean, + value: false, + }, + /** * Array of values (groups/accounts) that are removable. When this prop is * undefined, all values are removable. @@ -60,7 +78,7 @@ }, listeners: { - 'remove': '_handleRemove', + remove: '_handleRemove', }, get accountChips() { @@ -71,36 +89,52 @@ return this.$.entry.focusStart; }, - _handleAdd: function(e) { - var reviewer = e.detail.value; + _handleAdd(e) { + this._addReviewer(e.detail.value); + }, + + _addReviewer(reviewer) { // Append new account or group to the accounts property. We add our own // internal properties to the account/group here, so we clone the object // to avoid cluttering up the shared change object. - // TODO(logan): Polyfill for Object.assign in IE. if (reviewer.account) { - var account = Object.assign({}, reviewer.account, {_pendingAdd: true}); + const account = + Object.assign({}, reviewer.account, {_pendingAdd: true}); this.push('accounts', account); } else if (reviewer.group) { if (reviewer.confirm) { this.pendingConfirmation = reviewer; return; } - var group = Object.assign({}, reviewer.group, + const group = Object.assign({}, reviewer.group, {_pendingAdd: true, _group: true}); this.push('accounts', group); + } else if (this.allowAnyInput) { + if (!reviewer.includes('@')) { + // Repopulate the input with what the user tried to enter and have + // a toast tell them why they can't enter it. + this.$.entry.setText(reviewer); + this.dispatchEvent(new CustomEvent('show-alert', + {detail: {message: VALID_EMAIL_ALERT}, bubbles: true})); + return false; + } else { + const account = {email: reviewer, _pendingAdd: true}; + this.push('accounts', account); + } } this.pendingConfirmation = null; + return true; }, - confirmGroup: function(group) { + confirmGroup(group) { group = Object.assign( {}, group, {confirmed: true, _pendingAdd: true, _group: true}); this.push('accounts', group); this.pendingConfirmation = null; }, - _computeChipClass: function(account) { - var classes = []; + _computeChipClass(account) { + const classes = []; if (account._group) { classes.push('group'); } @@ -110,11 +144,23 @@ return classes.join(' '); }, - _computeRemovable: function(account) { + _accountMatches(a, b) { + if (a && b) { + if (a._account_id) { + return a._account_id === b._account_id; + } + if (a.email) { + return a.email === b.email; + } + } + return a === b; + }, + + _computeRemovable(account) { if (this.readonly) { return false; } if (this.removableValues) { - for (var i = 0; i < this.removableValues.length; i++) { - if (this.removableValues[i]._account_id === account._account_id) { + for (let i = 0; i < this.removableValues.length; i++) { + if (this._accountMatches(this.removableValues[i], account)) { return true; } } @@ -123,21 +169,21 @@ return true; }, - _handleRemove: function(e) { - var toRemove = e.detail.account; + _handleRemove(e) { + const toRemove = e.detail.account; this._removeAccount(toRemove); this.$.entry.focus(); }, - _removeAccount: function(toRemove) { + _removeAccount(toRemove) { if (!toRemove || !this._computeRemovable(toRemove)) { return; } - for (var i = 0; i < this.accounts.length; i++) { - var matches; - var account = this.accounts[i]; + for (let i = 0; i < this.accounts.length; i++) { + let matches; + const account = this.accounts[i]; if (toRemove._group) { matches = toRemove.id === account.id; } else { - matches = toRemove._account_id === account._account_id; + matches = this._accountMatches(toRemove, account); } if (matches) { this.splice('accounts', i, 1); @@ -147,8 +193,8 @@ console.warn('received remove event for missing account', toRemove); }, - _handleInputKeydown: function(e) { - var input = e.detail.input; + _handleInputKeydown(e) { + const input = e.detail.input; if (input.selectionStart !== input.selectionEnd || input.selectionStart !== 0) { return; @@ -158,18 +204,17 @@ this._removeAccount(this.accounts[this.accounts.length - 1]); break; case 37: // Left arrow - var chips = this.accountChips; - if (chips[chips.length - 1]) { - chips[chips.length - 1].focus(); + if (this.accountChips[this.accountChips.length - 1]) { + this.accountChips[this.accountChips.length - 1].focus(); } break; } }, - _handleChipKeydown: function(e) { - var chip = e.target; - var chips = this.accountChips; - var index = chips.indexOf(chip); + _handleChipKeydown(e) { + const chip = e.target; + const chips = this.accountChips; + const index = chips.indexOf(chip); switch (e.keyCode) { case 8: // Backspace case 13: // Enter @@ -204,19 +249,35 @@ } }, - additions: function() { - return this.accounts.filter(function(account) { + /** + * Submit the text of the entry as a reviewer value, if it exists. If it is + * a successful submit of the text, clear the entry value. + * + * @return {boolean} If there is text in the entry, return true if the + * submission was successful and false if not. If there is no text, + * return true. + */ + submitEntryText() { + const text = this.$.entry.getText(); + if (!text.length) { return true; } + const wasSubmitted = this._addReviewer(text); + if (wasSubmitted) { this.$.entry.clear(); } + return wasSubmitted; + }, + + additions() { + return this.accounts.filter(account => { return account._pendingAdd; - }).map(function(account) { + }).map(account => { if (account._group) { return {group: account}; } else { - return {account: account}; + return {account}; } }); }, - _computeEntryHidden: function(maxCount, accountsRecord, readonly) { + _computeEntryHidden(maxCount, accountsRecord, readonly) { return (maxCount && maxCount <= accountsRecord.base.length) || readonly; }, });
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html index b3c9e9e..a3a3c64 100644 --- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-account-list.html"> <script>void(0);</script> @@ -33,64 +32,65 @@ </test-fixture> <script> - suite('gr-account-list tests', function() { - var _nextAccountId = 0; - var makeAccount = function() { - var accountId = ++_nextAccountId; + suite('gr-account-list tests', () => { + let _nextAccountId = 0; + const makeAccount = function() { + const accountId = ++_nextAccountId; return { _account_id: accountId, }; }; - var makeGroup = function() { - var groupId = 'group' + (++_nextAccountId); + const makeGroup = function() { + const groupId = 'group' + (++_nextAccountId); return { id: groupId, + _group: true, }; }; - var existingReviewer1; - var existingReviewer2; - var sandbox; - var element; + let existingReviewer1; + let existingReviewer2; + let sandbox; + let element; function getChips() { return Polymer.dom(element.root).querySelectorAll('gr-account-chip'); } - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); existingReviewer1 = makeAccount(); existingReviewer2 = makeAccount(); stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, + getConfig() { return Promise.resolve({}); }, }); element = fixture('basic'); element.accounts = [existingReviewer1, existingReviewer2]; }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('account entry only appears when editable', function() { + test('account entry only appears when editable', () => { element.readonly = false; assert.isFalse(element.$.entry.hasAttribute('hidden')); element.readonly = true; assert.isTrue(element.$.entry.hasAttribute('hidden')); }); - test('addition and removal of account/group chips', function() { + test('addition and removal of account/group chips', () => { flushAsynchronousOperations(); sandbox.stub(element, '_computeRemovable').returns(true); // Existing accounts are listed. - var chips = getChips(); + let chips = getChips(); assert.equal(chips.length, 2); assert.isFalse(chips[0].classList.contains('pendingAdd')); assert.isFalse(chips[1].classList.contains('pendingAdd')); // New accounts are added to end with pendingAdd class. - var newAccount = makeAccount(); + const newAccount = makeAccount(); element._handleAdd({ detail: { value: { @@ -122,7 +122,7 @@ assert.isFalse(chips[0].classList.contains('pendingAdd')); // New groups are added to end with pendingAdd and group classes. - var newGroup = makeGroup(); + const newGroup = makeGroup(); element._handleAdd({ detail: { value: { @@ -144,8 +144,8 @@ assert.isFalse(chips[0].classList.contains('pendingAdd')); }); - test('_computeChipClass', function() { - var account = makeAccount(); + test('_computeChipClass', () => { + const account = makeAccount(); assert.equal(element._computeChipClass(account), ''); account._pendingAdd = true; assert.equal(element._computeChipClass(account), 'pendingAdd'); @@ -155,8 +155,8 @@ assert.equal(element._computeChipClass(account), 'group'); }); - test('_computeRemovable', function() { - var newAccount = makeAccount(); + test('_computeRemovable', () => { + const newAccount = makeAccount(); newAccount._pendingAdd = true; element.readonly = false; element.removableValues = []; @@ -174,10 +174,34 @@ assert.isFalse(element._computeRemovable(newAccount)); }); - test('additions returns sanitized new accounts and groups', function() { + test('submitEntryText', () => { + element.allowAnyInput = true; + flushAsynchronousOperations(); + + const getTextStub = sandbox.stub(element.$.entry, 'getText'); + getTextStub.onFirstCall().returns(''); + getTextStub.onSecondCall().returns('test'); + getTextStub.onThirdCall().returns('test@test'); + + // When entry is empty, return true. + const clearStub = sandbox.stub(element.$.entry, 'clear'); + assert.isTrue(element.submitEntryText()); + assert.isFalse(clearStub.called); + + // When entry is invalid, return false. + assert.isFalse(element.submitEntryText()); + assert.isFalse(clearStub.called); + + // When entry is valid, return true and clear text. + assert.isTrue(element.submitEntryText()); + assert.isTrue(clearStub.called); + assert.equal(element.additions()[0].account.email, 'test@test'); + }); + + test('additions returns sanitized new accounts and groups', () => { assert.equal(element.additions().length, 0); - var newAccount = makeAccount(); + const newAccount = makeAccount(); element._handleAdd({ detail: { value: { @@ -185,7 +209,7 @@ }, }, }); - var newGroup = makeGroup(); + const newGroup = makeGroup(); element._handleAdd({ detail: { value: { @@ -211,13 +235,13 @@ ]); }); - test('large group confirmations', function() { + test('large group confirmations', () => { assert.isNull(element.pendingConfirmation); assert.deepEqual(element.additions(), []); - var group = makeGroup(); - var reviewer = { - group: group, + const group = makeGroup(); + const reviewer = { + group, count: 10, confirm: true, }; @@ -244,17 +268,17 @@ ]); }); - test('removeAccount fails if account is not removable', function() { + test('removeAccount fails if account is not removable', () => { element.readonly = true; - var acct = makeAccount(); + const acct = makeAccount(); element.accounts = [acct]; element._removeAccount(acct); assert.equal(element.accounts.length, 1); }); - test('max-count', function() { + test('max-count', () => { element.maxCount = 1; - var acct = makeAccount(); + const acct = makeAccount(); element._handleAdd({ detail: { value: { @@ -266,10 +290,47 @@ assert.isTrue(element.$.entry.hasAttribute('hidden')); }); - suite('keyboard interactions', function() { + suite('allowAnyInput', () => { + let entry; - test('backspace at text input start removes last account', function() { - var input = element.$.entry.$.input; + setup(() => { + entry = element.$.entry; + sandbox.stub(entry, '_getReviewerSuggestions'); + sandbox.stub(entry.$.input, '_updateSuggestions'); + element.allowAnyInput = true; + }); + + test('adds emails', () => { + const accountLen = element.accounts.length; + element._handleAdd({detail: {value: 'test@test'}}); + assert.equal(element.accounts.length, accountLen + 1); + assert.equal(element.accounts[accountLen].email, 'test@test'); + }); + + test('toasts on invalid email', () => { + const toastHandler = sandbox.stub(); + element.addEventListener('show-alert', toastHandler); + element._handleAdd({detail: {value: 'test'}}); + assert.isTrue(toastHandler.called); + }); + }); + + test('_accountMatches', () => { + const acct = makeAccount(); + + assert.isTrue(element._accountMatches(acct, acct)); + acct.email = 'test'; + assert.isTrue(element._accountMatches(acct, acct)); + assert.isTrue(element._accountMatches({email: 'test'}, acct)); + + assert.isFalse(element._accountMatches({}, acct)); + assert.isFalse(element._accountMatches({email: 'test2'}, acct)); + assert.isFalse(element._accountMatches({_account_id: -1}, acct)); + }); + + suite('keyboard interactions', () => { + test('backspace at text input start removes last account', () => { + const input = element.$.entry.$.input; sandbox.stub(element.$.entry, '_getReviewerSuggestions'); sandbox.stub(input, '_updateSuggestions'); sandbox.stub(element, '_computeRemovable').returns(true); @@ -287,17 +348,17 @@ assert.equal(element.accounts.length, 1); }); - test('arrow key navigation', function() { - var input = element.$.entry.$.input; + test('arrow key navigation', () => { + const input = element.$.entry.$.input; input.text = ''; element.accounts = [makeAccount(), makeAccount()]; MockInteractions.focus(input.$.input); flushAsynchronousOperations(); - var chips = element.accountChips; - var chipsOneSpy = sandbox.spy(chips[1], 'focus'); + const chips = element.accountChips; + const chipsOneSpy = sandbox.spy(chips[1], 'focus'); MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left assert.isTrue(chipsOneSpy.called); - var chipsZeroSpy = sandbox.spy(chips[0], 'focus'); + const chipsZeroSpy = sandbox.spy(chips[0], 'focus'); MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left assert.isTrue(chipsZeroSpy.called); MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left @@ -306,12 +367,11 @@ assert.isTrue(chipsOneSpy.calledTwice); }); - test('delete', function(done) { + test('delete', done => { element.accounts = [makeAccount(), makeAccount()]; - flush(function() { - var chips = element.accountChips; - var focusSpy = sandbox.spy(element.accountChips[1], 'focus'); - var removeSpy = sandbox.spy(element, '_removeAccount'); + flush(() => { + const focusSpy = sandbox.spy(element.accountChips[1], 'focus'); + const removeSpy = sandbox.spy(element, '_removeAccount'); MockInteractions.pressAndReleaseKeyOn( element.accountChips[0], 8); // Backspace assert.isTrue(focusSpy.called);
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html index 71ccb04..cbf75cb 100644 --- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -14,10 +14,13 @@ limitations under the License. --> +<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> + <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> @@ -28,10 +31,11 @@ <link rel="import" href="../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html"> <link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html"> <link rel="import" href="../gr-confirm-revert-dialog/gr-confirm-revert-dialog.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-change-actions"> <template> - <style> + <style include="shared-styles"> :host { display: inline-block; font-family: var(--font-family); @@ -78,11 +82,11 @@ [[_actionLoadingMessage]]</span> <gr-dropdown id="moreActions" + tabindex="0" down-arrow vertical-offset="32" horizontal-align="right" - on-tap-item-cherrypick="_handleCherrypickTap" - on-tap-item-delete="_handleDeleteTap" + on-tap-item="_handleOveflowItemTap" hidden$="[[_shouldHideActions(_menuActions.*, _loading)]]" disabled-ids="[[_disabledMenuActions]]" items="[[_menuActions]]">More</gr-dropdown>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js index 2b0916d..c5660ac 100644 --- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -17,7 +17,7 @@ /** * @enum {number} */ - var LabelStatus = { + const LabelStatus = { /** * This label provides what is necessary for submission. */ @@ -44,41 +44,49 @@ }; // TODO(davido): Add the rest of the change actions. - var ChangeActions = { + const ChangeActions = { ABANDON: 'abandon', DELETE: '/', + IGNORE: 'ignore', + MUTE: 'mute', + PRIVATE: 'private', + PRIVATE_DELETE: 'private.delete', RESTORE: 'restore', REVERT: 'revert', + UNIGNORE: 'unignore', + UNMUTE: 'unmute', + WIP: 'wip', }; // TODO(andybons): Add the rest of the revision actions. - var RevisionActions = { + const RevisionActions = { CHERRYPICK: 'cherrypick', DELETE: '/', PUBLISH: 'publish', REBASE: 'rebase', SUBMIT: 'submit', + DOWNLOAD: 'download', }; - var ActionLoadingLabels = { - 'abandon': 'Abandoning...', - 'cherrypick': 'Cherry-Picking...', - 'delete': 'Deleting...', - 'publish': 'Publishing...', - 'rebase': 'Rebasing...', - 'restore': 'Restoring...', - 'revert': 'Reverting...', - 'submit': 'Submitting...', + const ActionLoadingLabels = { + abandon: 'Abandoning...', + cherrypick: 'Cherry-Picking...', + delete: 'Deleting...', + publish: 'Publishing...', + rebase: 'Rebasing...', + restore: 'Restoring...', + revert: 'Reverting...', + submit: 'Submitting...', }; - var ActionType = { + const ActionType = { CHANGE: 'change', REVISION: 'revision', }; - var ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_'; + const ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_'; - var QUICK_APPROVE_ACTION = { + const QUICK_APPROVE_ACTION = { __key: 'review', __type: 'change', enabled: true, @@ -87,14 +95,22 @@ method: 'POST', }; - /** - * Keys for actions to appear in the overflow menu rather than the top-level - * set of action buttons. - */ - var MENU_ACTION_KEYS = [ - 'cherrypick', - '/', // '/' is the key for the delete action. - ]; + const ActionPriority = { + CHANGE: 2, + DEFAULT: 0, + PRIMARY: 3, + REVIEW: -3, + REVISION: 1, + }; + + const DOWNLOAD_ACTION = { + enabled: true, + label: 'Download patch', + title: 'Open download dialog', + __key: 'download', + __primary: false, + __type: 'revision', + }; Polymer({ is: 'gr-change-actions', @@ -111,15 +127,21 @@ * @event <action key>-tap */ + /** + * Fires to show an alert when a send is attempted on the non-latest patch. + * + * @event show-alert + */ + properties: { change: Object, actions: { type: Object, - value: function() { return {}; }, + value() { return {}; }, }, primaryActionKeys: { type: Array, - value: function() { + value() { return [ RevisionActions.PUBLISH, RevisionActions.SUBMIT, @@ -144,7 +166,7 @@ }, revisionActions: { type: Object, - value: function() { return {}; }, + value() { return {}; }, }, _loading: { @@ -158,36 +180,95 @@ _allActionValues: { type: Array, computed: '_computeAllActions(actions.*, revisionActions.*,' + - 'primaryActionKeys.*, _additionalActions.*, change)', + 'primaryActionKeys.*, _additionalActions.*, change, ' + + '_actionPriorityOverrides.*)', }, _topLevelActions: { type: Array, computed: '_computeTopLevelActions(_allActionValues.*, ' + - '_hiddenActions.*)', + '_hiddenActions.*, _overflowActions.*)', }, _menuActions: { type: Array, - computed: '_computeMenuActions(_allActionValues.*, _hiddenActions.*)', + computed: '_computeMenuActions(_allActionValues.*, _hiddenActions.*, ' + + '_overflowActions.*)', + }, + _overflowActions: { + type: Array, + value() { + const value = [ + { + type: ActionType.CHANGE, + key: ChangeActions.WIP, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.DELETE, + }, + { + type: ActionType.REVISION, + key: RevisionActions.DELETE, + }, + { + type: ActionType.REVISION, + key: RevisionActions.CHERRYPICK, + }, + { + type: ActionType.REVISION, + key: RevisionActions.DOWNLOAD, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.IGNORE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.UNIGNORE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.MUTE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.UNMUTE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.PRIVATE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.PRIVATE_DELETE, + }, + ]; + return value; + }, + }, + _actionPriorityOverrides: { + type: Array, + value() { return []; }, }, _additionalActions: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _hiddenActions: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _disabledMenuActions: { type: Array, - value: function() { return []; }, + value() { return []; }, }, }, - ActionType: ActionType, - ChangeActions: ChangeActions, - RevisionActions: RevisionActions, + ActionType, + ChangeActions, + RevisionActions, behaviors: [ + Gerrit.PatchSetBehavior, Gerrit.RESTClientBehavior, ], @@ -195,53 +276,54 @@ '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)', ], - ready: function() { + ready() { this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this); this._loading = false; }, - reload: function() { + reload() { if (!this.changeNum || !this.patchNum) { return Promise.resolve(); } this._loading = true; - return this._getRevisionActions().then(function(revisionActions) { + return this._getRevisionActions().then(revisionActions => { if (!revisionActions) { return; } this.revisionActions = revisionActions; this._loading = false; - }.bind(this)).catch(function(err) { + }).catch(err => { alert('Couldn’t load revision actions. Check the console ' + 'and contact the PolyGerrit team for assistance.'); this._loading = false; throw err; - }.bind(this)); + }); }, - addActionButton: function(type, label) { + addActionButton(type, label) { if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { - throw Error('Invalid action type: ' + type); + throw Error(`Invalid action type: ${type}`); } - var action = { + const action = { enabled: true, - label: label, + label, __type: type, - __key: ADDITIONAL_ACTION_KEY_PREFIX + Math.random().toString(36), + __key: ADDITIONAL_ACTION_KEY_PREFIX + + Math.random().toString(36).substr(2), }; this.push('_additionalActions', action); return action.__key; }, - removeActionButton: function(key) { - var idx = this._indexOfActionButtonWithKey(key); + removeActionButton(key) { + const idx = this._indexOfActionButtonWithKey(key); if (idx === -1) { return; } this.splice('_additionalActions', idx, 1); }, - setActionButtonProp: function(key, prop, value) { + setActionButtonProp(key, prop, value) { this.set([ '_additionalActions', this._indexOfActionButtonWithKey(key), @@ -249,12 +331,48 @@ ], value); }, - setActionHidden: function(type, key, hidden) { + setActionOverflow(type, key, overflow) { if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { - throw Error('Invalid action type given: ' + type); + throw Error(`Invalid action type given: ${type}`); + } + const index = this._getActionOverflowIndex(type, key); + const action = { + type, + key, + overflow, + }; + if (!overflow && index !== -1) { + this.splice('_overflowActions', index, 1); + } else if (overflow) { + this.push('_overflowActions', action); + } + }, + + setActionPriority(type, key, priority) { + if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { + throw Error(`Invalid action type given: ${type}`); + } + const index = this._actionPriorityOverrides.findIndex(action => { + return action.type === type && action.key === key; + }); + const action = { + type, + key, + priority, + }; + if (index !== -1) { + this.set('_actionPriorityOverrides', index, action); + } else { + this.push('_actionPriorityOverrides', action); + } + }, + + setActionHidden(type, key, hidden) { + if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { + throw Error(`Invalid action type given: ${type}`); } - var idx = this._hiddenActions.indexOf(key); + const idx = this._hiddenActions.indexOf(key); if (hidden && idx === -1) { this.push('_hiddenActions', key); } else if (!hidden && idx !== -1) { @@ -262,8 +380,8 @@ } }, - _indexOfActionButtonWithKey: function(key) { - for (var i = 0; i < this._additionalActions.length; i++) { + _indexOfActionButtonWithKey(key) { + for (let i = 0; i < this._additionalActions.length; i++) { if (this._additionalActions[i].__key === key) { return i; } @@ -271,37 +389,43 @@ return -1; }, - _getRevisionActions: function() { + _getRevisionActions() { return this.$.restAPI.getChangeRevisionActions(this.changeNum, this.patchNum); }, - _shouldHideActions: function(actions, loading) { + _shouldHideActions(actions, loading) { return loading || !actions || !actions.base || !actions.base.length; }, - _keyCount: function(changeRecord) { + _keyCount(changeRecord) { return Object.keys((changeRecord && changeRecord.base) || {}).length; }, - _actionsChanged: function(actionsChangeRecord, revisionActionsChangeRecord, + _actionsChanged(actionsChangeRecord, revisionActionsChangeRecord, additionalActionsChangeRecord) { - var additionalActions = (additionalActionsChangeRecord && + const additionalActions = (additionalActionsChangeRecord && additionalActionsChangeRecord.base) || []; this.hidden = this._keyCount(actionsChangeRecord) === 0 && this._keyCount(revisionActionsChangeRecord) === 0 && additionalActions.length === 0; this._actionLoadingMessage = null; this._disabledMenuActions = []; + + const revisionActions = revisionActionsChangeRecord.base || {}; + if (Object.keys(revisionActions).length !== 0 && + !revisionActions.download) { + this.set('revisionActions.download', DOWNLOAD_ACTION); + } }, - _getValuesFor: function(obj) { - return Object.keys(obj).map(function(key) { + _getValuesFor(obj) { + return Object.keys(obj).map(key => { return obj[key]; }); }, - _getLabelStatus: function(label) { + _getLabelStatus(label) { if (label.approved) { return LabelStatus.OK; } else if (label.rejected) { @@ -319,21 +443,21 @@ * * @return {{label: string, score: string}} */ - _getTopMissingApproval: function() { + _getTopMissingApproval() { if (!this.change || !this.change.labels || !this.change.permitted_labels) { return null; } - var result; - for (var label in this.change.labels) { + let result; + for (const label in this.change.labels) { if (!(label in this.change.permitted_labels)) { continue; } if (this.change.permitted_labels[label].length === 0) { continue; } - var status = this._getLabelStatus(this.change.labels[label]); + const status = this._getLabelStatus(this.change.labels[label]); if (status === LabelStatus.NEED) { if (result) { // More than one label is missing, so it's unclear which to quick @@ -342,33 +466,33 @@ } result = label; } else if (status === LabelStatus.REJECT || - status === LabelStatus.IMPOSSIBLE) { + status === LabelStatus.IMPOSSIBLE) { return null; } } if (result) { - var score = this.change.permitted_labels[result].slice(-1)[0]; - var maxScore = + const score = this.change.permitted_labels[result].slice(-1)[0]; + const maxScore = Object.keys(this.change.labels[result].values).slice(-1)[0]; if (score === maxScore) { // Allow quick approve only for maximal score. return { label: result, - score: score, + score, }; } } return null; }, - _getQuickApproveAction: function() { - var approval = this._getTopMissingApproval(); + _getQuickApproveAction() { + const approval = this._getTopMissingApproval(); if (!approval) { return null; } - var action = Object.assign({}, QUICK_APPROVE_ACTION); + const action = Object.assign({}, QUICK_APPROVE_ACTION); action.label = approval.label + approval.score; - var review = { + const review = { drafts: 'PUBLISH_ALL_REVISIONS', labels: {}, }; @@ -377,20 +501,20 @@ return action; }, - _getActionValues: function(actionsChangeRecord, primariesChangeRecord, + _getActionValues(actionsChangeRecord, primariesChangeRecord, additionalActionsChangeRecord, type) { if (!actionsChangeRecord || !primariesChangeRecord) { return []; } - var actions = actionsChangeRecord.base || {}; - var primaryActionKeys = primariesChangeRecord.base || []; - var result = []; - var values = this._getValuesFor( + const actions = actionsChangeRecord.base || {}; + const primaryActionKeys = primariesChangeRecord.base || []; + const result = []; + const values = this._getValuesFor( type === ActionType.CHANGE ? ChangeActions : RevisionActions); - for (var a in actions) { - if (values.indexOf(a) === -1) { continue; } + for (const a in actions) { + if (!values.includes(a)) { continue; } actions[a].__key = a; actions[a].__type = type; - actions[a].__primary = primaryActionKeys.indexOf(a) !== -1; + actions[a].__primary = primaryActionKeys.includes(a); if (actions[a].label === 'Delete') { // This label is common within change and revision actions. Make it // more explicit to the user. @@ -405,32 +529,30 @@ result.push(Object.assign({}, actions[a])); } - var additionalActions = (additionalActionsChangeRecord && + let additionalActions = (additionalActionsChangeRecord && additionalActionsChangeRecord.base) || []; - additionalActions = additionalActions.filter(function(a) { + additionalActions = additionalActions.filter(a => { return a.__type === type; - }).map(function(a) { - a.__primary = primaryActionKeys.indexOf(a.__key) !== -1; + }).map(a => { + a.__primary = primaryActionKeys.includes(a.__key); // Triggers a re-render by ensuring object inequality. - // TODO(andybons): Polyfill for Object.assign. return Object.assign({}, a); }); return result.concat(additionalActions); }, - _computeLoadingLabel: function(action) { + _computeLoadingLabel(action) { return ActionLoadingLabels[action] || 'Working...'; }, - _canSubmitChange: function() { + _canSubmitChange() { return this.$.jsAPI.canSubmitChange(this.change, this._getRevision(this.change, this.patchNum)); }, - _getRevision: function(change, patchNum) { - var num = window.parseInt(patchNum, 10); - for (var hash in change.revisions) { - var rev = change.revisions[hash]; + _getRevision(change, patchNum) { + const num = window.parseInt(patchNum, 10); + for (const rev of Object.values(change.revisions)) { if (rev._number === num) { return rev; } @@ -438,62 +560,101 @@ return null; }, - _modifyRevertMsg: function() { + _modifyRevertMsg() { return this.$.jsAPI.modifyRevertMsg(this.change, this.$.confirmRevertDialog.message, this.commitMessage); }, - showRevertDialog: function() { + showRevertDialog() { this.$.confirmRevertDialog.populateRevertMessage( this.commitMessage, this.change.current_revision); this.$.confirmRevertDialog.message = this._modifyRevertMsg(); this._showActionDialog(this.$.confirmRevertDialog); }, - _handleActionTap: function(e) { + _handleActionTap(e) { e.preventDefault(); - var el = Polymer.dom(e).rootTarget; - var key = el.getAttribute('data-action-key'); - if (key.indexOf(ADDITIONAL_ACTION_KEY_PREFIX) === 0) { - this.fire(key + '-tap', {node: el}); + const el = Polymer.dom(e).rootTarget; + const key = el.getAttribute('data-action-key'); + if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX)) { + this.fire(`${key}-tap`, {node: el}); return; } - var type = el.getAttribute('data-action-type'); - if (type === ActionType.REVISION) { - this._handleRevisionAction(key); - } else if (key === ChangeActions.REVERT) { - this.showRevertDialog(); - } else if (key === ChangeActions.ABANDON) { - this._showActionDialog(this.$.confirmAbandonDialog); - } else if (key === QUICK_APPROVE_ACTION.key) { - var action = this._allActionValues.find(function(o) { - return o.key === key; - }); - this._fireAction( - this._prependSlash(key), action, true, action.payload); - } else { - this._fireAction(this._prependSlash(key), this.actions[key], false); + const type = el.getAttribute('data-action-type'); + this._handleAction(type, key); + }, + + _handleOveflowItemTap(e) { + this._handleAction(e.detail.action.__type, e.detail.action.__key); + }, + + _handleAction(type, key) { + switch (type) { + case ActionType.REVISION: + this._handleRevisionAction(key); + break; + case ActionType.CHANGE: + this._handleChangeAction(key); + break; + default: + this._fireAction(this._prependSlash(key), this.actions[key], false); } }, - _handleRevisionAction: function(key) { + _handleChangeAction(key) { + let action; + switch (key) { + case ChangeActions.REVERT: + this.showRevertDialog(); + break; + case ChangeActions.ABANDON: + this._showActionDialog(this.$.confirmAbandonDialog); + break; + case QUICK_APPROVE_ACTION.key: + action = this._allActionValues.find(o => { + return o.key === key; + }); + this._fireAction( + this._prependSlash(key), action, true, action.payload); + break; + case ChangeActions.DELETE: + this._handleDeleteTap(); + break; + case ChangeActions.WIP: + this._handleWipTap(); + break; + default: + this._fireAction(this._prependSlash(key), this.actions[key], false); + } + }, + + _handleRevisionAction(key) { switch (key) { case RevisionActions.REBASE: this._showActionDialog(this.$.confirmRebase); break; + case RevisionActions.DELETE: + this._handleDeleteConfirm(); + break; + case RevisionActions.CHERRYPICK: + this._handleCherrypickTap(); + break; + case RevisionActions.DOWNLOAD: + this._handleDownloadTap(); + break; case RevisionActions.SUBMIT: if (!this._canSubmitChange()) { return; } - /* falls through */ // required by JSHint + // eslint-disable-next-line no-fallthrough default: this._fireAction(this._prependSlash(key), this.revisionActions[key], true); } }, - _prependSlash: function(key) { - return key === '/' ? key : '/' + key; + _prependSlash(key) { + return key === '/' ? key : `/${key}`; }, /** @@ -501,40 +662,38 @@ * returns false otherwise. * @return {boolean} hasParent */ - _computeChainState: function(hasParent) { + _computeChainState(hasParent) { this._hasKnownChainState = true; }, - _calculateDisabled: function(action, hasKnownChainState) { + _calculateDisabled(action, hasKnownChainState) { if (action.__key === 'rebase' && hasKnownChainState === false) { return true; } return !action.enabled; }, - _handleConfirmDialogCancel: function() { + _handleConfirmDialogCancel() { this._hideAllDialogs(); }, - _hideAllDialogs: function() { - var dialogEls = + _hideAllDialogs() { + const dialogEls = Polymer.dom(this.root).querySelectorAll('.confirmDialog'); - for (var i = 0; i < dialogEls.length; i++) { - dialogEls[i].hidden = true; - } + for (const dialogEl of dialogEls) { dialogEl.hidden = true; } this.$.overlay.close(); }, - _handleRebaseConfirm: function() { - var el = this.$.confirmRebase; - var payload = {base: el.base}; + _handleRebaseConfirm() { + const el = this.$.confirmRebase; + const payload = {base: el.base}; this.$.overlay.close(); el.hidden = true; this._fireAction('/rebase', this.revisionActions.rebase, true, payload); }, - _handleCherrypickConfirm: function() { - var el = this.$.confirmCherrypick; + _handleCherrypickConfirm() { + const el = this.$.confirmCherrypick; if (!el.branch) { // TODO(davido): Fix error handling alert('The destination branch can’t be empty.'); @@ -557,31 +716,37 @@ ); }, - _handleRevertDialogConfirm: function() { - var el = this.$.confirmRevertDialog; + _handleRevertDialogConfirm() { + const el = this.$.confirmRevertDialog; this.$.overlay.close(); el.hidden = true; this._fireAction('/revert', this.actions.revert, false, {message: el.message}); }, - _handleAbandonDialogConfirm: function() { - var el = this.$.confirmAbandonDialog; + _handleAbandonDialogConfirm() { + const el = this.$.confirmAbandonDialog; this.$.overlay.close(); el.hidden = true; this._fireAction('/abandon', this.actions.abandon, false, {message: el.message}); }, - _handleDeleteConfirm: function() { + _handleDeleteConfirm() { this._fireAction('/', this.actions[ChangeActions.DELETE], false); }, - _setLoadingOnButtonWithKey: function(key) { + _getActionOverflowIndex(type, key) { + return this._overflowActions.findIndex(action => { + return action.type === type && action.key === key; + }); + }, + + _setLoadingOnButtonWithKey(type, key) { this._actionLoadingMessage = this._computeLoadingLabel(key); // If the action appears in the overflow menu. - if (MENU_ACTION_KEYS.indexOf(key) !== -1) { + if (this._getActionOverflowIndex(type, key) !== -1) { this.push('_disabledMenuActions', key === '/' ? 'delete' : key); return function() { this._actionLoadingMessage = null; @@ -590,7 +755,7 @@ } // Otherwise it's a top-level action. - var buttonEl = this.$$('[data-action-key="' + key + '"]'); + const buttonEl = this.$$(`[data-action-key="${key}"]`); buttonEl.setAttribute('loading', true); buttonEl.disabled = true; return function() { @@ -600,18 +765,18 @@ }.bind(this); }, - _fireAction: function(endpoint, action, revAction, opt_payload) { - var cleanupFn = this._setLoadingOnButtonWithKey(action.__key); - + _fireAction(endpoint, action, revAction, opt_payload) { + const cleanupFn = + this._setLoadingOnButtonWithKey(action.__type, action.__key); this._send(action.method, opt_payload, endpoint, revAction, cleanupFn) .then(this._handleResponse.bind(this, action)); }, - _showActionDialog: function(dialog) { + _showActionDialog(dialog) { this._hideAllDialogs(); dialog.hidden = false; - this.$.overlay.open().then(function() { + this.$.overlay.open().then(() => { if (dialog.resetFocus) { dialog.resetFocus(); } @@ -620,74 +785,102 @@ // TODO(rmistry): Redo this after // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved. - _setLabelValuesOnRevert: function(newChangeId) { - var labels = this.$.jsAPI.getLabelValuesPostRevert(this.change); + _setLabelValuesOnRevert(newChangeId) { + const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change); if (labels) { - var url = '/changes/' + newChangeId + '/revisions/current/review'; - this.$.restAPI.send(this.actions.revert.method, url, {labels: labels}); + const url = `/changes/${newChangeId}/revisions/current/review`; + this.$.restAPI.send(this.actions.revert.method, url, {labels}); } }, - _handleResponse: function(action, response) { + _handleResponse(action, response) { if (!response) { return; } - return this.$.restAPI.getResponseObject(response).then(function(obj) { - switch (action.__key) { - case ChangeActions.REVERT: - this._setLabelValuesOnRevert(obj.change_id); - /* falls through */ - case RevisionActions.CHERRYPICK: - page.show(this.changePath(obj._number)); - break; - case ChangeActions.DELETE: - case RevisionActions.DELETE: - if (action.__type === ActionType.CHANGE) { - page.show('/'); - } else { - page.show(this.changePath(this.changeNum)); - } - break; - default: - this.dispatchEvent(new CustomEvent('reload-change', - {detail: {action: action.__key}, bubbles: false})); - break; - } - }.bind(this)); + return this.$.restAPI.getResponseObject(response).then(obj => { + switch (action.__key) { + case ChangeActions.REVERT: + this._setLabelValuesOnRevert(obj.change_id); + /* falls through */ + case RevisionActions.CHERRYPICK: + page.show(this.changePath(obj._number)); + break; + case ChangeActions.DELETE: + case RevisionActions.DELETE: + if (action.__type === ActionType.CHANGE) { + page.show('/'); + } else { + page.show(this.changePath(this.changeNum)); + } + break; + case ChangeActions.WIP: + page.show(this.changePath(this.changeNum)); + break; + default: + this.dispatchEvent(new CustomEvent('reload-change', + {detail: {action: action.__key}, bubbles: false})); + break; + } + }); }, - _handleResponseError: function(response) { - return response.text().then(function(errText) { + _handleResponseError(response) { + return response.text().then(errText => { this.fire('show-alert', - { message: 'Could not perform action: ' + errText }); - if (errText.indexOf('Change is already up to date') !== 0) { + {message: `Could not perform action: ${errText}`}); + if (!errText.startsWith('Change is already up to date')) { throw Error(errText); } - }.bind(this)); + }); }, - _send: function(method, payload, actionEndpoint, revisionAction, - cleanupFn, opt_errorFn) { - var url = this.$.restAPI.getChangeActionURL(this.changeNum, - revisionAction ? this.patchNum : null, actionEndpoint); - return this.$.restAPI.send(method, url, payload, - this._handleResponseError, this).then(function(response) { - cleanupFn.call(this); - return response; - }.bind(this)); + _send(method, payload, actionEndpoint, revisionAction, cleanupFn, + opt_errorFn) { + return this.fetchIsLatestKnown(this.change, this.$.restAPI) + .then(isLatest => { + if (!isLatest) { + this.fire('show-alert', { + message: 'Cannot set label: a newer patch has been ' + + 'uploaded to this change.', + action: 'Reload', + callback: () => { + // Load the current change without any patch range. + Gerrit.Nav.navigateToChange(this.change); + }, + }); + cleanupFn(); + return Promise.resolve(); + } + + const url = this.$.restAPI.getChangeActionURL(this.changeNum, + revisionAction ? this.patchNum : null, actionEndpoint); + return this.$.restAPI.send(method, url, payload, + this._handleResponseError, this).then(response => { + cleanupFn.call(this); + return response; + }); + }); }, - _handleAbandonTap: function() { + _handleAbandonTap() { this._showActionDialog(this.$.confirmAbandonDialog); }, - _handleCherrypickTap: function() { + _handleCherrypickTap() { this.$.confirmCherrypick.branch = ''; this._showActionDialog(this.$.confirmCherrypick); }, - _handleDeleteTap: function() { + _handleDownloadTap() { + this.fire('download-tap', null, {bubbles: false}); + }, + + _handleDeleteTap() { this._showActionDialog(this.$.confirmDeleteDialog); }, + _handleWipTap() { + this._fireAction('/wip', this.actions.wip, false); + }, + /** * Merge sources of change actions into a single ordered array of action * values. @@ -698,70 +891,79 @@ * @param {Object} change The change object. * @return {Array} */ - _computeAllActions: function(changeActionsRecord, revisionActionsRecord, + _computeAllActions(changeActionsRecord, revisionActionsRecord, primariesRecord, additionalActionsRecord, change) { - var revisionActionValues = this._getActionValues(revisionActionsRecord, + const revisionActionValues = this._getActionValues(revisionActionsRecord, primariesRecord, additionalActionsRecord, ActionType.REVISION); - var changeActionValues = this._getActionValues(changeActionsRecord, + const changeActionValues = this._getActionValues(changeActionsRecord, primariesRecord, additionalActionsRecord, ActionType.CHANGE, change); - var quickApprove = this._getQuickApproveAction(); + const quickApprove = this._getQuickApproveAction(); if (quickApprove) { changeActionValues.unshift(quickApprove); } return revisionActionValues .concat(changeActionValues) - .sort(this._actionComparator); + .sort(this._actionComparator.bind(this)); + }, + + _getActionPriority(action) { + if (action.__type && action.__key) { + const overrideAction = this._actionPriorityOverrides.find(i => { + return i.type === action.__type && i.key === action.__key; + }); + + if (overrideAction !== undefined) { + return overrideAction.priority; + } + } + if (action.__key === 'review') { + return ActionPriority.REVIEW; + } else if (action.__primary) { + return ActionPriority.PRIMARY; + } else if (action.__type === ActionType.CHANGE) { + return ActionPriority.CHANGE; + } else if (action.__type === ActionType.REVISION) { + return ActionPriority.REVISION; + } + return ActionPriority.DEFAULT; }, /** * Sort comparator to define the order of change actions. */ - _actionComparator: function(actionA, actionB) { - // The code review action always appears first. - if (actionA.__key === 'review') { - return -1; - } else if (actionB.__key === 'review') { - return 1; + _actionComparator(actionA, actionB) { + const priorityDelta = this._getActionPriority(actionA) - + this._getActionPriority(actionB); + // Sort by the button label if same priority. + if (priorityDelta === 0) { + return actionA.label > actionB.label ? 1 : -1; + } else { + return priorityDelta; } - - // Primary actions always appear last. - if (actionA.__primary) { - return 1; - } else if (actionB.__primary) { - return -1; - } - - // Change actions appear before revision actions. - if (actionA.__type === 'change' && actionB.__type === 'revision') { - return 1; - } else if (actionA.__type === 'revision' && actionB.__type === 'change') { - return -1; - } - - // Otherwise, sort by the button label. - return actionA.label > actionB.label ? 1 : -1; }, - _computeTopLevelActions: function(actionRecord, hiddenActionsRecord) { - var hiddenActions = hiddenActionsRecord.base || []; - return actionRecord.base.filter(function(a) { - return MENU_ACTION_KEYS.indexOf(a.__key) === -1 && - hiddenActions.indexOf(a.__key) === -1; + _computeTopLevelActions(actionRecord, hiddenActionsRecord) { + const hiddenActions = hiddenActionsRecord.base || []; + return actionRecord.base.filter(a => { + const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1; + return !(overflow || hiddenActions.includes(a.__key)); }); }, - _computeMenuActions: function(actionRecord, hiddenActionsRecord) { - var hiddenActions = hiddenActionsRecord.base || []; - return actionRecord.base - .filter(function(a) { - return MENU_ACTION_KEYS.indexOf(a.__key) !== -1 && - hiddenActions.indexOf(a.__key) === -1; - }) - .map(function(action) { - var key = action.__key; - if (key === '/') { key = 'delete'; } - return {name: action.label, id: key, }; - }); + _computeMenuActions(actionRecord, hiddenActionsRecord) { + const hiddenActions = hiddenActionsRecord.base || []; + return actionRecord.base.filter(a => { + const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1; + return overflow && !hiddenActions.includes(a.__key); + }).map(action => { + let key = action.__key; + if (key === '/') { key = 'delete'; } + return { + name: action.label, + id: `${key}-${action.__type}`, + action, + }; + }); }, }); })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html index 83a1c7e..4e32116 100644 --- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-change-actions.html"> <script>void(0);</script> @@ -34,11 +34,13 @@ </test-fixture> <script> - suite('gr-change-actions tests', function() { - var element; - setup(function() { + suite('gr-change-actions tests', () => { + let element; + let sandbox; + + setup(() => { stub('gr-rest-api-interface', { - getChangeRevisionActions: function() { + getChangeRevisionActions() { return Promise.resolve({ '/': { method: 'DELETE', @@ -46,19 +48,19 @@ title: 'Delete draft revision 2', enabled: true, }, - cherrypick: { + 'cherrypick': { method: 'POST', label: 'Cherry Pick', title: 'Cherry pick change to a different branch', enabled: true, }, - rebase: { + 'rebase': { method: 'POST', label: 'Rebase', title: 'Rebase onto tip of branch or parent change', enabled: true, }, - submit: { + 'submit': { method: 'POST', label: 'Submit', title: 'Submit patch set 2 into master', @@ -66,18 +68,18 @@ }, }); }, - send: function(method, url, payload) { + send(method, url, payload) { if (method !== 'POST') { return Promise.reject('bad method'); } if (url === '/changes/42/revisions/2/submit') { return Promise.resolve({ ok: true, - text: function() { return Promise.resolve(')]}\'\n{}'); }, + text() { return Promise.resolve(')]}\'\n{}'); }, }); } else if (url === '/changes/42/revisions/2/rebase') { return Promise.resolve({ ok: true, - text: function() { return Promise.resolve(')]}\'\n{}'); }, + text() { return Promise.resolve(')]}\'\n{}'); }, }); } @@ -97,18 +99,24 @@ enabled: true, }, }; + sandbox = sinon.sandbox.create(); + return element.reload(); }); - test('_shouldHideActions', function() { + teardown(() => { + sandbox.restore(); + }); + + test('_shouldHideActions', () => { assert.isTrue(element._shouldHideActions(undefined, true)); assert.isTrue(element._shouldHideActions({base: {}}, false)); assert.isFalse(element._shouldHideActions({base: ['test']}, false)); }); - test('hide revision action', function(done) { - flush(function() { - var buttonEl = element.$$('[data-action-key="submit"]'); + test('hide revision action', done => { + flush(() => { + const buttonEl = element.$$('[data-action-key="submit"]'); assert.isOk(buttonEl); assert.throws(element.setActionHidden.bind(element, 'invalid type')); element.setActionHidden(element.ActionType.REVISION, @@ -117,14 +125,14 @@ element.setActionHidden(element.ActionType.REVISION, element.RevisionActions.SUBMIT, true); assert.lengthOf(element._hiddenActions, 1); - flush(function() { - var buttonEl = element.$$('[data-action-key="submit"]'); + flush(() => { + const buttonEl = element.$$('[data-action-key="submit"]'); assert.isNotOk(buttonEl); element.setActionHidden(element.ActionType.REVISION, - element.RevisionActions.SUBMIT, false); - flush(function() { - var buttonEl = element.$$('[data-action-key="submit"]'); + element.RevisionActions.SUBMIT, false); + flush(() => { + const buttonEl = element.$$('[data-action-key="submit"]'); assert.isOk(buttonEl); assert.isFalse(buttonEl.hasAttribute('hidden')); done(); @@ -133,9 +141,10 @@ }); }); - test('hide menu action', function(done) { - flush(function() { - var buttonEl = element.$.moreActions.$$('span[data-id="delete"]'); + test('hide menu action', done => { + flush(() => { + const buttonEl = + element.$.moreActions.$$('span[data-id="delete-revision"]'); assert.isOk(buttonEl); assert.throws(element.setActionHidden.bind(element, 'invalid type')); element.setActionHidden(element.ActionType.CHANGE, @@ -144,14 +153,16 @@ element.setActionHidden(element.ActionType.CHANGE, element.ChangeActions.DELETE, true); assert.lengthOf(element._hiddenActions, 1); - flush(function() { - var buttonEl = element.$.moreActions.$$('span[data-id="delete"]'); + flush(() => { + const buttonEl = + element.$.moreActions.$$('span[data-id="delete-revision"]'); assert.isNotOk(buttonEl); element.setActionHidden(element.ActionType.CHANGE, - element.RevisionActions.DELETE, false); - flush(function() { - var buttonEl = element.$.moreActions.$$('span[data-id="delete"]'); + element.RevisionActions.DELETE, false); + flush(() => { + const buttonEl = + element.$.moreActions.$$('span[data-id="delete-revision"]'); assert.isOk(buttonEl); done(); }); @@ -159,22 +170,22 @@ }); }); - test('buttons exist', function(done) { + test('buttons exist', done => { element._loading = false; - flush(function() { - var buttonEls = Polymer.dom(element.root) + flush(() => { + const buttonEls = Polymer.dom(element.root) .querySelectorAll('gr-button'); - var menuItems = element.$.moreActions.items; - assert.equal(buttonEls.length + menuItems.length, 6); + const menuItems = element.$.moreActions.items; + assert.equal(buttonEls.length + menuItems.length, 7); assert.isFalse(element.hidden); done(); }); }); - test('delete buttons have explicit labels', function(done) { - flush(function() { - var deleteItems = element.$.moreActions.items.filter(function(item) { - return item.id === 'delete'; + test('delete buttons have explicit labels', done => { + flush(() => { + const deleteItems = element.$.moreActions.items.filter(item => { + return item.id.startsWith('delete'); }); assert.equal(deleteItems.length, 2); assert.notEqual(deleteItems[0].name, deleteItems[1].name); @@ -190,9 +201,9 @@ }); }); - test('get revision object from change', function() { - var revObj = {_number: 2, foo: 'bar'}; - var change = { + test('get revision object from change', () => { + const revObj = {_number: 2, foo: 'bar'}; + const change = { revisions: { rev1: {_number: 1}, rev2: revObj, @@ -201,23 +212,24 @@ assert.deepEqual(element._getRevision(change, '2'), revObj); }); - test('_actionComparator sort order', function() { - var actions = [ + test('_actionComparator sort order', () => { + const actions = [ {label: '123', __type: 'change', __key: 'review'}, - {label: 'abc', __type: 'revision'}, + {label: 'abc-ro', __type: 'revision'}, {label: 'abc', __type: 'change'}, {label: 'def', __type: 'change'}, - {label: 'def', __type: 'change', __primary: true}, + {label: 'def-p', __type: 'change', __primary: true}, ]; - var result = actions.slice(); + const result = actions.slice(); result.reverse(); - result.sort(element._actionComparator); - + result.sort(element._actionComparator.bind(element)); assert.deepEqual(result, actions); }); - test('submit change', function(done) { + test('submit change', done => { + sandbox.stub(element, 'fetchIsLatestKnown', + () => { return Promise.resolve(true); }); element.change = { revisions: { rev1: {_number: 1}, @@ -226,44 +238,42 @@ }; element.patchNum = '2'; - flush(function() { - var submitButton = element.$$('gr-button[data-action-key="submit"]'); + flush(() => { + const submitButton = element.$$('gr-button[data-action-key="submit"]'); assert.ok(submitButton); MockInteractions.tap(submitButton); // Upon success it should fire the reload-change event. - element.addEventListener('reload-change', function(e) { + element.addEventListener('reload-change', () => { done(); }); }); }); - test('submit change with plugin hook', function(done) { - var canSubmitStub = sinon.stub(element, '_canSubmitChange', - function() { return false; }); - var fireActionStub = sinon.stub(element, '_fireAction'); - flush(function() { - var submitButton = element.$$('gr-button[data-action-key="submit"]'); + test('submit change with plugin hook', done => { + sandbox.stub(element, '_canSubmitChange', + () => { return false; }); + const fireActionStub = sandbox.stub(element, '_fireAction'); + flush(() => { + const submitButton = element.$$('gr-button[data-action-key="submit"]'); assert.ok(submitButton); MockInteractions.tap(submitButton); assert.equal(fireActionStub.callCount, 0); - canSubmitStub.restore(); - fireActionStub.restore(); done(); }); }); - test('chain state', function() { + test('chain state', () => { assert.equal(element._hasKnownChainState, false); element.hasParent = true; assert.equal(element._hasKnownChainState, true); element.hasParent = false; }); - test('_calculateDisabled', function() { - var hasKnownChainState = false; - var action = {__key: 'rebase', enabled: true}; + test('_calculateDisabled', () => { + let hasKnownChainState = false; + const action = {__key: 'rebase', enabled: true}; assert.equal( element._calculateDisabled(action, hasKnownChainState), true); @@ -281,12 +291,12 @@ element._calculateDisabled(action, hasKnownChainState), true); }); - test('rebase change', function(done) { - var fireActionStub = sinon.stub(element, '_fireAction'); - flush(function() { - var rebaseButton = element.$$('gr-button[data-action-key="rebase"]'); + test('rebase change', done => { + const fireActionStub = sandbox.stub(element, '_fireAction'); + flush(() => { + const rebaseButton = element.$$('gr-button[data-action-key="rebase"]'); MockInteractions.tap(rebaseButton); - var rebaseAction = { + const rebaseAction = { __key: 'rebase', __type: 'revision', __primary: false, @@ -313,15 +323,14 @@ assert.deepEqual(fireActionStub.lastCall.args, ['/rebase', rebaseAction, true, {base: ''}]); - fireActionStub.restore(); done(); }); }); - test('two dialogs are not shown at the same time', function(done) { + test('two dialogs are not shown at the same time', done => { element._hasKnownChainState = true; - flush(function() { - var rebaseButton = element.$$('gr-button[data-action-key="rebase"]'); + flush(() => { + const rebaseButton = element.$$('gr-button[data-action-key="rebase"]'); assert.ok(rebaseButton); MockInteractions.tap(rebaseButton); flushAsynchronousOperations(); @@ -335,23 +344,17 @@ }); }); - suite('cherry-pick', function() { - var fireActionStub; - var alertStub; + suite('cherry-pick', () => { + let fireActionStub; - setup(function() { - fireActionStub = sinon.stub(element, '_fireAction'); - alertStub = sinon.stub(window, 'alert'); + setup(() => { + fireActionStub = sandbox.stub(element, '_fireAction'); + sandbox.stub(window, 'alert'); }); - teardown(function() { - alertStub.restore(); - fireActionStub.restore(); - }); - - test('works', function() { + test('works', () => { element._handleCherrypickTap(); - var action = { + const action = { __key: 'cherrypick', __type: 'revision', __primary: false, @@ -386,8 +389,8 @@ ]); }); - test('branch name cleared when re-open cherrypick', function() { - var emptyBranchName = ''; + test('branch name cleared when re-open cherrypick', () => { + const emptyBranchName = ''; element.$.confirmCherrypick.branch = 'master'; element._handleCherrypickTap(); @@ -395,29 +398,30 @@ }); }); - test('custom actions', function(done) { + test('custom actions', done => { // Add a button with the same key as a server-based one to ensure // collisions are taken care of. - var key = element.addActionButton(element.ActionType.REVISION, 'Bork!'); - element.addEventListener(key + '-tap', function(e) { + const key = element.addActionButton(element.ActionType.REVISION, 'Bork!'); + element.addEventListener(key + '-tap', e => { assert.equal(e.detail.node.getAttribute('data-action-key'), key); element.removeActionButton(key); - flush(function() { + flush(() => { assert.notOk(element.$$('[data-action-key="' + key + '"]')); done(); }); }); - flush(function() { + flush(() => { MockInteractions.tap(element.$$('[data-action-key="' + key + '"]')); }); }); - test('_setLoadingOnButtonWithKey top-level', function() { - var key = 'rebase'; - var cleanup = element._setLoadingOnButtonWithKey(key); + test('_setLoadingOnButtonWithKey top-level', () => { + const key = 'rebase'; + const type = 'revision'; + const cleanup = element._setLoadingOnButtonWithKey(type, key); assert.equal(element._actionLoadingMessage, 'Rebasing...'); - var button = element.$$('[data-action-key="' + key + '"]'); + const button = element.$$('[data-action-key="' + key + '"]'); assert.isTrue(button.hasAttribute('loading')); assert.isTrue(button.disabled); @@ -430,9 +434,10 @@ assert.isNotOk(element._actionLoadingMessage); }); - test('_setLoadingOnButtonWithKey overflow menu', function() { - var key = 'cherrypick'; - var cleanup = element._setLoadingOnButtonWithKey(key); + test('_setLoadingOnButtonWithKey overflow menu', () => { + const key = 'cherrypick'; + const type = 'revision'; + const cleanup = element._setLoadingOnButtonWithKey(type, key); assert.equal(element._actionLoadingMessage, 'Cherry-Picking...'); assert.include(element._disabledMenuActions, 'cherrypick'); assert.isFunction(cleanup); @@ -443,13 +448,13 @@ assert.notInclude(element._disabledMenuActions, 'cherrypick'); }); - suite('revert change', function() { - var alertStub; - var fireActionStub; + suite('revert change', () => { + let alertStub; + let fireActionStub; - setup(function() { - fireActionStub = sinon.stub(element, '_fireAction'); - alertStub = sinon.stub(window, 'alert'); + setup(() => { + fireActionStub = sandbox.stub(element, '_fireAction'); + alertStub = sandbox.stub(window, 'alert'); element.actions = { revert: { method: 'POST', @@ -461,48 +466,39 @@ return element.reload(); }); - teardown(function() { - alertStub.restore(); - fireActionStub.restore(); - }); - - test('revert change with plugin hook', function(done) { + test('revert change with plugin hook', done => { element.change = { current_revision: 'abc1234', }; - var newRevertMsg = 'Modified revert msg'; - var modifyRevertMsgStub = sinon.stub(element, '_modifyRevertMsg', - function() { return newRevertMsg; }); - var populateRevertMsgStub = sinon.stub( - element.$.confirmRevertDialog, 'populateRevertMessage', - function() { return 'original msg'; }); - flush(function() { - var revertButton = element.$$('gr-button[data-action-key="revert"]'); + const newRevertMsg = 'Modified revert msg'; + sandbox.stub(element, '_modifyRevertMsg', + () => { return newRevertMsg; }); + sandbox.stub(element.$.confirmRevertDialog, 'populateRevertMessage', + () => { return 'original msg'; }); + flush(() => { + const revertButton = + element.$$('gr-button[data-action-key="revert"]'); MockInteractions.tap(revertButton); assert.equal(element.$.confirmRevertDialog.message, newRevertMsg); - - populateRevertMsgStub.restore(); - modifyRevertMsgStub.restore(); done(); }); }); - test('works', function() { + test('works', () => { element.change = { current_revision: 'abc1234', }; - var populateRevertMsgStub = sinon.stub( - element.$.confirmRevertDialog, 'populateRevertMessage', - function() { return 'original msg'; }); - var revertButton = element.$$('gr-button[data-action-key="revert"]'); + sandbox.stub(element.$.confirmRevertDialog, 'populateRevertMessage', + () => { return 'original msg'; }); + const revertButton = element.$$('gr-button[data-action-key="revert"]'); MockInteractions.tap(revertButton); element.$.confirmRevertDialog.message = 'foo message'; element._handleRevertDialogConfirm(); assert.notOk(alertStub.called); - var action = { + const action = { __key: 'revert', __type: 'change', __primary: false, @@ -515,16 +511,109 @@ '/revert', action, false, { message: 'foo message', }]); - populateRevertMsgStub.restore(); }); }); - suite('delete change', function() { - var fireActionStub; - var deleteAction; + suite('mark change private', () => { + setup(() => { + const privateAction = { + __key: 'private', + __type: 'change', + __primary: false, + method: 'POST', + label: 'Mark private', + title: 'Working...', + enabled: true, + }; - setup(function() { - fireActionStub = sinon.stub(element, '_fireAction'); + element.actions = { + private: privateAction, + }; + + element.change.is_private = false; + + element.changeNum = '2'; + element.patchNum = '2'; + + return element.reload(); + }); + + test('make sure the mark private change button is not outside of the ' + + 'overflow menu', done => { + flush(() => { + assert.isNotOk(element.$$('[data-action-key="private"]')); + done(); + }); + }); + + test('private change', done => { + flush(() => { + assert.isOk( + element.$.moreActions.$$('span[data-id="private-change"]')); + element.setActionOverflow('change', 'private', false); + flushAsynchronousOperations(); + assert.isOk(element.$$('[data-action-key="private"]')); + assert.isNotOk( + element.$.moreActions.$$('span[data-id="private-change"]')); + done(); + }); + }); + }); + + suite('unmark private change', () => { + setup(() => { + const unmarkPrivateAction = { + __key: 'private.delete', + __type: 'change', + __primary: false, + method: 'POST', + label: 'Unmark private', + title: 'Working...', + enabled: true, + }; + + element.actions = { + 'private.delete': unmarkPrivateAction, + }; + + element.change.is_private = true; + + element.changeNum = '2'; + element.patchNum = '2'; + + return element.reload(); + }); + + test('make sure the unmark private change button is not outside of the ' + + 'overflow menu', done => { + flush(() => { + assert.isNotOk(element.$$('[data-action-key="private.delete"]')); + done(); + }); + }); + + test('unmark the private change', done => { + flush(() => { + assert.isOk( + element.$.moreActions.$$('span[data-id="private.delete-change"]') + ); + element.setActionOverflow('change', 'private.delete', false); + flushAsynchronousOperations(); + assert.isOk(element.$$('[data-action-key="private.delete"]')); + assert.isNotOk( + element.$.moreActions.$$('span[data-id="private.delete-change"]') + ); + done(); + }); + }); + }); + + suite('delete change', () => { + let fireActionStub; + let deleteAction; + + setup(() => { + fireActionStub = sandbox.stub(element, '_fireAction'); element.change = { current_revision: 'abc1234', }; @@ -539,16 +628,12 @@ }; }); - teardown(function() { - fireActionStub.restore(); - }); - - test('does not delete on action', function() { + test('does not delete on action', () => { element._handleDeleteTap(); assert.isFalse(fireActionStub.called); }); - test('shows confirm dialog', function() { + test('shows confirm dialog', () => { element._handleDeleteTap(); assert.isFalse(element.$$('#confirmDeleteDialog').hidden); MockInteractions.tap( @@ -557,7 +642,7 @@ assert.isTrue(fireActionStub.calledWith('/', deleteAction, false)); }); - test('hides delete confirm on cancel', function() { + test('hides delete confirm on cancel', () => { element._handleDeleteTap(); MockInteractions.tap( element.$$('#confirmDeleteDialog').$$('gr-button:not([primary])')); @@ -567,8 +652,166 @@ }); }); - suite('quick approve', function() { - setup(function() { + suite('ignore change', () => { + setup(done => { + sandbox.stub(element, '_fireAction'); + + const IgnoreAction = { + __key: 'ignore', + __type: 'change', + __primary: false, + method: 'PUT', + label: 'Ignore', + title: 'Working...', + enabled: true, + }; + + element.actions = { + ignore: IgnoreAction, + }; + + element.changeNum = '2'; + element.patchNum = '2'; + + element.reload().then(() => {flush(done);}); + }); + + test('make sure the ignore button is not outside of the overflow menu', + () => { + assert.isNotOk(element.$$('[data-action-key="ignore"]')); + }); + + test('ignoring change', () => { + assert.isOk(element.$.moreActions.$$('span[data-id="ignore-change"]')); + element.setActionOverflow('change', 'ignore', false); + flushAsynchronousOperations(); + assert.isOk(element.$$('[data-action-key="ignore"]')); + assert.isNotOk( + element.$.moreActions.$$('span[data-id="ignore-change"]')); + }); + }); + + suite('unignore change', () => { + setup(done => { + sandbox.stub(element, '_fireAction'); + + const UnignoreAction = { + __key: 'unignore', + __type: 'change', + __primary: false, + method: 'PUT', + label: 'Unignore', + title: 'Working...', + enabled: true, + }; + + element.actions = { + unignore: UnignoreAction, + }; + + element.changeNum = '2'; + element.patchNum = '2'; + + element.reload().then(() => {flush(done);}); + }); + + + test('unignore button is not outside of the overflow menu', () => { + assert.isNotOk(element.$$('[data-action-key="unignore"]')); + }); + + test('unignoring change', () => { + assert.isOk( + element.$.moreActions.$$('span[data-id="unignore-change"]')); + element.setActionOverflow('change', 'unignore', false); + flushAsynchronousOperations(); + assert.isOk(element.$$('[data-action-key="unignore"]')); + assert.isNotOk( + element.$.moreActions.$$('span[data-id="unignore-change"]')); + }); + }); + + suite('mute change', () => { + setup(done => { + sandbox.stub(element, '_fireAction'); + + const MuteAction = { + __key: 'mute', + __type: 'change', + __primary: false, + method: 'PUT', + label: 'Mute', + title: 'Working...', + enabled: true, + }; + + element.actions = { + mute: MuteAction, + }; + + element.changeNum = '2'; + element.patchNum = '2'; + + element.reload().then(() => {flush(done);}); + }); + + test('make sure the mute button is not outside of the overflow menu', + () => { + assert.isNotOk(element.$$('[data-action-key="mute"]')); + }); + + test('muting change', () => { + assert.isOk(element.$.moreActions.$$('span[data-id="mute-change"]')); + element.setActionOverflow('change', 'mute', false); + flushAsynchronousOperations(); + assert.isOk(element.$$('[data-action-key="mute"]')); + assert.isNotOk( + element.$.moreActions.$$('span[data-id="mute-change"]')); + }); + }); + + suite('unmute change', () => { + setup(done => { + sandbox.stub(element, '_fireAction'); + + const UnmuteAction = { + __key: 'unmute', + __type: 'change', + __primary: false, + method: 'PUT', + label: 'Unmute', + title: 'Working...', + enabled: true, + }; + + element.actions = { + unmute: UnmuteAction, + }; + + element.changeNum = '2'; + element.patchNum = '2'; + + element.reload().then(() => {flush(done);}); + }); + + + test('unmute button not outside of the overflow menu', () => { + assert.isNotOk(element.$$('[data-action-key="unmute"]')); + }); + + test('unmuting change', () => { + assert.isOk( + element.$.moreActions.$$('span[data-id="unmute-change"]')); + element.setActionOverflow('change', 'unmute', false); + flushAsynchronousOperations(); + assert.isOk(element.$$('[data-action-key="unmute"]')); + assert.isNotOk( + element.$.moreActions.$$('span[data-id="unmute-change"]')); + }); + }); + + suite('quick approve', () => { + setup(() => { element.change = { current_revision: 'abc1234', }; @@ -590,17 +833,18 @@ flushAsynchronousOperations(); }); - test('added when can approve', function() { - var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + test('added when can approve', () => { + const approveButton = + element.$$('gr-button[data-action-key=\'review\']'); assert.isNotNull(approveButton); }); - test('is first in list of actions', function() { - var approveButton = element.$$('gr-button'); + test('is first in list of actions', () => { + const approveButton = element.$$('gr-button'); assert.equal(approveButton.getAttribute('data-label'), 'foo+1'); }); - test('not added when already approved', function() { + test('not added when already approved', () => { element.change = { current_revision: 'abc1234', labels: { @@ -614,11 +858,12 @@ }, }; flushAsynchronousOperations(); - var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + const approveButton = + element.$$('gr-button[data-action-key=\'review\']'); assert.isNull(approveButton); }); - test('not added when label not permitted', function() { + test('not added when label not permitted', () => { element.change = { current_revision: 'abc1234', labels: { @@ -629,23 +874,23 @@ }, }; flushAsynchronousOperations(); - var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + const approveButton = + element.$$('gr-button[data-action-key=\'review\']'); assert.isNull(approveButton); }); - test('approves when taped', function() { - var fireActionStub = sinon.stub(element, '_fireAction'); + test('approves when tapped', () => { + const fireActionStub = sandbox.stub(element, '_fireAction'); MockInteractions.tap( element.$$('gr-button[data-action-key=\'review\']')); flushAsynchronousOperations(); assert.isTrue(fireActionStub.called); assert.isTrue(fireActionStub.calledWith('/review')); - var payload = fireActionStub.lastCall.args[3]; + const payload = fireActionStub.lastCall.args[3]; assert.deepEqual(payload.labels, {foo: '+1'}); - fireActionStub.restore(); }); - test('not added when multiple labels are required', function() { + test('not added when multiple labels are required', () => { element.change = { current_revision: 'abc1234', labels: { @@ -658,11 +903,12 @@ }, }; flushAsynchronousOperations(); - var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + const approveButton = + element.$$('gr-button[data-action-key=\'review\']'); assert.isNull(approveButton); }); - test('button label for missing approval', function() { + test('button label for missing approval', () => { element.change = { current_revision: 'abc1234', labels: { @@ -680,11 +926,12 @@ }, }; flushAsynchronousOperations(); - var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + const approveButton = + element.$$('gr-button[data-action-key=\'review\']'); assert.equal(approveButton.getAttribute('data-label'), 'foo+1'); }); - test('no quick approve if score is not maximal for a label', function() { + test('no quick approve if score is not maximal for a label', () => { element.change = { current_revision: 'abc1234', labels: { @@ -702,11 +949,12 @@ }, }; flushAsynchronousOperations(); - var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + const approveButton = + element.$$('gr-button[data-action-key=\'review\']'); assert.isNull(approveButton); }); - test('approving label with a non-max score', function() { + test('approving label with a non-max score', () => { element.change = { current_revision: 'abc1234', labels: { @@ -724,9 +972,42 @@ }, }; flushAsynchronousOperations(); - var approveButton = element.$$('gr-button[data-action-key=\'review\']'); + const approveButton = + element.$$('gr-button[data-action-key=\'review\']'); assert.equal(approveButton.getAttribute('data-label'), 'bar+2'); }); }); + + test('adds download revision action', () => { + const handler = sandbox.stub(); + element.addEventListener('download-tap', handler); + assert.ok(element.revisionActions.download); + element._handleDownloadTap(); + flushAsynchronousOperations(); + + assert.isTrue(handler.called); + }); + + suite('setActionOverflow', () => { + test('move action from overflow', () => { + assert.isNotOk(element.$$('[data-action-key="cherrypick"]')); + assert.strictEqual( + element.$.moreActions.items[0].id, 'cherrypick-revision'); + element.setActionOverflow('revision', 'cherrypick', false); + flushAsynchronousOperations(); + assert.isOk(element.$$('[data-action-key="cherrypick"]')); + assert.notEqual( + element.$.moreActions.items[0].id, 'cherrypick-revision'); + }); + + test('move action to overflow', () => { + assert.isOk(element.$$('[data-action-key="submit"]')); + element.setActionOverflow('revision', 'submit', true); + flushAsynchronousOperations(); + assert.isNotOk(element.$$('[data-action-key="submit"]')); + assert.strictEqual( + element.$.moreActions.items[4].id, 'submit-revision'); + }); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html new file mode 100644 index 0000000..b347890 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -0,0 +1,114 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-change-metadata</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="../../plugins/gr-plugin-host/gr-plugin-host.html"> +<link rel="import" href="gr-change-metadata.html"> + +<script>void(0);</script> + +<test-fixture id="element"> + <template> + <gr-change-metadata></gr-change-metadata> + </template> +</test-fixture> + +<test-fixture id="plugin-host"> + <template> + <gr-plugin-host></gr-plugin-host> + </template> +</test-fixture> + +<script> + suite('gr-change-metadata integration tests', () => { + let sandbox; + let element; + + const sectionSelectors = [ + 'section.assignee', + 'section.labelStatus', + 'section.strategy', + 'section.topic', + ]; + + const getStyle = function(selector, name) { + return window.getComputedStyle( + Polymer.dom(element.root).querySelector(selector))[name]; + }; + + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-change-metadata', { + _computeShowLabelStatus() { return true; }, + _computeShowReviewersByState() { return true; }, + ready() { + this.change = {labels: [], status: 'NEW'}; + this.serverConfig = {}; + }, + }); + }); + + teardown(() => { + Gerrit._pluginsPending = -1; + Gerrit._allPluginsPromise = undefined; + sandbox.restore(); + }); + + suite('by default', () => { + setup(done => { + element = fixture('element'); + flush(done); + }); + + for (const sectionSelector of sectionSelectors) { + test(sectionSelector + ' does not have display: none', () => { + assert.notEqual(getStyle(sectionSelector, 'display'), 'none'); + }); + } + }); + + suite('with plugin style', () => { + setup(done => { + const pluginHost = fixture('plugin-host'); + pluginHost.config = { + js_resource_paths: [], + html_resource_paths: [ + new URL('test/plugin.html', window.location.href).toString(), + ], + }; + element = fixture('element'); + const importSpy = sandbox.spy(element.$.externalStyle, '_import'); + Gerrit.awaitPluginsLoaded().then(() => { + Promise.all(importSpy.returnValues).then(() => { + flush(done); + }); + }); + }); + + for (const sectionSelector of sectionSelectors) { + test(sectionSelector + ' may have display: none', () => { + assert.equal(getStyle(sectionSelector, 'display'), 'none'); + }); + } + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html index 78bcb9a..ed18d6a 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -14,20 +14,22 @@ limitations under the License. --> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../plugins/gr-external-style/gr-external-style.html"> +<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html"> -<link rel="import" href="../../shared/gr-label/gr-label.html"> <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> <link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html"> +<link rel="import" href="../../shared/gr-label/gr-label.html"> <link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-change-metadata"> <template> - <style> + <style include="shared-styles"> section:not(:first-of-type) { margin-top: 1em; } @@ -38,7 +40,8 @@ .title { color: #666; font-weight: bold; - white-space: nowrap; + max-width: 20em; + word-break: break-word; } gr-account-link { max-width: 20ch; @@ -69,12 +72,35 @@ .notApproved { background-color: #ffd4d4; } - .labelStatus { + .labelStatus .value { max-width: 9em; } + .labelStatus li { + list-style-type: disc; + } .webLink { display: block; } + #missingLabels { + padding-left: 1.5em; + } + + /* CSS Mixins should be applied last. */ + section.assignee { + @apply(--change-metadata-assignee); + } + section.labelStatus { + @apply(--change-metadata-label-status); + } + section.strategy { + @apply(--change-metadata-strategy); + } + section.topic { + @apply(--change-metadata-topic); + } + #externalStyle { + display: block; + } @media screen and (max-width: 50em), screen and (min-width: 75em) { :host { display: table; @@ -96,22 +122,22 @@ } } </style> - <section> - <span class="title">Updated</span> - <span class="value"> - <gr-date-formatter - has-tooltip - date-str="[[change.updated]]"></gr-date-formatter> - </span> - </section> - <section> - <span class="title">Owner</span> - <span class="value"> - <gr-account-link account="[[change.owner]]"></gr-account-link> - </span> - </section> - <template is="dom-if" if="[[_showReviewersByState]]"> + <gr-external-style id="externalStyle" name="change-metadata"> <section> + <span class="title">Updated</span> + <span class="value"> + <gr-date-formatter + has-tooltip + date-str="[[change.updated]]"></gr-date-formatter> + </span> + </section> + <section> + <span class="title">Owner</span> + <span class="value"> + <gr-account-link account="[[change.owner]]"></gr-account-link> + </span> + </section> + <section class="assignee"> <span class="title">Assignee</span> <span class="value"> <gr-account-list @@ -120,131 +146,139 @@ placeholder="Add assignee..." accounts="{{_assignee}}" change="[[change]]" - readonly="[[!mutable]]" + readonly="[[_computeAssigneeReadOnly(mutable, change)]]" allow-any-user></gr-account-list> </span> </section> + <template is="dom-if" if="[[_showReviewersByState]]"> + <section> + <span class="title">Reviewers</span> + <span class="value"> + <gr-reviewer-list + change="{{change}}" + mutable="[[mutable]]" + reviewers-only></gr-reviewer-list> + </span> + </section> + <section> + <span class="title">CC</span> + <span class="value"> + <gr-reviewer-list + change="{{change}}" + mutable="[[mutable]]" + ccs-only></gr-reviewer-list> + </span> + </section> + </template> + <template is="dom-if" if="[[!_showReviewersByState]]"> + <section> + <span class="title">Reviewers</span> + <span class="value"> + <gr-reviewer-list + change="{{change}}" + mutable="[[mutable]]"></gr-reviewer-list> + </span> + </section> + </template> <section> - <span class="title">Reviewers</span> + <span class="title">Project</span> <span class="value"> - <gr-reviewer-list - change="{{change}}" - mutable="[[mutable]]" - reviewers-only></gr-reviewer-list> + <a href$="[[_computeProjectURL(change.project)]]">[[change.project]]</a> </span> </section> <section> - <span class="title">CC</span> + <span class="title">Branch</span> <span class="value"> - <gr-reviewer-list - change="{{change}}" - mutable="[[mutable]]" - ccs-only></gr-reviewer-list> + <a href$="[[_computeBranchURL(change.project, change.branch)]]">[[change.branch]]</a> </span> </section> - </template> - <template is="dom-if" if="[[!_showReviewersByState]]"> - <section> - <span class="title">Assignee</span> + <section class="topic"> + <span class="title">Topic</span> <span class="value"> - <gr-account-list - max-count="1" - id="assigneeValue" - placeholder="Add assignee..." - accounts="{{_assignee}}" - change="[[change]]" - readonly="[[!mutable]]" - allow-any-user></gr-account-list> - </span> - </section> - <section> - <span class="title">Reviewers</span> - <span class="value"> - <gr-reviewer-list - change="{{change}}" - mutable="[[mutable]]"></gr-reviewer-list> - </span> - </section> - </template> - <section> - <span class="title">Project</span> - <span class="value">[[change.project]]</span> - </section> - <section> - <span class="title">Branch</span> - <span class="value">[[change.branch]]</span> - </section> - <section> - <span class="title">Topic</span> - <span class="value"> - <template is="dom-if" if="[[change.topic]]"> - <gr-linked-chip - text="[[change.topic]]" - href="[[_computeTopicHref(change.topic)]]" - removable="[[!_topicReadOnly]]" - on-remove="_handleTopicRemoved"></gr-linked-chip> - </template> - <template is="dom-if" if="[[!change.topic]]"> - <gr-editable-label - value="{{change.topic}}" - placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]" - read-only="[[_topicReadOnly]]" - on-changed="_handleTopicChanged"></gr-editable-label> - </template> - </span> - </section> - <section class="strategy" hidden$="[[_computeHideStrategy(change)]]" hidden> - <span class="title">Strategy</span> - <span class="value">[[_computeStrategy(change)]]</span> - </section> - <template is="dom-repeat" - items="[[_computeLabelNames(change.labels)]]" as="labelName"> - <section> - <span class="title">[[labelName]]</span> - <span class="value"> - <template is="dom-repeat" - items="[[_computeLabelValues(labelName, change.labels.*)]]" - as="label"> - <div class="labelValueContainer"> - <span class$="[[label.className]]"> - <gr-label - has-tooltip - title="[[_computeValueTooltip(label.value, labelName)]]" - class="labelValue"> - [[label.value]] - </gr-label> - <gr-account-chip - account="[[label.account]]" - data-account-id$="[[label.account._account_id]]" - label-name="[[labelName]]" - removable="[[_computeCanDeleteVote(label.account, mutable)]]" - transparent-background - on-remove="_onDeleteVote"></gr-account-chip> - </span> - </div> + <template is="dom-if" if="[[change.topic]]"> + <gr-linked-chip + text="[[change.topic]]" + href="[[_computeTopicURL(change.topic)]]" + removable="[[!_topicReadOnly]]" + on-remove="_handleTopicRemoved"></gr-linked-chip> + </template> + <template is="dom-if" if="[[!change.topic]]"> + <gr-editable-label + value="[[change.topic]]" + placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]" + read-only="[[_topicReadOnly]]" + on-changed="_handleTopicChanged"></gr-editable-label> </template> </span> </section> - </template> - <template is="dom-if" if="[[_showLabelStatus]]"> - <section> - <span class="title">Label Status</span> - <span class="value labelStatus"> - [[_computeSubmitStatus(change.labels)]] + <section class="strategy" hidden$="[[_computeHideStrategy(change)]]" hidden> + <span class="title">Strategy</span> + <span class="value">[[_computeStrategy(change)]]</span> + </section> + <template is="dom-repeat" + items="[[_computeLabelNames(change.labels)]]" as="labelName"> + <section> + <span class="title">[[labelName]]</span> + <span class="value"> + <template is="dom-repeat" + items="[[_computeLabelValues(labelName, change.labels.*)]]" + as="label"> + <div class="labelValueContainer"> + <span class$="[[label.className]]"> + <gr-label + has-tooltip + title="[[_computeValueTooltip(label.value, labelName)]]" + class="labelValue"> + [[label.value]] + </gr-label> + <gr-account-chip + account="[[label.account]]" + data-account-id$="[[label.account._account_id]]" + label-name="[[labelName]]" + removable="[[_computeCanDeleteVote(label.account, mutable)]]" + transparent-background + on-remove="_onDeleteVote"></gr-account-chip> + </span> + </div> + </template> + </span> + </section> + </template> + <template is="dom-if" if="[[_showLabelStatus]]"> + <section class="labelStatus"> + <span class="title">Label Status</span> + <span class="value"> + <div hidden$="[[!_isWip]]"> + Work in progress + </div> + <div hidden$="[[!_showMissingLabels(change.labels)]]"> + [[_computeMissingLabelsHeader(change.labels)]] + <ul id="missingLabels"> + <template + is="dom-repeat" + items="[[_computeMissingLabels(change.labels)]]"> + <li>[[item]]</li> + </template> + </ul> + </div> + <div hidden$="[[_showMissingRequirements(change.labels, _isWip)]]"> + Ready to submit + </div> + </span> + </section> + </template> + <section id="webLinks" hidden$="[[!_computeWebLinks(commitInfo)]]"> + <span class="title">Links</span> + <span class="value"> + <template is="dom-repeat" + items="[[_computeWebLinks(commitInfo)]]" as="link"> + <a href="[[link.url]]" class="webLink" rel="noopener" target="_blank"> + [[link.name]] + </a> + </template> </span> </section> - </template> - <section id="webLinks" hidden$="[[!_computeWebLinks(commitInfo)]]"> - <span class="title">Links</span> - <span class="value"> - <template is="dom-repeat" - items="[[_computeWebLinks(commitInfo)]]" as="link"> - <a href="[[link.url]]" class="webLink" rel="noopener" target="_blank"> - [[link.name]] - </a> - </template> - </span> - </section> + </gr-external-style> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template> <script src="gr-change-metadata.js"></script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js index 57e25f8..b1784e7 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -14,7 +14,7 @@ (function() { 'use strict'; - var SubmitTypeLabel = { + const SubmitTypeLabel = { FAST_FORWARD_ONLY: 'Fast Forward Only', MERGE_IF_NECESSARY: 'Merge if Necessary', REBASE_IF_NECESSARY: 'Rebase if Necessary', @@ -25,6 +25,12 @@ Polymer({ is: 'gr-change-metadata', + /** + * Fired when the change topic is changed. + * + * @event topic-changed + */ + properties: { change: Object, commitInfo: Object, @@ -44,10 +50,13 @@ }, _assignee: Array, + _isWip: { + type: Boolean, + computed: '_computeIsWip(change)', + }, }, behaviors: [ - Gerrit.BaseUrlBehavior, Gerrit.RESTClientBehavior, ], @@ -56,15 +65,15 @@ '_assigneeChanged(_assignee.*)', ], - _changeChanged: function(change) { + _changeChanged(change) { this._assignee = change.assignee ? [change.assignee] : []; }, - _assigneeChanged: function(assigneeRecord) { + _assigneeChanged(assigneeRecord) { if (!this.change) { return; } - var assignee = assigneeRecord.base; + const assignee = assigneeRecord.base; if (assignee.length) { - var acct = assignee[0]; + const acct = assignee[0]; if (this.change.assignee && acct._account_id === this.change.assignee._account_id) { return; } this.set(['change', 'assignee'], acct); @@ -76,7 +85,7 @@ } }, - _computeHideStrategy: function(change) { + _computeHideStrategy(change) { return !this.changeIsOpen(change.status); }, @@ -84,7 +93,7 @@ * This is a whitelist of web link types that provide direct links to * the commit in the url property. */ - _isCommitWebLink: function(link) { + _isCommitWebLink(link) { return link.name === 'gitiles' || link.name === 'gitweb'; }, @@ -94,34 +103,34 @@ * an existential check can be used to hide or show the webLinks * section. */ - _computeWebLinks: function(commitInfo) { - if (!commitInfo || !commitInfo.web_links) { return null } + _computeWebLinks(commitInfo) { + if (!commitInfo || !commitInfo.web_links) { return null; } // We are already displaying these types of links elsewhere, // don't include in the metadata links section. - var webLinks = commitInfo.web_links.filter( - function(l) {return !this._isCommitWebLink(l); }.bind(this)); + const webLinks = commitInfo.web_links.filter( + l => {return !this._isCommitWebLink(l); }); return webLinks.length ? webLinks : null; }, - _computeStrategy: function(change) { + _computeStrategy(change) { return SubmitTypeLabel[change.submit_type]; }, - _computeLabelNames: function(labels) { + _computeLabelNames(labels) { return Object.keys(labels).sort(); }, - _computeLabelValues: function(labelName, _labels) { - var result = []; - var labels = _labels.base; - var t = labels[labelName]; + _computeLabelValues(labelName, _labels) { + const result = []; + const labels = _labels.base; + const t = labels[labelName]; if (!t) { return result; } - var approvals = t.all || []; - approvals.forEach(function(label) { + const approvals = t.all || []; + for (const label of approvals) { if (label.value && label.value != labels[labelName].default_value) { - var labelClassName; - var labelValPrefix = ''; + let labelClassName; + let labelValPrefix = ''; if (label.value > 0) { labelValPrefix = '+'; labelClassName = 'approved'; @@ -134,29 +143,43 @@ account: label, }); } - }); + } return result; }, - _computeValueTooltip: function(score, labelName) { - var values = this.change.labels[labelName].values; + _computeValueTooltip(score, labelName) { + const values = this.change.labels[labelName].values; return values[score]; }, - _handleTopicChanged: function(e, topic) { + _handleTopicChanged(e, topic) { + const lastTopic = this.change.topic; if (!topic.length) { topic = null; } - this.$.restAPI.setChangeTopic(this.change._number, topic); + this.$.restAPI.setChangeTopic(this.change._number, topic) + .then(newTopic => { + this.set(['change', 'topic'], newTopic); + if (newTopic !== lastTopic) { + this.dispatchEvent( + new CustomEvent('topic-changed', {bubbles: true})); + } + }); }, - _computeTopicReadOnly: function(mutable, change) { + _computeTopicReadOnly(mutable, change) { return !mutable || !change.actions.topic || !change.actions.topic.enabled; }, - _computeTopicPlaceholder: function(_topicReadOnly) { + _computeAssigneeReadOnly(mutable, change) { + return !mutable || + !change.actions.assignee || + !change.actions.assignee.enabled; + }, + + _computeTopicPlaceholder(_topicReadOnly) { return _topicReadOnly ? 'No Topic' : 'Click to add topic'; }, - _computeShowReviewersByState: function(serverConfig) { + _computeShowReviewersByState(serverConfig) { return !!serverConfig.note_db_enabled; }, @@ -170,9 +193,9 @@ * @param {boolean} mutable this.mutable describes whether the * change-metadata section is modifiable by the current user. */ - _computeCanDeleteVote: function(reviewer, mutable) { + _computeCanDeleteVote(reviewer, mutable) { if (!mutable) { return false; } - for (var i = 0; i < this.change.removable_reviewers.length; i++) { + for (let i = 0; i < this.change.removable_reviewers.length; i++) { if (this.change.removable_reviewers[i]._account_id === reviewer._account_id) { return true; @@ -181,60 +204,89 @@ return false; }, - _onDeleteVote: function(e) { + _onDeleteVote(e) { e.preventDefault(); - var target = Polymer.dom(e).rootTarget; - var labelName = target.labelName; - var accountID = parseInt(target.getAttribute('data-account-id'), 10); + const target = Polymer.dom(e).rootTarget; + const labelName = target.labelName; + const accountID = parseInt(target.getAttribute('data-account-id'), 10); this._xhrPromise = this.$.restAPI.deleteVote(this.change.id, accountID, labelName) - .then(function(response) { - if (!response.ok) { return response; } - - var labels = this.change.labels[labelName].all || []; - for (var i = 0; i < labels.length; i++) { - if (labels[i]._account_id === accountID) { - this.splice(['change.labels', labelName, 'all'], i, 1); - break; - } - } - }.bind(this)); + .then(response => { + if (!response.ok) { return response; } + const label = this.change.labels[labelName]; + const labels = label.all || []; + for (let i = 0; i < labels.length; i++) { + if (labels[i]._account_id === accountID) { + for (const key in label) { + if (label.hasOwnProperty(key) && + label[key]._account_id === accountID) { + // Remove special label field, keeping change label values + // in sync with the backend. + this.set(['change.labels', labelName, key], null); + } + } + this.splice(['change.labels', labelName, 'all'], i, 1); + break; + } + } + }); }, - _computeShowLabelStatus: function(change) { - var isNewChange = change.status === this.ChangeStatus.NEW; - var hasLabels = Object.keys(change.labels).length > 0; + _computeShowLabelStatus(change) { + const isNewChange = change.status === this.ChangeStatus.NEW; + const hasLabels = Object.keys(change.labels).length > 0; return isNewChange && hasLabels; }, - _computeSubmitStatus: function(labels) { - var missingLabels = []; - var output = ''; - for (var label in labels) { - var obj = labels[label]; + _computeMissingLabels(labels) { + const missingLabels = []; + for (const label in labels) { + if (!labels.hasOwnProperty(label)) { continue; } + const obj = labels[label]; if (!obj.optional && !obj.approved) { missingLabels.push(label); } } - if (missingLabels.length) { - output += 'Needs '; - output += missingLabels.join(' and '); - output += missingLabels.length > 1 ? ' labels' : ' label'; - } else { - output = 'Ready to submit'; - } - return output; + return missingLabels; }, - _computeTopicHref: function(topic) { - var encodedTopic = encodeURIComponent('\"' + topic + '\"'); - return this.getBaseUrl() + '/q/topic:' + encodeURIComponent(encodedTopic) + - '+(status:open OR status:merged)'; + _computeMissingLabelsHeader(labels) { + return 'Needs label' + + (this._computeMissingLabels(labels).length > 1 ? 's' : '') + ':'; }, - _handleTopicRemoved: function() { - this.set(['change', 'topic'], ''); - this.$.restAPI.setChangeTopic(this.change._number, null); + _showMissingLabels(labels) { + return !!this._computeMissingLabels(labels).length; + }, + + _showMissingRequirements(labels, workInProgress) { + return workInProgress || this._showMissingLabels(labels); + }, + + _computeProjectURL(project) { + return Gerrit.Nav.getUrlForProject(project); + }, + + _computeBranchURL(project, branch) { + return Gerrit.Nav.getUrlForBranch(branch, project, + this.change.status == this.ChangeStatus.NEW ? 'open' : + this.change.status.toLowerCase()); + }, + + _computeTopicURL(topic) { + return Gerrit.Nav.getUrlForTopic(topic); + }, + + _handleTopicRemoved() { + this.$.restAPI.setChangeTopic(this.change._number, null).then(() => { + this.set(['change', 'topic'], ''); + this.dispatchEvent( + new CustomEvent('topic-changed', {bubbles: true})); + }); + }, + + _computeIsWip(change) { + return !!change.work_in_progress; }, }); })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html index 4eda281..a2d7afe 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-change-metadata.html"> <script>void(0);</script> @@ -33,25 +32,25 @@ </test-fixture> <script> - suite('gr-change-metadata tests', function() { - var element; - var sandbox; + suite('gr-change-metadata tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, - getLoggedIn: function() { return Promise.resolve(false); }, + getConfig() { return Promise.resolve({}); }, + getLoggedIn() { return Promise.resolve(false); }, }); element = fixture('basic'); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('computed fields', function() { + test('computed fields', () => { assert.isFalse(element._computeHideStrategy({status: 'NEW'})); assert.isFalse(element._computeHideStrategy({status: 'DRAFT'})); assert.isTrue(element._computeHideStrategy({status: 'MERGED'})); @@ -60,22 +59,22 @@ 'Cherry Pick'); }); - test('show strategy for open change', function() { + test('show strategy for open change', () => { element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {}}; flushAsynchronousOperations(); - var strategy = element.$$('.strategy'); + const strategy = element.$$('.strategy'); assert.ok(strategy); assert.isFalse(strategy.hasAttribute('hidden')); assert.equal(strategy.children[1].innerHTML, 'Cherry Pick'); }); - test('hide strategy for closed change', function() { + test('hide strategy for closed change', () => { element.change = {status: 'MERGED', labels: {}}; flushAsynchronousOperations(); assert.isTrue(element.$$('.strategy').hasAttribute('hidden')); }); - test('show CC section when NoteDb enabled', function() { + test('show CC section when NoteDb enabled', () => { function hasCc() { return element._showReviewersByState; } @@ -87,41 +86,54 @@ assert.isTrue(hasCc()); }); - test('computes submit status', function() { - var labels = {}; - assert.equal(element._computeSubmitStatus(labels), 'Ready to submit'); - labels = {test: {}}; - assert.equal(element._computeSubmitStatus(labels), 'Needs test label'); - labels.test.approved = true; - assert.equal(element._computeSubmitStatus(labels), 'Ready to submit'); - labels.test.approved = false; - labels.test.optional = true; - assert.equal(element._computeSubmitStatus(labels), 'Ready to submit'); - labels.test.optional = false; - labels.test2 = {}; - assert.equal(element._computeSubmitStatus(labels), - 'Needs test and test2 labels'); + test('computes submit status', () => { + let showMissingLabels = false; + sandbox.stub(element, '_showMissingLabels', () => { + return showMissingLabels; + }); + assert.isFalse(element._showMissingRequirements(null, false)); + assert.isTrue(element._showMissingRequirements(null, true)); + showMissingLabels = true; + assert.isTrue(element._showMissingRequirements(null, false)); }); - test('weblinks hidden when no weblinks', function() { + test('show missing labels', () => { + let labels = {}; + assert.isFalse(element._showMissingLabels(labels)); + labels = {test: {}}; + assert.isTrue(element._showMissingLabels(labels)); + assert.deepEqual(element._computeMissingLabels(labels), ['test']); + labels.test.approved = true; + assert.isFalse(element._showMissingLabels(labels)); + labels.test.approved = false; + labels.test.optional = true; + assert.isFalse(element._showMissingLabels(labels)); + labels.test.optional = false; + labels.test2 = {}; + assert.isTrue(element._showMissingLabels(labels)); + assert.deepEqual(element._computeMissingLabels(labels), + ['test', 'test2']); + }); + + test('weblinks hidden when no weblinks', () => { element.commitInfo = {}; flushAsynchronousOperations(); - var webLinks = element.$.webLinks; + const webLinks = element.$.webLinks; assert.isTrue(webLinks.hasAttribute('hidden')); }); - test('weblinks hidden when only gitiles weblink', function() { + test('weblinks hidden when only gitiles weblink', () => { element.commitInfo = {web_links: [{name: 'gitiles', url: '#'}]}; flushAsynchronousOperations(); - var webLinks = element.$.webLinks; + const webLinks = element.$.webLinks; assert.isTrue(webLinks.hasAttribute('hidden')); assert.equal(element._computeWebLinks(element.commitInfo), null); }); - test('weblinks are visible when other weblinks', function() { + test('weblinks are visible when other weblinks', () => { element.commitInfo = {web_links: [{name: 'test', url: '#'}]}; flushAsynchronousOperations(); - var webLinks = element.$.webLinks; + const webLinks = element.$.webLinks; assert.isFalse(webLinks.hasAttribute('hidden')); assert.equal(element._computeWebLinks(element.commitInfo).length, 1); // With two non-gitiles weblinks, there are two returned. @@ -130,19 +142,32 @@ assert.equal(element._computeWebLinks(element.commitInfo).length, 2); }); - test('weblinks are visible when gitiles and other weblinks', function() { + test('weblinks are visible when gitiles and other weblinks', () => { element.commitInfo = { web_links: [{name: 'test', url: '#'}, {name: 'gitiles', url: '#'}]}; flushAsynchronousOperations(); - var webLinks = element.$.webLinks; + const webLinks = element.$.webLinks; assert.isFalse(webLinks.hasAttribute('hidden')); // Only the non-gitiles weblink is returned. assert.equal(element._computeWebLinks(element.commitInfo).length, 1); }); - suite('Topic removal', function() { - var change; - setup(function() { + test('determines whether to show "Ready to Submit" label', () => { + const showMissingSpy = sandbox.spy(element, '_showMissingRequirements'); + element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: { + test: { + all: [{_account_id: 1, name: 'bojack', value: 1}], + default_value: 0, + values: [], + }, + }}; + flushAsynchronousOperations(); + assert.isTrue(showMissingSpy.called); + }); + + suite('Topic removal', () => { + let change; + setup(() => { change = { _number: 'the number', actions: { @@ -163,8 +188,8 @@ }; }); - test('_computeTopicReadOnly', function() { - var mutable = false; + test('_computeTopicReadOnly', () => { + let mutable = false; assert.isTrue(element._computeTopicReadOnly(mutable, change)); mutable = true; assert.isTrue(element._computeTopicReadOnly(mutable, change)); @@ -174,31 +199,32 @@ assert.isTrue(element._computeTopicReadOnly(mutable, change)); }); - test('topic read only hides delete button', function() { + test('topic read only hides delete button', () => { element.mutable = false; element.change = change; flushAsynchronousOperations(); - var button = element.$$('gr-linked-chip').$$('gr-button'); + const button = element.$$('gr-linked-chip').$$('gr-button'); assert.isTrue(button.hasAttribute('hidden')); }); - test('topic not read only does not hide delete button', function() { + test('topic not read only does not hide delete button', () => { element.mutable = true; change.actions.topic.enabled = true; element.change = change; flushAsynchronousOperations(); - var button = element.$$('gr-linked-chip').$$('gr-button'); + const button = element.$$('gr-linked-chip').$$('gr-button'); assert.isFalse(button.hasAttribute('hidden')); }); }); - suite('remove reviewer votes', function() { - setup(function() { + suite('remove reviewer votes', () => { + setup(() => { sandbox.stub(element, '_computeValueTooltip').returns(''); sandbox.stub(element, '_computeTopicReadOnly').returns(true); element.change = { _number: 'the number', change_id: 'the id', + actions: [], topic: 'the topic', status: 'NEW', submit_type: 'CHERRY_PICK', @@ -211,96 +237,113 @@ }, removable_reviewers: [], }; + flushAsynchronousOperations(); }); - test('_computeCanDeleteVote hides delete button', function() { - flushAsynchronousOperations(); - var button = element.$$('gr-account-chip').$$('gr-button'); + test('_computeCanDeleteVote hides delete button', () => { + const button = element.$$('gr-account-chip').$$('gr-button'); assert.isTrue(button.hasAttribute('hidden')); element.mutable = true; assert.isTrue(button.hasAttribute('hidden')); }); - test('_computeCanDeleteVote shows delete button', function() { + test('_computeCanDeleteVote shows delete button', () => { element.change.removable_reviewers = [ { _account_id: 1, name: 'bojack', - } + }, ]; element.mutable = true; - flushAsynchronousOperations(); - var button = element.$$('gr-account-chip').$$('gr-button'); + const button = element.$$('gr-account-chip').$$('gr-button'); assert.isFalse(button.hasAttribute('hidden')); }); - test('deletes votes', function(done) { + test('deletes votes', done => { sandbox.stub(element.$.restAPI, 'deleteVote') - .returns(Promise.resolve({'ok': true})); + .returns(Promise.resolve({ok: true})); + const spliceStub = sandbox.stub(element, 'splice', (path, index, + length) => { + assert.deepEqual(path, ['change.labels', 'test', 'all']); + assert.equal(index, 0); + assert.equal(length, 1); + assert.notOk(element.change.labels.test.recommended); + spliceStub.restore(); + done(); + }); element.change.removable_reviewers = [ { _account_id: 1, name: 'bojack', - } + }, ]; + element.change.labels.test.recommended = {_account_id: 1}; element.mutable = true; flushAsynchronousOperations(); - var button = element.$$('gr-account-chip').$$('gr-button'); + const button = element.$$('gr-account-chip').$$('gr-button'); MockInteractions.tap(button); - flushAsynchronousOperations(); - var spliceStub = sinon.stub(element, 'splice', - function(path, index, length) { - assert.deepEqual(path, ['change.labels', 'test', 'all']); - assert.equal(index, 0); - assert.equal(length, 1); - spliceStub.restore(); - done(); - }); }); - test('changing topic calls setChangeTopic', function() { - var topicStub = sandbox.stub(element.$.restAPI, 'setChangeTopic', - function() {}); - element._handleTopicChanged({}, 'the new topic'); - assert.isTrue(topicStub.calledWith('the number', 'the new topic')); + test('changing topic', () => { + const newTopic = 'the new topic'; + sandbox.stub(element.$.restAPI, 'setChangeTopic').returns( + Promise.resolve(newTopic)); + element._handleTopicChanged({}, newTopic); + const topicChangedSpy = sandbox.spy(); + element.addEventListener('topic-changed', topicChangedSpy); + assert.isTrue(element.$.restAPI.setChangeTopic.calledWith( + 'the number', newTopic)); + return element.$.restAPI.setChangeTopic.lastCall.returnValue + .then(() => { + assert.equal(element.change.topic, newTopic); + assert.isTrue(topicChangedSpy.called); + }); }); - test('topic href has quotes', function() { - var hrefArr = element._computeTopicHref('test') - .split('%2522'); // Double-escaped quote. - assert.equal(hrefArr[1], 'test'); - }); - - test('clicking x on topic chip removes topic', function() { - var topicStub = sandbox.stub(element.$.restAPI, 'setChangeTopic'); - flushAsynchronousOperations(); - var remove = element.$$('gr-linked-chip').$.remove; + test('topic removal', () => { + sandbox.stub(element.$.restAPI, 'setChangeTopic').returns( + Promise.resolve()); + const remove = element.$$('gr-linked-chip').$.remove; + const topicChangedSpy = sandbox.spy(); + element.addEventListener('topic-changed', topicChangedSpy); MockInteractions.tap(remove); - assert.equal(element.change.topic, ''); - assert.isTrue(topicStub.called); + assert.isTrue(element.$.restAPI.setChangeTopic.calledWith( + 'the number', null)); + return element.$.restAPI.setChangeTopic.lastCall.returnValue + .then(() => { + assert.equal(element.change.topic, ''); + assert.isTrue(topicChangedSpy.called); + }); }); - suite('assignee field', function() { - var dummyAccount = { + suite('assignee field', () => { + const dummyAccount = { _account_id: 1, name: 'bojack', }; - var deleteStub; - var setStub; - setup(function() { + const change = { + actions: { + assignee: {enabled: false}, + }, + assignee: dummyAccount, + }; + let deleteStub; + let setStub; + + setup(() => { deleteStub = sandbox.stub(element.$.restAPI, 'deleteAssignee'); setStub = sandbox.stub(element.$.restAPI, 'setAssignee'); }); - test('changing change recomputes _assignee', function() { + test('changing change recomputes _assignee', () => { assert.isFalse(!!element._assignee.length); - var change = element.change; + const change = element.change; change.assignee = dummyAccount; element._changeChanged(change); assert.deepEqual(element._assignee[0], dummyAccount); }); - test('modifying _assignee calls API', function() { + test('modifying _assignee calls API', () => { assert.isFalse(!!element._assignee.length); element.set('_assignee', [dummyAccount]); assert.isTrue(setStub.calledOnce); @@ -313,6 +356,17 @@ element.set('_assignee', []); assert.isTrue(deleteStub.calledOnce); }); + + test('_computeAssigneeReadOnly', () => { + let mutable = false; + assert.isTrue(element._computeAssigneeReadOnly(mutable, change)); + mutable = true; + assert.isTrue(element._computeAssigneeReadOnly(mutable, change)); + change.actions.assignee.enabled = true; + assert.isFalse(element._computeAssigneeReadOnly(mutable, change)); + mutable = false; + assert.isTrue(element._computeAssigneeReadOnly(mutable, change)); + }); }); }); });
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/test/plugin.html b/polygerrit-ui/app/elements/change/gr-change-metadata/test/plugin.html new file mode 100644 index 0000000..d0ed4a1 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/test/plugin.html
@@ -0,0 +1,26 @@ +<dom-module id="my-plugin"> + <script> + Gerrit.install(plugin => { + plugin.registerStyleModule('change-metadata', 'my-plugin-style'); + }); + </script> +</dom-module> + +<dom-module id="my-plugin-style"> + <style> + html { + --change-metadata-assignee: { + display: none; + } + --change-metadata-label-status: { + display: none; + } + --change-metadata-strategy: { + display: none; + } + --change-metadata-topic: { + display: none; + } + } + </style> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html index 8455053..5b5ece6 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -14,11 +14,12 @@ limitations under the License. --> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> +<link rel="import" href="../../diff/gr-diff-preferences/gr-diff-preferences.html"> <link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> <link rel="import" href="../../shared/gr-select/gr-select.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> @@ -39,10 +40,11 @@ <link rel="import" href="../gr-messages-list/gr-messages-list.html"> <link rel="import" href="../gr-related-changes-list/gr-related-changes-list.html"> <link rel="import" href="../gr-reply-dialog/gr-reply-dialog.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-change-view"> <template> - <style> + <style include="shared-styles"> .container:not(.loading) { background-color: var(--view-background-color); } @@ -68,11 +70,19 @@ transition: box-shadow 250ms linear; width: 100%; } + .header.wip { + background-color: #fcfad6; + border-bottom: 1px solid #ddd; + margin-bottom: .5em; + } .header-title { flex: 1; font-size: 1.2em; font-weight: bold; } + .prefsButton { + float: right; + } gr-change-star { margin-right: .25em; vertical-align: -.425em; @@ -107,14 +117,17 @@ } .commitMessage { font-family: var(--monospace-font-family); - max-width: 100ch; margin-right: 1em; margin-bottom: 1em; + max-width: var(--commit-message-max-width, 72ch);; } .commitMessage gr-linked-text { overflow: auto; word-break: break-all; } + #commitMessageEditor { + min-width: 72ch; + } .editCommitMessage { margin-top: 1em; } @@ -134,7 +147,7 @@ flex-direction: column; min-width: 0; } - .commitAndRelated { + #commitAndRelated { align-content: flex-start; display: flex; flex: 1; @@ -188,6 +201,9 @@ height: 0; margin-bottom: 1em; } + #diffPrefsContainer { + margin: auto 0 auto auto; + } .patchInfo-header-wrapper { width: 100%; } @@ -206,10 +222,17 @@ .commitContainer { display: flex; flex-direction: column; + flex-shrink: 0; } .collapseToggleContainer { display: flex; } + #relatedChangesToggle { + display: none; + } + #relatedChangesToggle.showToggle { + display: flex; + } .collapseToggleContainer gr-button { display: block; } @@ -217,6 +240,24 @@ margin-left: 1em; padding-top: var(--related-change-btn-top-padding, 0); } + @media screen and (min-width: 80em) { + .commitMessage { + max-width: var(--commit-message-max-width, 100ch); + } + } + /* NOTE: If you update this breakpoint, also update the + BREAKPOINT_RELATED_MED in the JS */ + @media screen and (max-width: 60em) { + #commitAndRelated { + flex-direction: column; + flex-wrap: nowrap; + } + #commitMessageEditor { + min-width: 0; + } + } + /* NOTE: If you update this breakpoint, also update the + BREAKPOINT_RELATED_SMALL in the JS */ @media screen and (max-width: 50em) { .mobile { display: block; @@ -249,7 +290,7 @@ padding-right: 0; } .changeInfo, - .commitAndRelated { + #commitAndRelated { flex-direction: column; flex-wrap: nowrap; } @@ -277,14 +318,14 @@ </style> <div class="container loading" hidden$="[[!_loading]]">Loading...</div> <div class="container" hidden$="{{_loading}}"> - <div class="header"> + <div class$="[[_computeHeaderClass(_change)]]"> <span class="header-title"> <gr-change-star id="changeStar" change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star> <a aria-label$="[[_computeChangePermalinkAriaLabel(_change._number)]]" - href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><!-- + href$="[[_computeChangeUrl(_change)]]">[[_change._number]]</a><!-- --><template is="dom-if" if="[[_changeStatus]]"><!-- --> (<!-- --><span @@ -297,10 +338,12 @@ <gr-commit-info change="[[_change]]" commit-info="[[_computeMergedCommitInfo(_change.current_revision, _change.revisions)]]" - server-config="[[serverConfig]]"></gr-commit-info><!-- + server-config="[[_serverConfig]]"></gr-commit-info><!-- --></template><!-- -->)<!-- --></template><!-- + --><span hidden$="[[!_change.work_in_progress]]"> (Work in progress)</span><!-- + --><span>[[_privateChanges(_change)]]</span><!-- -->: [[_change.subject]] </span> </div> @@ -309,7 +352,7 @@ <gr-change-metadata change="{{_change}}" commit-info="[[_commitInfo]]" - server-config="[[serverConfig]]" + server-config="[[_serverConfig]]" mutable="[[_loggedIn]]" on-show-reply-dialog="_handleShowReplyDialog"> </gr-change-metadata> @@ -333,12 +376,13 @@ change-num="[[_changeNum]]" change-status="[[_change.status]]" commit-num="[[_commitInfo.commit]]" - patch-num="[[_computeLatestPatchNum(_allPatchSets)]]" + patch-num="[[computeLatestPatchNum(_allPatchSets)]]" commit-message="[[_latestCommitMessage]]" - on-reload-change="_handleReloadChange"></gr-change-actions> + on-reload-change="_handleReloadChange" + on-download-tap="_handleDownloadTap"></gr-change-actions> </div> <hr class="mobile"> - <div class="commitAndRelated"> + <div id="commitAndRelated"> <div class="commitContainer"> <div id="commitMessage" @@ -385,12 +429,12 @@ change="[[_change]]" has-parent="{{hasParent}}" loading="{{_relatedChangesLoading}}" - patch-num="[[_computeLatestPatchNum(_allPatchSets)]]"> + on-update="_updateRelatedChangeMaxHeight" + patch-num="[[computeLatestPatchNum(_allPatchSets)]]"> </gr-related-changes-list> <div id="relatedChangesToggle" - class="collapseToggleContainer" - hidden$="[[_computeRelatedChangesToggleHidden(_relatedChangesLoading)]]"> + class$="collapseToggleContainer [[_computeRelatedChangesToggleClass(_relatedChangesLoading)]]"> <gr-button link id="relatedChangesToggleButton" @@ -422,7 +466,8 @@ disabled$="[[_computePatchSetDisabled(patchNum.num, _patchRange.basePatchNum)]]"> [[patchNum.num]] / - [[_computeLatestPatchNum(_allPatchSets)]] + [[computeLatestPatchNum(_allPatchSets)]] + [[_computePatchSetCommentsString(_comments, patchNum.num)]] [[_computePatchSetDescription(_change, patchNum.num)]] </option> </template> @@ -430,11 +475,11 @@ / <gr-commit-info change="[[_change]]" - server-config="[[serverConfig]]" + server-config="[[_serverConfig]]" commit-info="[[_commitInfo]]"></gr-commit-info> <span class="latestPatchContainer"> / - <a href$="[[getBaseUrl()]]/c/[[_change._number]]">Go to latest patch set</a> + <a href$="[[_computeChangeUrl(_change)]]">Go to latest patch set</a> </span> <span class="downloadContainer desktop"> / @@ -452,9 +497,17 @@ read-only="[[_descriptionReadOnly]]" on-changed="_handleDescriptionChanged"></gr-editable-label> </span> + <span id="diffPrefsContainer" + hidden$="[[_computePrefsButtonHidden(_diffPrefs, _loggedIn)]]" + hidden> + <gr-button link + class="prefsButton desktop" + on-tap="_handlePrefsTap">Diff Preferences</gr-button> + </span> </div> </div> <gr-file-list id="fileList" + diff-prefs="{{_diffPrefs}}" change="[[_change]]" change-num="[[_changeNum]]" patch-range="{{_patchRange}}" @@ -463,7 +516,9 @@ revisions="[[_change.revisions]]" project-config="[[_projectConfig]]" selected-index="{{viewState.selectedFileIndex}}" - diff-view-mode="{{viewState.diffMode}}"></gr-file-list> + diff-view-mode="{{viewState.diffMode}}" + num-files-shown="{{_numFilesShown}}" + file-list-increment="{{_numFilesShown}}"></gr-file-list> </section> <gr-messages-list id="messageList" change-num="[[_changeNum]]" @@ -478,27 +533,29 @@ <gr-download-dialog id="downloadDialog" change="[[_change]]" - logged-in="[[_loggedIn]]" patch-num="[[_patchRange.patchNum]]" - config="[[serverConfig.download]]" + config="[[_serverConfig.download]]" on-close="_handleDownloadDialogClose"></gr-download-dialog> </gr-overlay> <gr-overlay id="replyOverlay" class="scrollable" no-cancel-on-outside-click + no-cancel-on-esc-key on-iron-overlay-opened="_handleReplyOverlayOpen" with-backdrop> <gr-reply-dialog id="replyDialog" change="{{_change}}" - patch-num="[[_computeLatestPatchNum(_allPatchSets)]]" + patch-num="[[computeLatestPatchNum(_allPatchSets)]]" permitted-labels="[[_change.permitted_labels]]" diff-drafts="[[_diffDrafts]]" - server-config="[[serverConfig]]" + server-config="[[_serverConfig]]" project-config="[[_projectConfig]]" + can-be-started="[[_canStartReview]]" on-send="_handleReplySent" on-cancel="_handleReplyCancel" on-autogrow="_handleReplyAutogrow" - hidden$="[[!_loggedIn]]">Reply</gr-reply-dialog> + hidden$="[[!_loggedIn]]"> + </gr-reply-dialog> </gr-overlay> <gr-js-api-interface id="jsAPI"></gr-js-api-interface> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js index baaf0029..f8a1ea2 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -14,18 +14,31 @@ (function() { 'use strict'; - var CHANGE_ID_ERROR = { + const CHANGE_ID_ERROR = { MISMATCH: 'mismatch', MISSING: 'missing', }; - var CHANGE_ID_REGEX_PATTERN = /^Change-Id\:\s(I[0-9a-f]{8,40})/gm; - var COMMENT_SAVE = 'Saving... Try again after all comments are saved.'; + const CHANGE_ID_REGEX_PATTERN = /^Change-Id\:\s(I[0-9a-f]{8,40})/gm; + const COMMENT_SAVE = 'Saving... Try again after all comments are saved.'; - var MIN_LINES_FOR_COMMIT_COLLAPSE = 30; + const MIN_LINES_FOR_COMMIT_COLLAPSE = 30; + const DEFAULT_NUM_FILES_SHOWN = 200; // Maximum length for patch set descriptions. - var PATCH_DESC_MAX_LENGTH = 500; - var REVIEWERS_REGEX = /^R=/gm; + const PATCH_DESC_MAX_LENGTH = 500; + const REVIEWERS_REGEX = /^R=/gm; + const MIN_CHECK_INTERVAL_SECS = 0; + + // These are the same as the breakpoint set in CSS. Make sure both are changed + // together. + const BREAKPOINT_RELATED_SMALL = '50em'; + const BREAKPOINT_RELATED_MED = '60em'; + + // In the event that the related changes medium width calculation is too close + // to zero, provide some height. + const MINIMUM_RELATED_MAX_HEIGHT = 100; + + const SMALL_RELATED_HEIGHT = 400; Polymer({ is: 'gr-change-view', @@ -42,6 +55,12 @@ * @event page-error */ + /** + * Fired if being logged in is required. + * + * @event show-auth-required + */ + properties: { /** * URL params passed from the router. @@ -53,20 +72,33 @@ viewState: { type: Object, notify: true, - value: function() { return {}; }, + value() { return {}; }, + observer: '_viewStateChanged', }, backPage: String, hasParent: Boolean, - serverConfig: Object, keyEventTarget: { type: Object, - value: function() { return document.body; }, + value() { return document.body; }, }, - + _serverConfig: { + type: Object, + observer: '_startUpdateCheckTimer', + }, + _diffPrefs: Object, + _numFilesShown: { + type: Number, + value: DEFAULT_NUM_FILES_SHOWN, + observer: '_numFilesShownChanged', + }, _account: { type: Object, value: {}, }, + _canStartReview: { + type: Boolean, + computed: '_computeCanStartReview(_loggedIn, _change, _account)', + }, _comments: Object, _change: { type: Object, @@ -77,7 +109,7 @@ _changeNum: String, _diffDrafts: { type: Object, - value: function() { return {}; }, + value() { return {}; }, }, _editingCommitMessage: { type: Boolean, @@ -109,7 +141,7 @@ _currentRevisionActions: Object, _allPatchSets: { type: Array, - computed: '_computeAllPatchSets(_change, _change.revisions.*)', + computed: 'computeAllPatchSets(_change, _change.revisions.*)', }, _loggedIn: { type: Boolean, @@ -121,7 +153,7 @@ _replyButtonLabel: { type: String, value: 'Reply', - computed: '_computeReplyButtonLabel(_diffDrafts.*)', + computed: '_computeReplyButtonLabel(_diffDrafts.*, _canStartReview)', }, _selectedPatchSet: String, _initialLoadComplete: { @@ -135,7 +167,7 @@ _replyDisabled: { type: Boolean, value: true, - computed: '_computeReplyDisabled(serverConfig)', + computed: '_computeReplyDisabled(_serverConfig)', }, _changeStatus: { type: String, @@ -149,15 +181,18 @@ type: Boolean, value: true, }, + _updateCheckTimerHandle: Number, }, behaviors: [ - Gerrit.BaseUrlBehavior, Gerrit.KeyboardShortcutBehavior, Gerrit.PatchSetBehavior, Gerrit.RESTClientBehavior, ], + listeners: { + 'topic-changed': '_handleTopicChanged', + }, observers: [ '_labelsChanged(_change.labels.*)', '_paramsAndChangeChanged(params, _change)', @@ -171,17 +206,22 @@ 'u': '_handleUKey', 'x': '_handleXKey', 'z': '_handleZKey', + ',': '_handleCommaKey', }, - attached: function() { - this._getLoggedIn().then(function(loggedIn) { + attached() { + this._getServerConfig().then(config => { + this._serverConfig = config; + }); + + this._getLoggedIn().then(loggedIn => { this._loggedIn = loggedIn; if (loggedIn) { - this.$.restAPI.getAccount().then(function(acct) { + this.$.restAPI.getAccount().then(acct => { this._account = acct; - }.bind(this)); + }); } - }.bind(this)); + }); this.addEventListener('comment-save', this._handleCommentSave.bind(this)); this.addEventListener('comment-discard', @@ -191,53 +231,56 @@ this.addEventListener('editable-content-cancel', this._handleCommitMessageCancel.bind(this)); this.listen(window, 'scroll', '_handleScroll'); + this.listen(document, 'visibilitychange', '_handleVisibilityChange'); }, - detached: function() { + detached() { this.unlisten(window, 'scroll', '_handleScroll'); + this.unlisten(document, 'visibilitychange', '_handleVisibilityChange'); + + if (this._updateCheckTimerHandle) { + this._cancelUpdateCheckTimer(); + } }, - _handleEditCommitMessage: function(e) { + _computePrefsButtonHidden(prefs, loggedIn) { + return !loggedIn || !prefs; + }, + + _handleEditCommitMessage(e) { this._editingCommitMessage = true; this.$.commitMessageEditor.focusTextarea(); }, - _handleCommitMessageSave: function(e) { - var message = e.detail.content; + _handleCommitMessageSave(e) { + const message = e.detail.content; this.$.jsAPI.handleCommitMessage(this._change, message); this.$.commitMessageEditor.disabled = true; - this._saveCommitMessage(message).then(function(resp) { - this.$.commitMessageEditor.disabled = false; - if (!resp.ok) { return; } + this.$.restAPI.putChangeCommitMessage( + this._changeNum, message).then(resp => { + this.$.commitMessageEditor.disabled = false; + if (!resp.ok) { return; } - this._latestCommitMessage = this._prepareCommitMsgForLinkify(message); - this._editingCommitMessage = false; - this._reloadWindow(); - }.bind(this)).catch(function(err) { - this.$.commitMessageEditor.disabled = false; - }.bind(this)); + this._latestCommitMessage = this._prepareCommitMsgForLinkify( + message); + this._editingCommitMessage = false; + this._reloadWindow(); + }).catch(err => { + this.$.commitMessageEditor.disabled = false; + }); }, - _reloadWindow: function() { + _reloadWindow() { window.location.reload(); }, - _handleCommitMessageCancel: function(e) { + _handleCommitMessageCancel(e) { this._editingCommitMessage = false; }, - _saveCommitMessage: function(message) { - return this.$.restAPI.saveChangeCommitMessageEdit( - this._changeNum, message).then(function(resp) { - if (!resp.ok) { return resp; } - - return this.$.restAPI.publishChangeEdit(this._changeNum); - }.bind(this)); - }, - - _computeHideEditCommitMessage: function(loggedIn, editing, change) { + _computeHideEditCommitMessage(loggedIn, editing, change) { if (!loggedIn || editing || change.status === this.ChangeStatus.MERGED) { return true; } @@ -245,24 +288,28 @@ return false; }, - _handleCommentSave: function(e) { + _handlePrefsTap(e) { + e.preventDefault(); + this.$.fileList.openDiffPrefs(); + }, + + _handleCommentSave(e) { if (!e.target.comment.__draft) { return; } - var draft = e.target.comment; + const draft = e.target.comment; draft.patch_set = draft.patch_set || this._patchRange.patchNum; // The use of path-based notification helpers (set, push) can’t be used // because the paths could contain dots in them. A new object must be // created to satisfy Polymer’s dirty checking. // https://github.com/Polymer/polymer/issues/3127 - // TODO(andybons): Polyfill for Object.assign in IE. - var diffDrafts = Object.assign({}, this._diffDrafts); + const diffDrafts = Object.assign({}, this._diffDrafts); if (!diffDrafts[draft.path]) { diffDrafts[draft.path] = [draft]; this._diffDrafts = diffDrafts; return; } - for (var i = 0; i < this._diffDrafts[draft.path].length; i++) { + for (let i = 0; i < this._diffDrafts[draft.path].length; i++) { if (this._diffDrafts[draft.path][i].id === draft.id) { diffDrafts[draft.path][i] = draft; this._diffDrafts = diffDrafts; @@ -270,7 +317,7 @@ } } diffDrafts[draft.path].push(draft); - diffDrafts[draft.path].sort(function(c1, c2) { + diffDrafts[draft.path].sort((c1, c2) => { // No line number means that it’s a file comment. Sort it above the // others. return (c1.line || -1) - (c2.line || -1); @@ -278,15 +325,15 @@ this._diffDrafts = diffDrafts; }, - _handleCommentDiscard: function(e) { + _handleCommentDiscard(e) { if (!e.target.comment.__draft) { return; } - var draft = e.target.comment; + const draft = e.target.comment; if (!this._diffDrafts[draft.path]) { return; } - var index = -1; - for (var i = 0; i < this._diffDrafts[draft.path].length; i++) { + let index = -1; + for (let i = 0; i < this._diffDrafts[draft.path].length; i++) { if (this._diffDrafts[draft.path][i].id === draft.id) { index = i; break; @@ -304,8 +351,7 @@ // because the paths could contain dots in them. A new object must be // created to satisfy Polymer’s dirty checking. // https://github.com/Polymer/polymer/issues/3127 - // TODO(andybons): Polyfill for Object.assign in IE. - var diffDrafts = Object.assign({}, this._diffDrafts); + const diffDrafts = Object.assign({}, this._diffDrafts); diffDrafts[draft.path].splice(index, 1); if (diffDrafts[draft.path].length === 0) { delete diffDrafts[draft.path]; @@ -313,32 +359,32 @@ this._diffDrafts = diffDrafts; }, - _handlePatchChange: function(e) { + _handlePatchChange(e) { this._changePatchNum(parseInt(e.target.value, 10), true); }, - _handleReplyTap: function(e) { + _handleReplyTap(e) { e.preventDefault(); this._openReplyDialog(); }, - _handleDownloadTap: function(e) { + _handleDownloadTap(e) { e.preventDefault(); - this.$.downloadOverlay.open().then(function() { + this.$.downloadOverlay.open().then(() => { this.$.downloadOverlay .setFocusStops(this.$.downloadDialog.getFocusStops()); this.$.downloadDialog.focus(); - }.bind(this)); + }); }, - _handleDownloadDialogClose: function(e) { + _handleDownloadDialogClose(e) { this.$.downloadOverlay.close(); }, - _handleMessageReply: function(e) { - var msg = e.detail.message.message; - var quoteStr = msg.split('\n').map( - function(line) { return '> ' + line; }).join('\n') + '\n\n'; + _handleMessageReply(e) { + const msg = e.detail.message.message; + const quoteStr = msg.split('\n').map( + line => { return '> ' + line; }).join('\n') + '\n\n'; if (quoteStr !== this.$.replyDialog.quote) { this.$.replyDialog.draft = quoteStr; @@ -347,49 +393,48 @@ this._openReplyDialog(); }, - _handleReplyOverlayOpen: function(e) { - this.$.replyDialog.focus(); + _handleReplyOverlayOpen(e) { + // This is needed so that focus is not set on the reply overlay + // when the suggestion overaly from gr-autogrow-textarea opens. + if (e.target === this.$.replyOverlay) { + this.$.replyDialog.focus(); + } }, - _handleReplySent: function(e) { + _handleReplySent(e) { this.$.replyOverlay.close(); this._reload(); }, - _handleReplyCancel: function(e) { + _handleReplyCancel(e) { this.$.replyOverlay.close(); }, - _handleReplyAutogrow: function(e) { + _handleReplyAutogrow(e) { this.$.replyOverlay.refit(); }, - _handleShowReplyDialog: function(e) { - var target = this.$.replyDialog.FocusTarget.REVIEWERS; + _handleShowReplyDialog(e) { + let target = this.$.replyDialog.FocusTarget.REVIEWERS; if (e.detail.value && e.detail.value.ccsOnly) { target = this.$.replyDialog.FocusTarget.CCS; } this._openReplyDialog(target); }, - _handleScroll: function() { - this.debounce('scroll', function() { - history.replaceState( - { - scrollTop: document.body.scrollTop, - path: location.pathname, - }, - location.pathname); + _handleScroll() { + this.debounce('scroll', () => { + this.viewState.scrollTop = document.body.scrollTop; }, 150); }, - _paramsChanged: function(value) { + _paramsChanged(value) { if (value.view !== this.tagName.toLowerCase()) { this._initialLoadComplete = false; return; } - var patchChanged = this._patchRange && + const patchChanged = this._patchRange && (value.patchNum !== undefined && value.basePatchNum !== undefined) && (this._patchRange.patchNum !== value.patchNum || this._patchRange.basePatchNum !== value.basePatchNum); @@ -398,22 +443,22 @@ this._initialLoadComplete = false; } - var patchRange = { + const patchRange = { patchNum: value.patchNum, basePatchNum: value.basePatchNum || 'PARENT', }; if (this._initialLoadComplete && patchChanged) { if (patchRange.patchNum == null) { - patchRange.patchNum = this._computeLatestPatchNum(this._allPatchSets); + patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets); } this._patchRange = patchRange; - this._reloadPatchNumDependentResources().then(function() { + this._reloadPatchNumDependentResources().then(() => { this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, { change: this._change, patchNum: patchRange.patchNum, }); - }.bind(this)); + }); return; } @@ -421,27 +466,27 @@ this._patchRange = patchRange; this.$.relatedChanges.clear(); - this._reload().then(function() { + this._reload().then(() => { this._performPostLoadTasks(); - }.bind(this)); + }); }, - _performPostLoadTasks: function() { + _performPostLoadTasks() { // Allow the message list and related changes to render before scrolling. // Related changes are loaded here (after everything else) because they // take the longest and are secondary information. Because the element may // alter the total height of the page, the call to potentially scroll to // a linked message is performed after related changes is fully loaded. - this.$.relatedChanges.reload().then(function() { - this.async(function() { - if (history.state && history.state.scrollTop) { + this.$.relatedChanges.reload().then(() => { + this.async(() => { + if (this.viewState.scrollTop) { document.documentElement.scrollTop = - document.body.scrollTop = history.state.scrollTop; + document.body.scrollTop = this.viewState.scrollTop; } else { - this._maybeScrollToMessage(); + this._maybeScrollToMessage(window.location.hash); } }, 1); - }.bind(this)); + }); this._maybeShowReplyDialog(); @@ -455,10 +500,10 @@ this._initialLoadComplete = true; }, - _paramsAndChangeChanged: function(value) { + _paramsAndChangeChanged(value) { // If the change number or patch range is different, then reset the // selected file index. - var patchRangeState = this.viewState.patchRange; + const patchRangeState = this.viewState.patchRange; if (this.viewState.changeNum !== this._changeNum || patchRangeState.basePatchNum !== this._patchRange.basePatchNum || patchRangeState.patchNum !== this._patchRange.patchNum) { @@ -466,24 +511,32 @@ } }, - _maybeScrollToMessage: function() { - var msgPrefix = '#message-'; - var hash = window.location.hash; - if (hash.indexOf(msgPrefix) === 0) { + _viewStateChanged(viewState) { + this._numFilesShown = viewState.numFilesShown ? + viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN; + }, + + _numFilesShownChanged(numFilesShown) { + this.viewState.numFilesShown = numFilesShown; + }, + + _maybeScrollToMessage(hash) { + const msgPrefix = '#message-'; + if (hash.startsWith(msgPrefix)) { this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length)); } }, - _getLocationSearch: function() { + _getLocationSearch() { // Not inlining to make it easier to test. return window.location.search; }, - _getUrlParameter: function(param) { - var pageURL = this._getLocationSearch().substring(1); - var vars = pageURL.split('&'); - for (var i = 0; i < vars.length; i++) { - var name = vars[i].split('='); + _getUrlParameter(param) { + const pageURL = this._getLocationSearch().substring(1); + const vars = pageURL.split('&'); + for (let i = 0; i < vars.length; i++) { + const name = vars[i].split('='); if (name[0] == param) { return name[0]; } @@ -491,57 +544,61 @@ return null; }, - _maybeShowRevertDialog: function() { + _maybeShowRevertDialog() { Gerrit.awaitPluginsLoaded() - .then(this._getLoggedIn.bind(this)) - .then(function(loggedIn) { - if (!loggedIn || this._change.status !== this.ChangeStatus.MERGED) { + .then(this._getLoggedIn.bind(this)) + .then(loggedIn => { + if (!loggedIn || this._change.status !== this.ChangeStatus.MERGED) { // Do not display dialog if not logged-in or the change is not // merged. - return; - } - if (!!this._getUrlParameter('revert')) { - this.$.actions.showRevertDialog(); - } - }.bind(this)); + return; + } + if (this._getUrlParameter('revert')) { + this.$.actions.showRevertDialog(); + } + }); }, - _maybeShowReplyDialog: function() { - this._getLoggedIn().then(function(loggedIn) { + _maybeShowReplyDialog() { + this._getLoggedIn().then(loggedIn => { if (!loggedIn) { return; } if (this.viewState.showReplyDialog) { this._openReplyDialog(); - this.async(function() { this.$.replyOverlay.center(); }, 1); + // TODO(kaspern@): Find a better signal for when to call center. + this.async(() => { this.$.replyOverlay.center(); }, 100); + this.async(() => { this.$.replyOverlay.center(); }, 1000); this.set('viewState.showReplyDialog', false); } - }.bind(this)); + }); }, - _resetFileListViewState: function() { + _resetFileListViewState() { this.set('viewState.selectedFileIndex', 0); + this.set('viewState.scrollTop', 0); if (!!this.viewState.changeNum && this.viewState.changeNum !== this._changeNum) { // Reset the diff mode to null when navigating from one change to // another, so that the user's preference is restored. this.set('viewState.diffMode', null); + this.set('_numFilesShown', DEFAULT_NUM_FILES_SHOWN); } this.set('viewState.changeNum', this._changeNum); this.set('viewState.patchRange', this._patchRange); }, - _changeChanged: function(change) { - if (!change) { return; } + _changeChanged(change) { + if (!change || !this._patchRange || !this._allPatchSets) { return; } this.set('_patchRange.basePatchNum', this._patchRange.basePatchNum || 'PARENT'); this.set('_patchRange.patchNum', this._patchRange.patchNum || - this._computeLatestPatchNum(this._allPatchSets)); + this.computeLatestPatchNum(this._allPatchSets)); this._updateSelected(); - var title = change.subject + ' (' + change.change_id.substr(0, 9) + ')'; - this.fire('title-change', {title: title}); + const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')'; + this.fire('title-change', {title}); }, /** @@ -551,14 +608,14 @@ * always include the patch range, even if the requested patchNum is * known to be the latest. */ - _changePatchNum: function(patchNum, opt_forceParams) { + _changePatchNum(patchNum, opt_forceParams) { if (!opt_forceParams) { - var currentPatchNum; + let currentPatchNum; if (this._change.current_revision) { currentPatchNum = this._change.revisions[this._change.current_revision]._number; } else { - currentPatchNum = this._computeLatestPatchNum(this._allPatchSets); + currentPatchNum = this.computeLatestPatchNum(this._allPatchSets); } if (patchNum === currentPatchNum && this._patchRange.basePatchNum === 'PARENT') { @@ -566,19 +623,19 @@ return; } } - var patchExpr = this._patchRange.basePatchNum === 'PARENT' ? patchNum : + const patchExpr = this._patchRange.basePatchNum === 'PARENT' ? patchNum : this._patchRange.basePatchNum + '..' + patchNum; page.show(this.changePath(this._changeNum) + '/' + patchExpr); }, - _computeChangePermalink: function(changeNum) { - return this.getBaseUrl() + '/' + changeNum; + _computeChangeUrl(change) { + return Gerrit.Nav.getUrlForChange(change); }, - _computeChangeStatus: function(change, patchNum) { - var statusString = this.changeStatusString(change); + _computeChangeStatus(change, patchNum) { + let statusString = this.changeStatusString(change); if (change.status === this.ChangeStatus.NEW) { - var rev = this.getRevisionByPatchNum(change.revisions, patchNum); + const rev = this.getRevisionByPatchNum(change.revisions, patchNum); if (rev && rev.draft === true) { statusString = 'Draft'; } @@ -586,12 +643,16 @@ return statusString; }, - _computeShowCommitInfo: function(changeStatus, current_revision) { + _privateChanges(change) { + return change.is_private ? ' (Private)' : ''; + }, + + _computeShowCommitInfo(changeStatus, current_revision) { return changeStatus === 'Merged' && current_revision; }, - _computeMergedCommitInfo: function(current_revision, revisions) { - var rev = revisions[current_revision]; + _computeMergedCommitInfo(current_revision, revisions) { + const rev = revisions[current_revision]; if (!rev || !rev.commit) { return {}; } // CommitInfo.commit is optional. Set commit in all cases to avoid error // in <gr-commit-info>. @see Issue 5337 @@ -599,11 +660,11 @@ return rev.commit; }, - _computeChangeIdClass: function(displayChangeId) { + _computeChangeIdClass(displayChangeId) { return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : ''; }, - _computeTitleAttributeWarning: function(displayChangeId) { + _computeTitleAttributeWarning(displayChangeId) { if (displayChangeId === CHANGE_ID_ERROR.MISMATCH) { return 'Change-Id mismatch'; } else if (displayChangeId === CHANGE_ID_ERROR.MISSING) { @@ -611,12 +672,12 @@ } }, - _computeChangeIdCommitMessageError: function(commitMessage, change) { + _computeChangeIdCommitMessageError(commitMessage, change) { if (!commitMessage) { return CHANGE_ID_ERROR.MISSING; } // Find the last match in the commit message: - var changeId; - var changeIdArr; + let changeId; + let changeIdArr; while (changeIdArr = CHANGE_ID_REGEX_PATTERN.exec(commitMessage)) { changeId = changeIdArr[1]; @@ -636,13 +697,9 @@ return CHANGE_ID_ERROR.MISSING; }, - _computeLatestPatchNum: function(allPatchSets) { - return allPatchSets[allPatchSets.length - 1].num; - }, - - _computePatchInfoClass: function(patchNum, allPatchSets) { + _computePatchInfoClass(patchNum, allPatchSets) { if (parseInt(patchNum, 10) === - this._computeLatestPatchNum(allPatchSets)) { + this.computeLatestPatchNum(allPatchSets)) { return ''; } return 'patchInfo--oldPatchSet'; @@ -655,37 +712,24 @@ * @param {Number|String} basePatchNum Base patch number from file list * @return {Boolean} */ - _computePatchSetDisabled: function(patchNum, basePatchNum) { + _computePatchSetDisabled(patchNum, basePatchNum) { basePatchNum = basePatchNum === 'PARENT' ? 0 : basePatchNum; return parseInt(patchNum, 10) <= parseInt(basePatchNum, 10); }, - _computeAllPatchSets: function(change) { - var patchNums = []; - for (var commit in change.revisions) { - if (change.revisions.hasOwnProperty(commit)) { - patchNums.push({ - num: change.revisions[commit]._number, - desc: change.revisions[commit].description, - }); - } - } - return patchNums.sort(function(a, b) { return a.num - b.num; }); - }, - - _computeLabelNames: function(labels) { + _computeLabelNames(labels) { return Object.keys(labels).sort(); }, - _computeLabelValues: function(labelName, labels) { - var result = []; - var t = labels[labelName]; + _computeLabelValues(labelName, labels) { + const result = []; + const t = labels[labelName]; if (!t) { return result; } - var approvals = t.all || []; - approvals.forEach(function(label) { + const approvals = t.all || []; + for (label of approvals) { if (label.value && label.value != labels[labelName].default_value) { - var labelClassName; - var labelValPrefix = ''; + let labelClassName; + let labelValPrefix = ''; if (label.value > 0) { labelValPrefix = '+'; labelClassName = 'approved'; @@ -698,33 +742,44 @@ account: label, }); } - }); + } return result; }, - _computeReplyButtonLabel: function(changeRecord) { - var drafts = (changeRecord && changeRecord.base) || {}; - var draftCount = Object.keys(drafts).reduce(function(count, file) { + _computeReplyButtonLabel(changeRecord, canStartReview) { + if (canStartReview) { + return 'Start review'; + } + + const drafts = (changeRecord && changeRecord.base) || {}; + const draftCount = Object.keys(drafts).reduce((count, file) => { return count + drafts[file].length; }, 0); - var label = 'Reply'; + let label = 'Reply'; if (draftCount > 0) { label += ' (' + draftCount + ')'; } return label; }, - _handleAKey: function(e) { + _handleAKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e) || - !this._loggedIn) { return; } + this.modifierPressed(e)) { + return; + } + this._getLoggedIn().then(isLoggedIn => { + if (!isLoggedIn) { + this.fire('show-auth-required'); + return; + } - e.preventDefault(); - this._openReplyDialog(); + e.preventDefault(); + this._openReplyDialog(); + }); }, - _handleDKey: function(e) { + _handleDKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -732,13 +787,13 @@ this.$.downloadOverlay.open(); }, - _handleCapitalRKey: function(e) { + _handleCapitalRKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); page.show('/c/' + this._change._number); }, - _handleSKey: function(e) { + _handleSKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -746,14 +801,14 @@ this.$.changeStar.toggleStar(); }, - _handleUKey: function(e) { + _handleUKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); this._determinePageBack(); }, - _handleXKey: function(e) { + _handleXKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -761,7 +816,7 @@ this.$.messageList.handleExpandCollapse(true); }, - _handleZKey: function(e) { + _handleZKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -769,20 +824,26 @@ this.$.messageList.handleExpandCollapse(false); }, - _determinePageBack: function() { + _handleCommaKey(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.fileList.openDiffPrefs(); + }, + + _determinePageBack() { // Default backPage to '/' if user came to change view page // via an email link, etc. page.show(this.backPage || '/'); }, - _handleLabelRemoved: function(splices, path) { - for (var i = 0; i < splices.length; i++) { - var splice = splices[i]; - for (var j = 0; j < splice.removed.length; j++) { - var removed = splice.removed[j]; - var changePath = path.split('.'); - var labelPath = changePath.splice(0, changePath.length - 2); - var labelDict = this.get(labelPath); + _handleLabelRemoved(splices, path) { + for (const splice of splices) { + for (const removed of splice.removed) { + const changePath = path.split('.'); + const labelPath = changePath.splice(0, changePath.length - 2); + const labelDict = this.get(labelPath); if (labelDict.approved && labelDict.approved._account_id === removed._account_id) { this._reload(); @@ -792,9 +853,9 @@ } }, - _labelsChanged: function(changeRecord) { + _labelsChanged(changeRecord) { if (!changeRecord) { return; } - if (changeRecord.value.indexSplices) { + if (changeRecord.value && changeRecord.value.indexSplices) { this._handleLabelRemoved(changeRecord.value.indexSplices, changeRecord.path); } @@ -803,51 +864,57 @@ }); }, - _openReplyDialog: function(opt_section) { + _openReplyDialog(opt_section) { if (this.$.restAPI.hasPendingDiffDrafts()) { this.dispatchEvent(new CustomEvent('show-alert', {detail: {message: COMMENT_SAVE}, bubbles: true})); return; } - this.$.replyOverlay.open().then(function() { + this.$.replyOverlay.open().then(() => { this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops()); this.$.replyDialog.open(opt_section); - }.bind(this)); + Polymer.dom.flush(); + this.$.replyOverlay.center(); + }); }, - _handleReloadChange: function(e) { - return this._reload().then(function() { + _handleReloadChange(e) { + return this._reload().then(() => { // If the change was rebased, we need to reload the page with the // latest patch. if (e.detail.action === 'rebase') { page.show(this.changePath(this._changeNum)); } - }.bind(this)); + }); }, - _handleGetChangeDetailError: function(response) { - this.fire('page-error', {response: response}); + _handleGetChangeDetailError(response) { + this.fire('page-error', {response}); }, - _getDiffDrafts: function() { + _getDiffDrafts() { return this.$.restAPI.getDiffDrafts(this._changeNum).then( - function(drafts) { + drafts => { return this._diffDrafts = drafts; - }.bind(this)); + }); }, - _getLoggedIn: function() { + _getLoggedIn() { return this.$.restAPI.getLoggedIn(); }, - _getProjectConfig: function() { - return this.$.restAPI.getProjectConfig(this._change.project).then( - function(config) { - this._projectConfig = config; - }.bind(this)); + _getServerConfig() { + return this.$.restAPI.getConfig(); }, - _updateRebaseAction: function(revisionActions) { + _getProjectConfig() { + return this.$.restAPI.getProjectConfig(this._change.project).then( + config => { + this._projectConfig = config; + }); + }, + + _updateRebaseAction(revisionActions) { if (revisionActions && revisionActions.rebase) { revisionActions.rebase.rebaseOnCurrent = !!revisionActions.rebase.enabled; @@ -856,73 +923,76 @@ return revisionActions; }, - _prepareCommitMsgForLinkify: function(msg) { + _prepareCommitMsgForLinkify(msg) { // TODO(wyatta) switch linkify sequence, see issue 5526. // This is a zero-with space. It is added to prevent the linkify library // from including R= as part of the email address. return msg.replace(REVIEWERS_REGEX, 'R=\u200B'); }, - _getChangeDetail: function() { + _getChangeDetail() { return this.$.restAPI.getChangeDetail(this._changeNum, this._handleGetChangeDetailError.bind(this)).then( - function(change) { - // Issue 4190: Coalesce missing topics to null. - if (!change.topic) { change.topic = null; } - if (!change.reviewer_updates) { - change.reviewer_updates = null; - } - var latestRevisionSha = this._getLatestRevisionSHA(change); - var currentRevision = change.revisions[latestRevisionSha]; - if (currentRevision.commit && currentRevision.commit.message) { - this._latestCommitMessage = this._prepareCommitMsgForLinkify( - currentRevision.commit.message); - } else { - this._latestCommitMessage = null; - } - var lineHeight = getComputedStyle(this).lineHeight; - this._lineHeight = lineHeight.slice(0, lineHeight.length - 2); + change => { + if (!change) { + return ''; + } + // Issue 4190: Coalesce missing topics to null. + if (!change.topic) { change.topic = null; } + if (!change.reviewer_updates) { + change.reviewer_updates = null; + } + const latestRevisionSha = this._getLatestRevisionSHA(change); + const currentRevision = change.revisions[latestRevisionSha]; + if (currentRevision.commit && currentRevision.commit.message) { + this._latestCommitMessage = this._prepareCommitMsgForLinkify( + currentRevision.commit.message); + } else { + this._latestCommitMessage = null; + } + const lineHeight = getComputedStyle(this).lineHeight; + this._lineHeight = lineHeight.slice(0, lineHeight.length - 2); - this._change = change; - if (!this._patchRange || !this._patchRange.patchNum || + this._change = change; + if (!this._patchRange || !this._patchRange.patchNum || this._patchRange.patchNum === currentRevision._number) { // CommitInfo.commit is optional, and may need patching. - if (!currentRevision.commit.commit) { - currentRevision.commit.commit = latestRevisionSha; - } - this._commitInfo = currentRevision.commit; - this._currentRevisionActions = + if (!currentRevision.commit.commit) { + currentRevision.commit.commit = latestRevisionSha; + } + this._commitInfo = currentRevision.commit; + this._currentRevisionActions = this._updateRebaseAction(currentRevision.actions); // TODO: Fetch and process files. - } - }.bind(this)); + } + }); }, - _getComments: function() { + _getComments() { return this.$.restAPI.getDiffComments(this._changeNum).then( - function(comments) { + comments => { this._comments = comments; - }.bind(this)); + }); }, - _getLatestCommitMessage: function() { + _getLatestCommitMessage() { return this.$.restAPI.getChangeCommitInfo(this._changeNum, - this._computeLatestPatchNum(this._allPatchSets)).then( - function(commitInfo) { - this._latestCommitMessage = + this.computeLatestPatchNum(this._allPatchSets)).then( + commitInfo => { + this._latestCommitMessage = this._prepareCommitMsgForLinkify(commitInfo.message); - }.bind(this)); + }); }, - _getLatestRevisionSHA: function(change) { + _getLatestRevisionSHA(change) { if (change.current_revision) { return change.current_revision; } // current_revision may not be present in the case where the latest rev is // a draft and the user doesn’t have permission to view that rev. - var latestRev = null; - var latestPatchNum = -1; - for (var rev in change.revisions) { + let latestRev = null; + let latestPatchNum = -1; + for (const rev in change.revisions) { if (!change.revisions.hasOwnProperty(rev)) { continue; } if (change.revisions[rev]._number > latestPatchNum) { @@ -933,54 +1003,54 @@ return latestRev; }, - _getCommitInfo: function() { + _getCommitInfo() { return this.$.restAPI.getChangeCommitInfo( this._changeNum, this._patchRange.patchNum).then( - function(commitInfo) { - this._commitInfo = commitInfo; - }.bind(this)); + commitInfo => { + this._commitInfo = commitInfo; + }); }, - _reloadDiffDrafts: function() { + _reloadDiffDrafts() { this._diffDrafts = {}; - this._getDiffDrafts().then(function() { + this._getDiffDrafts().then(() => { if (this.$.replyOverlay.opened) { - this.async(function() { this.$.replyOverlay.center(); }, 1); + this.async(() => { this.$.replyOverlay.center(); }, 1); } - }.bind(this)); + }); }, - _reload: function() { + _reload() { this._loading = true; this._relatedChangesCollapsed = true; - this._getLoggedIn().then(function(loggedIn) { + this._getLoggedIn().then(loggedIn => { if (!loggedIn) { return; } this._reloadDiffDrafts(); - }.bind(this)); + }); - var detailCompletes = this._getChangeDetail().then(function() { + const detailCompletes = this._getChangeDetail().then(() => { this._loading = false; this._getProjectConfig(); - }.bind(this)); + }); this._getComments(); if (this._patchRange.patchNum) { return Promise.all([ this._reloadPatchNumDependentResources(), detailCompletes, - ]).then(function() { + ]).then(() => { return this.$.actions.reload(); - }.bind(this)); + }); } else { // The patch number is reliant on the change detail request. - return detailCompletes.then(function() { + return detailCompletes.then(() => { this.$.fileList.reload(); if (!this._latestCommitMessage) { this._getLatestCommitMessage(); } - }.bind(this)); + }); } }, @@ -988,39 +1058,61 @@ * Kicks off requests for resources that rely on the patch range * (`this._patchRange`) being defined. */ - _reloadPatchNumDependentResources: function() { + _reloadPatchNumDependentResources() { return Promise.all([ this._getCommitInfo(), this.$.fileList.reload(), ]); }, - _updateSelected: function() { + _updateSelected() { this._selectedPatchSet = this._patchRange.patchNum; }, - _computePatchSetDescription: function(change, patchNum) { - var rev = this.getRevisionByPatchNum(change.revisions, patchNum); + _computePatchSetDescription(change, patchNum) { + const rev = this.getRevisionByPatchNum(change.revisions, patchNum); return (rev && rev.description) ? rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; }, - _computeDescriptionPlaceholder: function(readOnly) { + _computePatchSetCommentsString(allComments, patchNum) { + let numComments = 0; + let numUnresolved = 0; + for (const file in allComments) { + if (allComments.hasOwnProperty(file)) { + numComments += this.$.fileList.getCommentsForPath( + allComments, patchNum, file).length; + numUnresolved += this.$.fileList.computeUnresolvedNum( + allComments, {}, patchNum, file); + } + } + let commentsStr = ''; + if (numComments > 0) { + commentsStr = '(' + numComments + ' comments'; + if (numUnresolved > 0) { + commentsStr += ', ' + numUnresolved + ' unresolved'; + } + commentsStr += ')'; + } + return commentsStr; + }, + + _computeDescriptionPlaceholder(readOnly) { return (readOnly ? 'No' : 'Add a') + ' patch set description'; }, - _handleDescriptionChanged: function(e) { - var desc = e.detail.trim(); - var rev = this.getRevisionByPatchNum(this._change.revisions, + _handleDescriptionChanged(e) { + const desc = e.detail.trim(); + const rev = this.getRevisionByPatchNum(this._change.revisions, this._selectedPatchSet); - var sha = this._getPatchsetHash(this._change.revisions, rev); + const sha = this._getPatchsetHash(this._change.revisions, rev); this.$.restAPI.setDescription(this._changeNum, this._selectedPatchSet, desc) - .then(function(res) { + .then(res => { if (res.ok) { this.set(['_change', 'revisions', sha, 'description'], desc); } - }.bind(this)); + }); }, @@ -1029,8 +1121,8 @@ * @param {Object} patchSet A revision already fetched from {revisions} * @return {string} the SHA hash corresponding to the revision. */ - _getPatchsetHash: function(revisions, patchSet) { - for (var rev in revisions) { + _getPatchsetHash(revisions, patchSet) { + for (const rev in revisions) { if (revisions.hasOwnProperty(rev) && revisions[rev] === patchSet) { return rev; @@ -1038,65 +1130,70 @@ } }, - _computeDescriptionReadOnly: function(loggedIn, change, account) { + _computeCanStartReview(loggedIn, change, account) { + return !!(loggedIn && change.work_in_progress && + change.owner._account_id === account._account_id); + }, + + _computeDescriptionReadOnly(loggedIn, change, account) { return !(loggedIn && (account._account_id === change.owner._account_id)); }, - _computeReplyDisabled: function() { return false; }, + _computeReplyDisabled() { return false; }, - _computeChangePermalinkAriaLabel: function(changeNum) { + _computeChangePermalinkAriaLabel(changeNum) { return 'Change ' + changeNum; }, - _computeCommitClass: function(collapsed, commitMessage) { + _computeCommitClass(collapsed, commitMessage) { if (this._computeCommitToggleHidden(commitMessage)) { return ''; } return collapsed ? 'collapsed' : ''; }, - _computeRelatedChangesClass: function(collapsed, loading) { + _computeRelatedChangesClass(collapsed, loading) { if (!loading && !this.customStyle['--relation-chain-max-height']) { this._updateRelatedChangeMaxHeight(); } return collapsed ? 'collapsed' : ''; }, - _computeCollapseText: function(collapsed) { + _computeCollapseText(collapsed) { // Symbols are up and down triangles. return collapsed ? '\u25bc Show more' : '\u25b2 Show less'; }, - _toggleCommitCollapsed: function() { + _toggleCommitCollapsed() { this._commitCollapsed = !this._commitCollapsed; if (this._commitCollapsed) { window.scrollTo(0, 0); } }, - _toggleRelatedChangesCollapsed: function() { + _toggleRelatedChangesCollapsed() { this._relatedChangesCollapsed = !this._relatedChangesCollapsed; if (this._relatedChangesCollapsed) { window.scrollTo(0, 0); } }, - _computeCommitToggleHidden: function(commitMessage) { + _computeCommitToggleHidden(commitMessage) { if (!commitMessage) { return true; } return commitMessage.split('\n').length < MIN_LINES_FOR_COMMIT_COLLAPSE; }, - _getOffsetHeight: function(element) { + _getOffsetHeight(element) { return element.offsetHeight; }, - _getScrollHeight: function(element) { + _getScrollHeight(element) { return element.scrollHeight; }, /** * Get the line height of an element to the nearest integer. */ - _getLineHeight: function(element) { - var lineHeightStr = getComputedStyle(element).lineHeight; + _getLineHeight(element) { + const lineHeightStr = getComputedStyle(element).lineHeight; return Math.round(lineHeightStr.slice(0, lineHeightStr.length - 2)); }, @@ -1104,41 +1201,121 @@ * New max height for the related changes section, shorter than the existing * change info height. */ - _updateRelatedChangeMaxHeight: function() { + _updateRelatedChangeMaxHeight() { // Takes into account approximate height for the expand button and - // bottom margin - var extraHeight = 24; - var maxExistingHeight; - var hasCommitToggle = + // bottom margin. + const EXTRA_HEIGHT = 30; + let newHeight; + const hasCommitToggle = !this._computeCommitToggleHidden(this._latestCommitMessage); - if (hasCommitToggle) { - // Make sure the content is lined up if both areas have buttons. If the - // commit message is not collapsed, instead use the change info hight. - maxExistingHeight = this._getOffsetHeight(this.$.commitMessage); - } else { - maxExistingHeight = this._getOffsetHeight(this.$.mainChangeInfo) - - extraHeight; - } + if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_SMALL})`) + .matches) { + // In a small (mobile) view, give the relation chain some space. + newHeight = SMALL_RELATED_HEIGHT; + } else if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_MED})`) + .matches) { + // Since related changes are below the commit message, but still next to + // metadata, the height should be the height of the metadata minus the + // height of the commit message to reduce jank. However, if that doesn't + // result in enough space, instead use the MINIMUM_RELATED_MAX_HEIGHT. + // Note: extraHeight is to take into account margin/padding. + const medRelatedHeight = Math.max( + this._getOffsetHeight(this.$.mainChangeInfo) - + this._getOffsetHeight(this.$.commitMessage) - 2 * EXTRA_HEIGHT, + MINIMUM_RELATED_MAX_HEIGHT); + newHeight = medRelatedHeight; + } else { + if (hasCommitToggle) { + // Make sure the content is lined up if both areas have buttons. If + // the commit message is not collapsed, instead use the change info + // height. + newHeight = this._getOffsetHeight(this.$.commitMessage); + } else { + newHeight = this._getOffsetHeight(this.$.commitAndRelated) - + EXTRA_HEIGHT; + } + } + if (this.$.relatedChanges.hidden) { + this.customStyle['--commit-message-max-width'] = 'none'; + } // Get the line height of related changes, and convert it to the nearest // integer. - var lineHeight = this._getLineHeight(this.$.relatedChanges); + const lineHeight = this._getLineHeight(this.$.relatedChanges); // Figure out a new height that is divisible by the rounded line height. - var remainder = maxExistingHeight % lineHeight; - var newHeight = maxExistingHeight - remainder; + const remainder = newHeight % lineHeight; + newHeight = newHeight - remainder; - // Update the max-height of the relation chain to this new height; this.customStyle['--relation-chain-max-height'] = newHeight + 'px'; + + // Update the max-height of the relation chain to this new height. if (hasCommitToggle) { - this.customStyle['--related-change-btn-top-padding'] = remainder + 'px'; + this.customStyle['--related-change-btn-top-padding'] = + remainder + 'px'; } this.updateStyles(); }, - _computeRelatedChangesToggleHidden: function() { - return this._getScrollHeight(this.$.relatedChanges) <= - this._getOffsetHeight(this.$.relatedChanges); + _computeRelatedChangesToggleClass() { + // Prevents showMore from showing when click on related change, since the + // line height would be positive, but related changes height is 0. + if (!this._getScrollHeight(this.$.relatedChanges)) { return ''; } + + return this._getScrollHeight(this.$.relatedChanges) > + (this._getOffsetHeight(this.$.relatedChanges) + + this._getLineHeight(this.$.relatedChanges)) ? 'showToggle' : ''; + }, + + _startUpdateCheckTimer() { + if (!this._serverConfig || + !this._serverConfig.change || + this._serverConfig.change.update_delay === undefined || + this._serverConfig.change.update_delay <= MIN_CHECK_INTERVAL_SECS) { + return; + } + + this._updateCheckTimerHandle = this.async(() => { + this.fetchIsLatestKnown(this._change, this.$.restAPI) + .then(latest => { + if (latest) { + this._startUpdateCheckTimer(); + } else { + this._cancelUpdateCheckTimer(); + this.fire('show-alert', { + message: 'A newer patch set has been uploaded.', + // Persist this alert. + dismissOnNavigation: true, + action: 'Reload', + callback: function() { + // Load the current change without any patch range. + Gerrit.Nav.navigateToChange(this._change); + }.bind(this), + }); + } + }); + }, this._serverConfig.change.update_delay * 1000); + }, + + _cancelUpdateCheckTimer() { + this.cancelAsync(this._updateCheckTimerHandle); + this._updateCheckTimerHandle = null; + }, + + _handleVisibilityChange() { + if (document.hidden && this._updateCheckTimerHandle) { + this._cancelUpdateCheckTimer(); + } else if (!this._updateCheckTimerHandle) { + this._startUpdateCheckTimer(); + } + }, + + _handleTopicChanged() { + this.$.relatedChanges.reload(); + }, + + _computeHeaderClass(change) { + return change.work_in_progress ? 'header wip' : 'header'; }, }); })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html index 28164cd..797b6b3 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../bower_components/page/page.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-change-view.html"> <script>void(0);</script> @@ -34,114 +34,179 @@ </test-fixture> <script> - suite('gr-change-view tests', function() { - var element; - var sandbox; - var showStub; - var TEST_SCROLL_TOP_PX = 100; + suite('gr-change-view tests', () => { + let element; + let sandbox; + let showStub; + const TEST_SCROLL_TOP_PX = 100; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); showStub = sandbox.stub(page, 'show'); stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, - getAccount: function() { return Promise.resolve(null); }, + getConfig() { return Promise.resolve({test: 'config'}); }, + getAccount() { return Promise.resolve(null); }, }); element = fixture('basic'); }); - teardown(function(done) { - flush(function() { + teardown(done => { + flush(() => { sandbox.restore(); done(); }); }); - suite('keyboard shortcuts', function() { - test('S should toggle the CL star', function() { - var starStub = sandbox.stub(element.$.changeStar, 'toggleStar'); + suite('keyboard shortcuts', () => { + test('S should toggle the CL star', () => { + const starStub = sandbox.stub(element.$.changeStar, 'toggleStar'); MockInteractions.pressAndReleaseKeyOn(element, 83, null, 's'); assert(starStub.called); }); - test('U should navigate to / if no backPage set', function() { + test('U should navigate to / if no backPage set', () => { MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); assert(showStub.lastCall.calledWithExactly('/')); }); - test('U should navigate to backPage if set', function() { + test('U should navigate to backPage if set', () => { element.backPage = '/dashboard/self'; MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); assert(showStub.lastCall.calledWithExactly('/dashboard/self')); }); - test('A should toggle overlay', function() { + test('A fires an error event when not logged in', done => { + sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(false)); + const loggedInErrorSpy = sandbox.spy(); + element.addEventListener('show-auth-required', loggedInErrorSpy); MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); - var overlayEl = element.$.replyOverlay; - assert.isFalse(overlayEl.opened); - element._loggedIn = true; - - MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a'); - assert.isFalse(overlayEl.opened); - - MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); - assert.isTrue(overlayEl.opened); - overlayEl.close(); - assert.isFalse(overlayEl.opened); + flush(() => { + assert.isFalse(element.$.replyOverlay.opened); + assert.isTrue(loggedInErrorSpy.called); + done(); + }); }); - test('X should expand all messages', function() { - var handleExpand = + test('shift A does not open reply overlay', done => { + sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true)); + MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a'); + flush(() => { + assert.isFalse(element.$.replyOverlay.opened); + done(); + }); + }); + + test('A toggles overlay when logged in', done => { + sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true)); + sandbox.stub(element.$.replyDialog, 'fetchIsLatestKnown') + .returns(Promise.resolve(true)); + element._change = {labels: {}}; + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); + flush(() => { + assert.isTrue(element.$.replyOverlay.opened); + element.$.replyOverlay.close(); + assert.isFalse(element.$.replyOverlay.opened); + done(); + }); + }); + + test('X should expand all messages', () => { + const handleExpand = sandbox.stub(element.$.messageList, 'handleExpandCollapse'); MockInteractions.pressAndReleaseKeyOn(element, 88, null, 'x'); assert(handleExpand.calledWith(true)); }); - test('Z should collapse all messages', function() { - var handleExpand = + test('Z should collapse all messages', () => { + const handleExpand = sandbox.stub(element.$.messageList, 'handleExpandCollapse'); MockInteractions.pressAndReleaseKeyOn(element, 90, null, 'z'); assert(handleExpand.calledWith(false)); }); test('shift + R should fetch and navigate to the latest patch set', - function(done) { - element._changeNum = '42'; - element._patchRange = { - basePatchNum: 'PARENT', - patchNum: 1, - }; - element._change = { - change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', - _number: 42, - revisions: { - rev1: {_number: 1}, - }, - current_revision: 'rev1', - status: 'NEW', - labels: {}, - actions: {}, - }; + done => { + element._changeNum = '42'; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 1, + }; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + _number: 42, + revisions: { + rev1: {_number: 1}, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + actions: {}, + }; - sandbox.stub(element.$.actions, 'reload'); + sandbox.stub(element.$.actions, 'reload'); - showStub.restore(); - showStub = sandbox.stub(page, 'show', function(arg) { - assert.equal(arg, '/c/42'); - done(); - }); + showStub.restore(); + showStub = sandbox.stub(page, 'show', arg => { + assert.equal(arg, '/c/42'); + done(); + }); - MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r'); + MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r'); + }); + + test('d should open download overlay', () => { + const stub = sandbox.stub(element.$.downloadOverlay, 'open'); + MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd'); + assert.isTrue(stub.called); }); - test('d should open download overlay', function() { - var stub = sandbox.stub(element.$.downloadOverlay, 'open'); - MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd'); + test(', should open diff preferences', () => { + const stub = sandbox.stub(element.$.fileList.$.diffPreferences, 'open'); + MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); assert.isTrue(stub.called); }); }); - test('_computeDescriptionReadOnly', function() { + test('fetches the server config on attached', done => { + flush(() => { + assert.equal(element._serverConfig.test, 'config'); + done(); + }); + }); + + test('Diff preferences hidden when no prefs or logged out', () => { + element._loggedIn = false; + flushAsynchronousOperations(); + assert.isTrue(element.$.diffPrefsContainer.hidden); + + element._loggedIn = true; + flushAsynchronousOperations(); + assert.isTrue(element.$.diffPrefsContainer.hidden); + + element._loggedIn = false; + element._diffPrefs = {font_size: '12'}; + flushAsynchronousOperations(); + assert.isTrue(element.$.diffPrefsContainer.hidden); + + element._loggedIn = true; + flushAsynchronousOperations(); + assert.isFalse(element.$.diffPrefsContainer.hidden); + }); + + test('prefsButton opens gr-diff-preferences', () => { + const handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap'); + const overlayOpenStub = sandbox.stub(element.$.fileList, + 'openDiffPrefs'); + const prefsButton = Polymer.dom(element.root).querySelectorAll( + '.prefsButton')[0]; + + MockInteractions.tap(prefsButton); + + assert.isTrue(handlePrefsTapSpy.called); + assert.isTrue(overlayOpenStub.called); + }); + + test('_computeDescriptionReadOnly', () => { assert.equal(element._computeDescriptionReadOnly(false, {owner: {_account_id: 1}}, {_account_id: 1}), true); assert.equal(element._computeDescriptionReadOnly(true, @@ -150,16 +215,16 @@ {owner: {_account_id: 1}}, {_account_id: 1}), false); }); - test('_computeDescriptionPlaceholder', function() { + test('_computeDescriptionPlaceholder', () => { assert.equal(element._computeDescriptionPlaceholder(true), 'No patch set description'); assert.equal(element._computeDescriptionPlaceholder(false), 'Add a patch set description'); }); - test('_computePatchSetDisabled', function() { - var basePatchNum = 'PARENT'; - var patchNum = 1; + test('_computePatchSetDisabled', () => { + let basePatchNum = 'PARENT'; + let patchNum = 1; assert.equal(element._computePatchSetDisabled(patchNum, basePatchNum), false); basePatchNum = 1; @@ -170,18 +235,56 @@ false); }); - test('_prepareCommitMsgForLinkify', function() { - var commitMessage = 'R=test@google.com'; - var result = element._prepareCommitMsgForLinkify(commitMessage); + test('_prepareCommitMsgForLinkify', () => { + let commitMessage = 'R=test@google.com'; + let result = element._prepareCommitMsgForLinkify(commitMessage); assert.equal(result, 'R=\u200Btest@google.com'); commitMessage = 'R=test@google.com\nR=test@google.com'; - var result = element._prepareCommitMsgForLinkify(commitMessage); + result = element._prepareCommitMsgForLinkify(commitMessage); assert.equal(result, 'R=\u200Btest@google.com\nR=\u200Btest@google.com'); }), - test('_handleDescriptionChanged', function() { - var putDescStub = sandbox.stub(element.$.restAPI, 'setDescription') + test('_computePatchSetCommentsString', () => { + // Test string with unresolved comments. + comments = { + foo: 'foo comments', + bar: 'bar comments', + xyz: 'xyz comments', + }; + sandbox.stub(element.$.fileList, 'getCommentsForPath', (c, p, f) => { + if (f == 'foo') { + return ['comment1', 'comment2']; + } else if (f == 'bar') { + return ['comment1']; + } else { + return []; + } + }); + sandbox.stub(element.$.fileList, 'computeUnresolvedNum', (c, d, p, f) => { + if (f == 'foo') { + return 0; + } else if (f == 'bar') { + return 1; + } else { + return 0; + } + }); + assert.equal(element._computePatchSetCommentsString(comments, 1), + '(3 comments, 1 unresolved)'); + + // Test string with no unresolved comments. + delete comments['bar']; + assert.equal(element._computePatchSetCommentsString(comments, 1), + '(2 comments)'); + + // Test string with no comments. + delete comments['foo']; + assert.equal(element._computePatchSetCommentsString(comments, 1), ''); + }); + + test('_handleDescriptionChanged', () => { + const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription') .returns(Promise.resolve({ok: true})); sandbox.stub(element, '_computeDescriptionReadOnly'); @@ -206,7 +309,7 @@ element._loggedIn = true; flushAsynchronousOperations(); - var label = element.$.descriptionLabel; + const label = element.$.descriptionLabel; assert.equal(label.value, 'test'); label.editing = true; label._inputText = 'test2'; @@ -216,19 +319,19 @@ assert.equal(putDescStub.args[0][2], 'test2'); }); - test('_updateRebaseAction', function() { - var currentRevisionActions = { + test('_updateRebaseAction', () => { + const currentRevisionActions = { cherrypick: { enabled: true, label: 'Cherry Pick', method: 'POST', - title: 'cherrypick' + title: 'cherrypick', }, rebase: { enabled: true, label: 'Rebase', method: 'POST', - title: 'Rebase onto tip of branch or parent change' + title: 'Rebase onto tip of branch or parent change', }, }; @@ -236,7 +339,7 @@ // When rebase is enabled initially, rebaseOnCurrent should be set to // true. assert.equal(element._updateRebaseAction(currentRevisionActions), - currentRevisionActions); + currentRevisionActions); assert.isTrue(currentRevisionActions.rebase.enabled); assert.isTrue(currentRevisionActions.rebase.rebaseOnCurrent); @@ -246,14 +349,14 @@ // When rebase is not enabled initially, rebaseOnCurrent should be set to // false. assert.equal(element._updateRebaseAction(currentRevisionActions), - currentRevisionActions); + currentRevisionActions); assert.isTrue(currentRevisionActions.rebase.enabled); assert.isFalse(currentRevisionActions.rebase.rebaseOnCurrent); }); - test('_reload is called when an approved label is removed', function() { - var vote = {_account_id: 1, name: 'bojack', value: 1}; + test('_reload is called when an approved label is removed', () => { + const vote = {_account_id: 1, name: 'bojack', value: 1}; element._changeNum = '42'; element._patchRange = { basePatchNum: 'PARENT', @@ -279,7 +382,7 @@ }, }; flushAsynchronousOperations(); - var reloadStub = sandbox.stub(element, '_reload'); + const reloadStub = sandbox.stub(element, '_reload'); element.splice('_change.labels.test.all', 0, 1); assert.isFalse(reloadStub.called); element._change.labels.test.all.push(vote); @@ -291,30 +394,37 @@ assert.isTrue(reloadStub.calledOnce); }); - test('reply button has updated count when there are drafts', function() { - var replyButton = element.$$('gr-button.reply'); - assert.ok(replyButton); - assert.equal(replyButton.textContent, 'Reply'); + test('reply button has updated count when there are drafts', () => { + const getLabel = element._computeReplyButtonLabel; - element._diffDrafts = null; - assert.equal(replyButton.textContent, 'Reply'); + assert.equal(getLabel(null, false), 'Reply'); + assert.equal(getLabel(null, true), 'Start review'); - element._diffDrafts = {}; - assert.equal(replyButton.textContent, 'Reply'); + const changeRecord = {base: null}; + assert.equal(getLabel(changeRecord, false), 'Reply'); - element._diffDrafts = { + changeRecord.base = {}; + assert.equal(getLabel(changeRecord, false), 'Reply'); + + changeRecord.base = { 'file1.txt': [{}], 'file2.txt': [{}, {}], }; - assert.equal(replyButton.textContent, 'Reply (3)'); + assert.equal(getLabel(changeRecord, false), 'Reply (3)'); }); - test('comment events properly update diff drafts', function() { + test('start review button when owner of WIP change', () => { + assert.equal( + element._computeReplyButtonLabel(null, true), + 'Start review'); + }); + + test('comment events properly update diff drafts', () => { element._patchRange = { basePatchNum: 'PARENT', patchNum: 2, }; - var draft = { + const draft = { __draft: true, id: 'id1', path: '/foo/bar.txt', @@ -328,7 +438,7 @@ element._handleCommentSave({target: {comment: draft}}); draft.patch_set = 2; assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]}); - var draft2 = { + const draft2 = { __draft: true, id: 'id2', path: '/foo/bar.txt', @@ -345,7 +455,7 @@ assert.deepEqual(element._diffDrafts, {}); }); - test('change num change', function() { + test('change num change', () => { element._changeNum = null; element._patchRange = { basePatchNum: 'PARENT', @@ -357,8 +467,12 @@ }; element.viewState.changeNum = null; element.viewState.diffMode = 'UNIFIED'; + assert.equal(element.viewState.numFilesShown, 200); + assert.equal(element._numFilesShown, 200); + element._numFilesShown = 150; flushAsynchronousOperations(); assert.equal(element.viewState.diffMode, 'UNIFIED'); + assert.equal(element.viewState.numFilesShown, 150); element._changeNum = '1'; element.params = {changeNum: '1'}; @@ -373,9 +487,11 @@ flushAsynchronousOperations(); assert.isNull(element.viewState.diffMode); assert.equal(element.viewState.changeNum, '2'); + assert.equal(element.viewState.numFilesShown, 200); + assert.equal(element._numFilesShown, 200); }); - test('patch num change', function(done) { + test('patch num change', done => { element._changeNum = '42'; element._patchRange = { basePatchNum: 'PARENT', @@ -396,19 +512,19 @@ element.viewState.diffMode = 'UNIFIED'; flushAsynchronousOperations(); - var selectEl = element.$$('.patchInfo-header select'); + const selectEl = element.$$('.patchInfo-header select'); assert.ok(selectEl); - var optionEls = Polymer.dom(element.root).querySelectorAll( + const optionEls = Polymer.dom(element.root).querySelectorAll( '.patchInfo-header option'); assert.equal(optionEls.length, 4); - var select = element.$$('.patchInfo-header #patchSetSelect').bindValue; + const select = element.$$('.patchInfo-header #patchSetSelect').bindValue; assert.notEqual(select, 1); assert.equal(select, 2); assert.notEqual(select, 3); assert.equal(optionEls[3].value, 13); - var numEvents = 0; - selectEl.addEventListener('change', function(e) { + let numEvents = 0; + selectEl.addEventListener('change', e => { assert.equal(element.viewState.diffMode, 'UNIFIED'); numEvents++; if (numEvents == 1) { @@ -426,7 +542,7 @@ element.fire('change', {}, {node: selectEl}); }); - test('patch num change with missing current_revision', function(done) { + test('patch num change with missing current_revision', done => { element._changeNum = '42'; element._patchRange = { basePatchNum: 'PARENT', @@ -444,21 +560,21 @@ labels: {}, }; flushAsynchronousOperations(); - var selectEl = element.$$('.patchInfo-header select'); + const selectEl = element.$$('.patchInfo-header select'); assert.ok(selectEl); - var optionEls = Polymer.dom(element.root).querySelectorAll( + const optionEls = Polymer.dom(element.root).querySelectorAll( '.patchInfo-header option'); assert.equal(optionEls.length, 4); assert.notEqual( - element.$$('.patchInfo-header #patchSetSelect').bindValue, 1); + element.$$('.patchInfo-header #patchSetSelect').bindValue, 1); assert.equal( - element.$$('.patchInfo-header #patchSetSelect').bindValue, 2); + element.$$('.patchInfo-header #patchSetSelect').bindValue, 2); assert.notEqual( - element.$$('.patchInfo-header #patchSetSelect').bindValue, 3); + element.$$('.patchInfo-header #patchSetSelect').bindValue, 3); assert.equal(optionEls[3].value, 13); - var numEvents = 0; - selectEl.addEventListener('change', function(e) { + let numEvents = 0; + selectEl.addEventListener('change', e => { numEvents++; if (numEvents == 1) { assert(showStub.lastCall.calledWithExactly('/c/42/1'), @@ -475,15 +591,15 @@ element.fire('change', {}, {node: selectEl}); }); - test('don’t reload entire page when patchRange changes', function() { - var reloadStub = sandbox.stub(element, '_reload', - function() { return Promise.resolve(); }); - var reloadPatchDependentStub = sandbox.stub(element, + test('don’t reload entire page when patchRange changes', () => { + const reloadStub = sandbox.stub(element, '_reload', + () => { return Promise.resolve(); }); + const reloadPatchDependentStub = sandbox.stub(element, '_reloadPatchNumDependentResources', - function() { return Promise.resolve(); }); - var relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear'); + () => { return Promise.resolve(); }); + const relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear'); - var value = { + const value = { view: 'gr-change-view', patchNum: '1', }; @@ -501,11 +617,11 @@ assert.isTrue(relatedClearSpy.calledOnce); }); - test('reload entire page when patchRange doesnt change', function() { - var reloadStub = sandbox.stub(element, '_reload', - function() { return Promise.resolve(); }); + test('reload entire page when patchRange doesnt change', () => { + const reloadStub = sandbox.stub(element, '_reload', + () => { return Promise.resolve(); }); - var value = { + const value = { view: 'gr-change-view', }; element._paramsChanged(value); @@ -515,7 +631,7 @@ assert.isTrue(reloadStub.calledTwice); }); - test('include base patch when not parent', function() { + test('include base patch when not parent', () => { element._changeNum = '42'; element._patchRange = { basePatchNum: '2', @@ -543,33 +659,32 @@ }); test('related changes are updated and new patch selected after rebase', - function(done) { - element._changeNum = '42'; - sandbox.stub(element, '_computeLatestPatchNum', function() { - return 1; - }); - sandbox.stub(element, '_reload', - function() { return Promise.resolve(); }); - var e = {detail: {action: 'rebase'}}; - element._handleReloadChange(e).then(function() { - assert.isTrue(showStub.lastCall.calledWithExactly('/c/42')); - done(); - }); - }); + done => { + element._changeNum = '42'; + sandbox.stub(element, 'computeLatestPatchNum', () => { + return 1; + }); + sandbox.stub(element, '_reload', + () => { return Promise.resolve(); }); + const e = {detail: {action: 'rebase'}}; + element._handleReloadChange(e).then(() => { + assert.isTrue(showStub.lastCall.calledWithExactly('/c/42')); + done(); + }); + }); - test('related changes are not updated after other action', function(done) { - sandbox.stub(element, '_reload', - function() { return Promise.resolve(); }); + test('related changes are not updated after other action', done => { + sandbox.stub(element, '_reload', () => { return Promise.resolve(); }); sandbox.stub(element, '_updateSelected'); sandbox.stub(element.$.relatedChanges, 'reload'); - var e = {detail: {action: 'abandon'}}; - element._handleReloadChange(e).then(function() { + const e = {detail: {action: 'abandon'}}; + element._handleReloadChange(e).then(() => { assert.isFalse(showStub.called); done(); }); }); - test('change status new', function() { + test('change status new', () => { element._changeNum = '1'; element._patchRange = { basePatchNum: 'PARENT', @@ -584,11 +699,11 @@ status: 'NEW', labels: {}, }; - var status = element._computeChangeStatus(element._change, '1'); + const status = element._computeChangeStatus(element._change, '1'); assert.equal(status, ''); }); - test('change status draft', function() { + test('change status draft', () => { element._changeNum = '1'; element._patchRange = { basePatchNum: 'PARENT', @@ -603,11 +718,11 @@ status: 'DRAFT', labels: {}, }; - var status = element._computeChangeStatus(element._change, '1'); + const status = element._computeChangeStatus(element._change, '1'); assert.equal(status, 'Draft'); }); - test('change status conflict', function() { + test('change status conflict', () => { element._changeNum = '1'; element._patchRange = { basePatchNum: 'PARENT', @@ -623,11 +738,51 @@ status: 'NEW', labels: {}, }; - var status = element._computeChangeStatus(element._change, '1'); + const status = element._computeChangeStatus(element._change, '1'); assert.equal(status, 'Merge Conflict'); }); - test('change status merged', function() { + test('change status private', () => { + element._changeNum = '1'; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 1, + }; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev1: {_number: 1}, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + is_private: true, + }; + const status = element._privateChanges(element._change); + assert.equal(status, ' (Private)'); + }); + + test('change status without private', () => { + element._changeNum = '1'; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 1, + }; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev1: {_number: 1}, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + is_private: false, + }; + const status = element._privateChanges(element._change); + assert.equal(status, ''); + }); + + test('change status merged', () => { element._changeNum = '1'; element._patchRange = { basePatchNum: 'PARENT', @@ -642,11 +797,11 @@ status: element.ChangeStatus.MERGED, labels: {}, }; - var status = element._computeChangeStatus(element._change, '1'); + const status = element._computeChangeStatus(element._change, '1'); assert.equal(status, 'Merged'); }); - test('revision status draft', function() { + test('revision status draft', () => { element._changeNum = '1'; element._patchRange = { basePatchNum: 'PARENT', @@ -665,12 +820,12 @@ status: 'NEW', labels: {}, }; - var status = element._computeChangeStatus(element._change, '2'); + const status = element._computeChangeStatus(element._change, '2'); assert.equal(status, 'Draft'); }); - test('_computeMergedCommitInfo', function() { - var dummyRevs = { + test('_computeMergedCommitInfo', () => { + const dummyRevs = { 1: {commit: {commit: 1}}, 2: {commit: {}}, }; @@ -679,13 +834,13 @@ dummyRevs[1].commit); // Regression test for issue 5337. - var commit = element._computeMergedCommitInfo(2, dummyRevs); + const commit = element._computeMergedCommitInfo(2, dummyRevs); assert.notDeepEqual(commit, dummyRevs[2]); assert.deepEqual(commit, {commit: 2}); }); - test('get latest revision', function() { - var change = { + test('get latest revision', () => { + let change = { revisions: { rev1: {_number: 1}, rev3: {_number: 3}, @@ -701,8 +856,8 @@ assert.equal(element._getLatestRevisionSHA(change), 'rev1'); }); - test('show commit message edit button', function() { - var _change = { + test('show commit message edit button', () => { + const _change = { status: element.ChangeStatus.MERGED, }; assert.isTrue(element._computeHideEditCommitMessage(false, false, {})); @@ -713,80 +868,80 @@ _change)); }); - test('_computeChangeIdCommitMessageError', function() { - var commitMessage = + test('_computeChangeIdCommitMessageError', () => { + let commitMessage = 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483'; - var change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; + let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - null); + element._computeChangeIdCommitMessageError(commitMessage, change), + null); change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - 'mismatch'); + element._computeChangeIdCommitMessageError(commitMessage, change), + 'mismatch'); commitMessage = 'This is the greatest change.'; assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - 'missing'); + element._computeChangeIdCommitMessageError(commitMessage, change), + 'missing'); }); - test('multiple change Ids in commit message picks last', function() { - var commitMessage = [ - 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484', - 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483', + test('multiple change Ids in commit message picks last', () => { + const commitMessage = [ + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484', + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483', ].join('\n'); - var change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; + let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - null); + element._computeChangeIdCommitMessageError(commitMessage, change), + null); change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - 'mismatch'); + element._computeChangeIdCommitMessageError(commitMessage, change), + 'mismatch'); }); - test('does not count change Id that starts mid line', function() { - var commitMessage = [ - 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484', - 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483', + test('does not count change Id that starts mid line', () => { + const commitMessage = [ + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484', + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483', ].join(' and '); - var change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; + let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - null); + element._computeChangeIdCommitMessageError(commitMessage, change), + null); change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - 'mismatch'); + element._computeChangeIdCommitMessageError(commitMessage, change), + 'mismatch'); }); - test('_computeTitleAttributeWarning', function() { - var changeIdCommitMessageError = 'missing'; + test('_computeTitleAttributeWarning', () => { + let changeIdCommitMessageError = 'missing'; assert.equal( element._computeTitleAttributeWarning(changeIdCommitMessageError), 'No Change-Id in commit message'); - var changeIdCommitMessageError = 'mismatch'; + changeIdCommitMessageError = 'mismatch'; assert.equal( element._computeTitleAttributeWarning(changeIdCommitMessageError), 'Change-Id mismatch'); }); - test('_computeChangeIdClass', function() { - var changeIdCommitMessageError = 'missing'; + test('_computeChangeIdClass', () => { + let changeIdCommitMessageError = 'missing'; assert.equal( - element._computeChangeIdClass(changeIdCommitMessageError), ''); + element._computeChangeIdClass(changeIdCommitMessageError), ''); - var changeIdCommitMessageError = 'mismatch'; + changeIdCommitMessageError = 'mismatch'; assert.equal( - element._computeChangeIdClass(changeIdCommitMessageError), 'warning'); + element._computeChangeIdClass(changeIdCommitMessageError), 'warning'); }); - test('topic is coalesced to null', function(done) { + test('topic is coalesced to null', done => { sandbox.stub(element, '_changeChanged'); - sandbox.stub(element.$.restAPI, 'getChangeDetail', function() { + sandbox.stub(element.$.restAPI, 'getChangeDetail', () => { return Promise.resolve({ id: '123456789', labels: {}, @@ -795,15 +950,15 @@ }); }); - element._getChangeDetail().then(function() { + element._getChangeDetail().then(() => { assert.isNull(element._change.topic); done(); }); }); - test('commit sha is populated from getChangeDetail', function(done) { + test('commit sha is populated from getChangeDetail', done => { sandbox.stub(element, '_changeChanged'); - sandbox.stub(element.$.restAPI, 'getChangeDetail', function() { + sandbox.stub(element.$.restAPI, 'getChangeDetail', () => { return Promise.resolve({ id: '123456789', labels: {}, @@ -812,17 +967,17 @@ }); }); - element._getChangeDetail().then(function() { + element._getChangeDetail().then(() => { assert.equal('foo', element._commitInfo.commit); done(); }); }); - test('reply dialog focus can be controlled', function() { - var FocusTarget = element.$.replyDialog.FocusTarget; - var openStub = sandbox.stub(element, '_openReplyDialog'); + test('reply dialog focus can be controlled', () => { + const FocusTarget = element.$.replyDialog.FocusTarget; + const openStub = sandbox.stub(element, '_openReplyDialog'); - var e = {detail: {}}; + const e = {detail: {}}; element._handleShowReplyDialog(e); assert(openStub.lastCall.calledWithExactly(FocusTarget.REVIEWERS), '_openReplyDialog should have been passed REVIEWERS'); @@ -833,8 +988,8 @@ '_openReplyDialog should have been passed CCS'); }); - test('class is applied to file list on old patch set', function() { - var allPatchSets = [{num: 1}, {num: 2}, {num: 4}]; + test('class is applied to file list on old patch set', () => { + const allPatchSets = [{num: 1}, {num: 2}, {num: 4}]; assert.equal(element._computePatchInfoClass('1', allPatchSets), 'patchInfo--oldPatchSet'); assert.equal(element._computePatchInfoClass('2', allPatchSets), @@ -842,8 +997,8 @@ assert.equal(element._computePatchInfoClass('4', allPatchSets), ''); }); - test('getUrlParameter functionality', function() { - var locationStub = sandbox.stub(element, '_getLocationSearch'); + test('getUrlParameter functionality', () => { + const locationStub = sandbox.stub(element, '_getLocationSearch'); locationStub.returns('?test'); assert.equal(element._getUrlParameter('test'), 'test'); @@ -855,14 +1010,13 @@ assert.isNull(element._getUrlParameter('test')); locationStub.returns('?test2'); assert.isNull(element._getUrlParameter('test')); - }); - test('revert dialog opened with revert param', function(done) { - sandbox.stub(element.$.restAPI, 'getLoggedIn', function() { + test('revert dialog opened with revert param', done => { + sandbox.stub(element.$.restAPI, 'getLoggedIn', () => { return Promise.resolve(true); }); - sandbox.stub(Gerrit, 'awaitPluginsLoaded', function() { + sandbox.stub(Gerrit, 'awaitPluginsLoaded', () => { return Promise.resolve(); }); @@ -881,95 +1035,94 @@ actions: {}, }; - var urlParamStub = sandbox.stub(element, '_getUrlParameter', - function(param) { + sandbox.stub(element, '_getUrlParameter', + param => { assert.equal(param, 'revert'); return param; }); - var revertDialogStub = sandbox.stub(element.$.actions, 'showRevertDialog', + sandbox.stub(element.$.actions, 'showRevertDialog', done); element._maybeShowRevertDialog(); assert.isTrue(Gerrit.awaitPluginsLoaded.called); }); - suite('scroll related tests', function() { - test('document scrolling calls function to set scroll height', - function(done) { - var originalHeight = document.body.scrollHeight; - var scrollStub = sandbox.stub(element, '_handleScroll', - function() { - assert.isTrue(scrollStub.called); - document.body.style.height = - originalHeight + 'px'; - scrollStub.restore(); - done(); - }); - document.body.style.height = '10000px'; - document.body.scrollTop = TEST_SCROLL_TOP_PX; - element._handleScroll(); - }); - - test('history is loaded correctly', function() { - history.replaceState( - { - scrollTop: 100, - path: location.pathname, - }, - location.pathname); - - var reloadStub = sandbox.stub(element, '_reload', - function() { - // When element is reloaded, ensure that the history - // state has the scrollTop set earlier. This will then - // be reset. - assert.isTrue(history.state.scrollTop == 100); - return Promise.resolve({}); + suite('scroll related tests', () => { + test('document scrolling calls function to set scroll height', done => { + const originalHeight = document.body.scrollHeight; + const scrollStub = sandbox.stub(element, '_handleScroll', + () => { + assert.isTrue(scrollStub.called); + document.body.style.height = originalHeight + 'px'; + scrollStub.restore(); + done(); }); + document.body.style.height = '10000px'; + element._handleScroll(); + }); + + test('scrollTop is set correctly', () => { + element.viewState = {scrollTop: TEST_SCROLL_TOP_PX}; + + sandbox.stub(element, '_reload', () => { + // When element is reloaded, ensure that the history + // state has the scrollTop set earlier. This will then + // be reset. + assert.isTrue(element.viewState.scrollTop == TEST_SCROLL_TOP_PX); + return Promise.resolve({}); + }); // simulate reloading component, which is done when route // changes to match a regex of change view type. element._paramsChanged({view: 'gr-change-view'}); }); + + test('scrollTop is reset when new change is loaded', () => { + element._resetFileListViewState(); + assert.equal(element.viewState.scrollTop, 0); + }); }); - suite('reply dialog tests', function() { - setup(function() { + suite('reply dialog tests', () => { + setup(() => { sandbox.stub(element.$.replyDialog, '_draftChanged'); + sandbox.stub(element.$.replyDialog, 'fetchIsLatestKnown', + () => { return Promise.resolve(true); }); + element._change = {labels: {}}; }); - test('reply from comment adds quote text', function() { - var e = {detail: {message: {message: 'quote text'}}}; + test('reply from comment adds quote text', () => { + const e = {detail: {message: {message: 'quote text'}}}; element._handleMessageReply(e); assert.equal(element.$.replyDialog.draft, '> quote text\n\n'); assert.equal(element.$.replyDialog.quote, '> quote text\n\n'); }); - test('reply from comment replaces quote text', function() { + test('reply from comment replaces quote text', () => { element.$.replyDialog.draft = '> old quote text\n\n some draft text'; element.$.replyDialog.quote = '> old quote text\n\n'; - var e = {detail: {message: {message: 'quote text'}}}; + const e = {detail: {message: {message: 'quote text'}}}; element._handleMessageReply(e); assert.equal(element.$.replyDialog.draft, '> quote text\n\n'); assert.equal(element.$.replyDialog.quote, '> quote text\n\n'); }); - test('reply from same comment preserves quote text', function() { + test('reply from same comment preserves quote text', () => { element.$.replyDialog.draft = '> quote text\n\n some draft text'; element.$.replyDialog.quote = '> quote text\n\n'; - var e = {detail: {message: {message: 'quote text'}}}; + const e = {detail: {message: {message: 'quote text'}}}; element._handleMessageReply(e); assert.equal(element.$.replyDialog.draft, '> quote text\n\n some draft text'); assert.equal(element.$.replyDialog.quote, '> quote text\n\n'); }); - test('reply from top of page contains previous draft', function() { - var div = document.createElement('div'); + test('reply from top of page contains previous draft', () => { + const div = document.createElement('div'); element.$.replyDialog.draft = '> quote text\n\n some draft text'; element.$.replyDialog.quote = '> quote text\n\n'; - var e = {target: div, preventDefault: sandbox.spy()}; + const e = {target: div, preventDefault: sandbox.spy()}; element._handleReplyTap(e); assert.equal(element.$.replyDialog.draft, '> quote text\n\n some draft text'); @@ -977,24 +1130,29 @@ }); }); - test('reply button is disabled until server config is loaded', function() { + test('reply button is disabled until server config is loaded', () => { assert.isTrue(element._replyDisabled); - element.serverConfig = {}; + element._serverConfig = {}; assert.isFalse(element._replyDisabled); }); - suite('commit message expand/collapse', function() { - test('commitCollapseToggle hidden for short commit message', function() { + suite('commit message expand/collapse', () => { + setup(() => { + sandbox.stub(element, 'fetchIsLatestKnown', + () => { return Promise.resolve(false); }); + }); + + test('commitCollapseToggle hidden for short commit message', () => { element._latestCommitMessage = ''; assert.isTrue(element.$.commitCollapseToggle.hasAttribute('hidden')); }); - test('commitCollapseToggle shown for long commit message', function() { + test('commitCollapseToggle shown for long commit message', () => { element._latestCommitMessage = _.times(31, String).join('\n'); assert.isFalse(element.$.commitCollapseToggle.hasAttribute('hidden')); }); - test('commitCollapseToggle functions', function() { + test('commitCollapseToggle functions', () => { element._latestCommitMessage = _.times(31, String).join('\n'); assert.isTrue(element._commitCollapsed); assert.isTrue( @@ -1006,51 +1164,43 @@ }); }); - suite('related changes expand/collapse', function() { - var updateHeightSpy; - setup(function() { + suite('related changes expand/collapse', () => { + let updateHeightSpy; + setup(() => { updateHeightSpy = sandbox.spy(element, '_updateRelatedChangeMaxHeight'); }); test('relatedChangesToggle shown height greater than changeInfo height', - function() { - assert.isTrue(element.$.relatedChangesToggle.hasAttribute('hidden')); - - sandbox.stub(element, '_getOffsetHeight', function() { - return 50; - }); - - sandbox.stub(element, '_getScrollHeight', function() { - return 60; - }); - element._relatedChangesLoading = false; - assert.isFalse(element.$.relatedChangesToggle.hasAttribute('hidden')); - assert.equal(updateHeightSpy.callCount, 1); - }); + () => { + assert.isFalse(element.$.relatedChangesToggle.classList + .contains('showToggle')); + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(element, '_getScrollHeight', () => 60); + sandbox.stub(element, '_getLineHeight', () => 5); + sandbox.stub(window, 'matchMedia', () => ({matches: true})); + element._relatedChangesLoading = false; + assert.isTrue(element.$.relatedChangesToggle.classList + .contains('showToggle')); + assert.equal(updateHeightSpy.callCount, 1); + }); test('relatedChangesToggle hidden height less than changeInfo height', - function() { - assert.isTrue(element.$.relatedChangesToggle.hasAttribute('hidden')); - sandbox.stub(element, '_getOffsetHeight', function() { - return 50; - }); + () => { + assert.isFalse(element.$.relatedChangesToggle.classList + .contains('showToggle')); + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(element, '_getScrollHeight', () => 40); + sandbox.stub(element, '_getLineHeight', () => 5); + sandbox.stub(window, 'matchMedia', () => ({matches: true})); + element._relatedChangesLoading = false; + assert.isFalse(element.$.relatedChangesToggle.classList + .contains('showToggle')); + assert.equal(updateHeightSpy.callCount, 1); + }); - sandbox.stub(element, '_getScrollHeight', function() { - return 40; - }); - element._relatedChangesLoading = false; - assert.isTrue(element.$.relatedChangesToggle.hasAttribute('hidden')); - assert.equal(updateHeightSpy.callCount, 1); - }); - - test('relatedChangesToggle functions', function() { - sandbox.stub(element, '_getOffsetHeight', function() { - return 50; - }); - - sandbox.stub(element, '_getScrollHeight', function() { - return 60; - }); + test('relatedChangesToggle functions', () => { + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(window, 'matchMedia', () => ({matches: false})); element._relatedChangesLoading = false; assert.isTrue(element._relatedChangesCollapsed); assert.isTrue( @@ -1061,35 +1211,27 @@ element.$.relatedChanges.classList.contains('collapsed')); }); - test('_updateRelatedChangeMaxHeight without commit toggle', function() { - sandbox.stub(element, '_getOffsetHeight', function() { - return 50; - }); + test('_updateRelatedChangeMaxHeight without commit toggle', () => { + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(element, '_getLineHeight', () => 12); + sandbox.stub(window, 'matchMedia', () => ({matches: false})); - sandbox.stub(element, '_getLineHeight', function() { - return 12; - }); - - // 50 (existing height) - 24 (extra height) = 26 (adjusted height). - // 50 (existing height) % 12 (line height) = 2 (remainder). - // 26 (adjusted height) - 2 (remainder) = 24 (max height to set). + // 50 (existing height) - 30 (extra height) = 20 (adjusted height). + // 20 (max existing height) % 12 (line height) = 6 (remainder). + // 20 (adjusted height) - 8 (remainder) = 12 (max height to set). element._updateRelatedChangeMaxHeight(); assert.equal(element.customStyle['--relation-chain-max-height'], - '24px'); + '12px'); assert.equal(element.customStyle['--related-change-btn-top-padding'], undefined); }); - test('_updateRelatedChangeMaxHeight with commit toggle', function() { + test('_updateRelatedChangeMaxHeight with commit toggle', () => { element._latestCommitMessage = _.times(31, String).join('\n'); - sandbox.stub(element, '_getOffsetHeight', function() { - return 50; - }); - - sandbox.stub(element, '_getLineHeight', function() { - return 12; - }); + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(element, '_getLineHeight', () => 12); + sandbox.stub(window, 'matchMedia', () => ({matches: false})); // 50 (existing height) % 12 (line height) = 2 (remainder). // 50 (existing height) - 2 (remainder) = 48 (max height to set). @@ -1100,6 +1242,122 @@ assert.equal(element.customStyle['--related-change-btn-top-padding'], '2px'); }); + + test('_updateRelatedChangeMaxHeight in small screen mode', () => { + element._latestCommitMessage = _.times(31, String).join('\n'); + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(element, '_getLineHeight', () => 12); + sandbox.stub(window, 'matchMedia', () => ({matches: true})); + + element._updateRelatedChangeMaxHeight(); + + // 400 (new height) % 12 (line height) = 4 (remainder). + // 400 (new height) - 4 (remainder) = 396. + + assert.equal(element.customStyle['--relation-chain-max-height'], + '396px'); + }); + + test('_updateRelatedChangeMaxHeight in medium screen mode', () => { + element._latestCommitMessage = _.times(31, String).join('\n'); + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(element, '_getLineHeight', () => 12); + sandbox.stub(window, 'matchMedia', () => { + if (window.matchMedia.lastCall.args[0] === '(max-width: 60em)') { + return {matches: true}; + } else { + return {matches: false}; + } + }); + + // 100 (new height) % 12 (line height) = 4 (remainder). + // 100 (new height) - 4 (remainder) = 96. + element._updateRelatedChangeMaxHeight(); + assert.equal(element.customStyle['--relation-chain-max-height'], + '96px'); + }); + + + suite('update checks', () => { + setup(() => { + sandbox.spy(element, '_startUpdateCheckTimer'); + sandbox.stub(element, 'async', f => { + // Only fire the async callback one time. + if (element.async.callCount > 1) { return; } + f.call(element); + }); + }); + + test('_startUpdateCheckTimer negative delay', () => { + sandbox.stub(element, 'fetchIsLatestKnown'); + + element._serverConfig = {change: {update_delay: -1}}; + + assert.isTrue(element._startUpdateCheckTimer.called); + assert.isFalse(element.fetchIsLatestKnown.called); + }); + + test('_startUpdateCheckTimer up-to-date', () => { + sandbox.stub(element, 'fetchIsLatestKnown', + () => { return Promise.resolve(true); }); + + element._serverConfig = {change: {update_delay: 12345}}; + + assert.isTrue(element._startUpdateCheckTimer.called); + assert.isTrue(element.fetchIsLatestKnown.called); + assert.equal(element.async.lastCall.args[1], 12345 * 1000); + }); + + test('_startUpdateCheckTimer out-of-date shows an alert', done => { + sandbox.stub(element, 'fetchIsLatestKnown', + () => { return Promise.resolve(false); }); + element.addEventListener('show-alert', () => { + done(); + }); + element._serverConfig = {change: {update_delay: 12345}}; + }); + }); + + test('canStartReview computation', () => { + const account1 = {_account_id: 1}; + const account2 = {_account_id: 2}; + const change = { + owner: {_account_id: 1}, + }; + assert.isFalse(element._computeCanStartReview(true, change, account1)); + change.work_in_progress = false; + assert.isFalse(element._computeCanStartReview(true, change, account1)); + change.work_in_progress = true; + assert.isTrue(element._computeCanStartReview(true, change, account1)); + assert.isFalse(element._computeCanStartReview(false, change, account1)); + assert.isFalse(element._computeCanStartReview(true, change, account2)); + }); + + test('header class computation', () => { + assert.equal(element._computeHeaderClass({}), 'header'); + assert.equal(element._computeHeaderClass({work_in_progress: true}), + 'header wip'); + }); + }); + + test('_maybeScrollToMessage', () => { + const scrollStub = sandbox.stub(element.$.messageList, 'scrollToMessage'); + + element._maybeScrollToMessage(''); + assert.isFalse(scrollStub.called); + + element._maybeScrollToMessage('message'); + assert.isFalse(scrollStub.called); + + element._maybeScrollToMessage('#message-TEST'); + assert.isTrue(scrollStub.called); + assert.equal(scrollStub.lastCall.args[0], 'TEST'); + }); + + test('topic update reloads related changes', () => { + sandbox.stub(element.$.relatedChanges, 'reload'); + element.dispatchEvent(new CustomEvent('topic-changed')); + assert.isTrue(element.$.relatedChanges.reload.calledOnce); }); }); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html index a03f6cc..9094933 100644 --- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html +++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -16,12 +16,19 @@ <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html"> +<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<!-- + The custom CSS property `--gr-formatted-text-prose-max-width` controls the max + width of formatted text blocks that are not code. +--> <dom-module id="gr-comment-list"> <template> - <style> + <style include="shared-styles"> :host { display: block; word-wrap: break-word; @@ -43,7 +50,7 @@ } .message { flex: 1; - max-width: 80ch; + --gr-formatted-text-prose-max-width: 80ch; } </style> <template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js index 98a2508..d8cdb02 100644 --- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js +++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -14,8 +14,8 @@ (function() { 'use strict'; - var COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; - var MERGE_LIST_PATH = '/MERGE_LIST'; + const COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; + const MERGE_LIST_PATH = '/MERGE_LIST'; Polymer({ is: 'gr-comment-list', @@ -23,6 +23,7 @@ behaviors: [ Gerrit.BaseUrlBehavior, Gerrit.PathListBehavior, + Gerrit.URLEncodingBehavior, ], properties: { @@ -32,17 +33,17 @@ projectConfig: Object, }, - _computeFilesFromComments: function(comments) { - var arr = Object.keys(comments || {}); + _computeFilesFromComments(comments) { + const arr = Object.keys(comments || {}); return arr.sort(this.specialFilePathCompare); }, - _computeFileDiffURL: function(file, changeNum, patchNum) { - return this.getBaseUrl() + '/c/' + changeNum + - '/' + patchNum + '/' + file; + _computeFileDiffURL(file, changeNum, patchNum) { + return this.getBaseUrl() + '/c/' + changeNum + '/' + patchNum + '/' + + this.encodeURL(file); }, - _computeFileDisplayName: function(path) { + _computeFileDisplayName(path) { if (path === COMMIT_MESSAGE_PATH) { return 'Commit message'; } else if (path === MERGE_LIST_PATH) { @@ -51,12 +52,12 @@ return path; }, - _isOnParent: function(comment) { + _isOnParent(comment) { return comment.side === 'PARENT'; }, - _computeDiffLineURL: function(file, changeNum, patchNum, comment) { - var diffURL = this._computeFileDiffURL(file, changeNum, patchNum); + _computeDiffLineURL(file, changeNum, patchNum, comment) { + let diffURL = this._computeFileDiffURL(file, changeNum, patchNum); if (comment.line) { diffURL += '#'; if (this._isOnParent(comment)) { diffURL += 'b'; } @@ -65,18 +66,18 @@ return diffURL; }, - _computeCommentsForFile: function(comments, file) { + _computeCommentsForFile(comments, file) { // Changes are not picked up by the dom-repeat due to the array instance // identity not changing even when it has elements added/removed from it. return (comments[file] || []).slice(); }, - _computePatchDisplayName: function(comment) { + _computePatchDisplayName(comment) { if (this._isOnParent(comment)) { return 'Base, '; } if (comment.patch_set != this.patchNum) { - return 'PS' + comment.patch_set + ', '; + return `PS${comment.patch_set}, `; } return ''; },
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html index e27bad0..d25664e 100644 --- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -20,6 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-comment-list.html"> <script>void(0);</script> @@ -31,41 +32,41 @@ </test-fixture> <script> - suite('gr-comment-list tests', function() { - var element; + suite('gr-comment-list tests', () => { + let element; - setup(function() { + setup(() => { element = fixture('basic'); }); - test('_computeFilesFromComments w/ special file path sorting', function() { - var comments = { + test('_computeFilesFromComments w/ special file path sorting', () => { + const comments = { 'file_b.html': [], 'file_c.css': [], 'file_a.js': [], 'test.cc': [], 'test.h': [], }; - var expected = [ + const expected = [ 'file_a.js', 'file_b.html', 'file_c.css', 'test.h', - 'test.cc' + 'test.cc', ]; - var actual = element._computeFilesFromComments(comments); + const actual = element._computeFilesFromComments(comments); assert.deepEqual(actual, expected); assert.deepEqual(element._computeFilesFromComments(null), []); }); - test('_computeFileDiffURL', function() { - var expected = '/c/<change>/<patch>/<file>'; - var actual = element._computeFileDiffURL('<file>', '<change>', '<patch>'); + test('_computeFileDiffURL', () => { + const expected = '/c/change/patch/file'; + const actual = element._computeFileDiffURL('file', 'change', 'patch'); assert.equal(actual, expected); }); - test('_computeFileDisplayName', function() { + test('_computeFileDisplayName', () => { assert.equal(element._computeFileDisplayName('/COMMIT_MSG'), 'Commit message'); assert.equal(element._computeFileDisplayName('/MERGE_LIST'), @@ -74,23 +75,29 @@ '/foo/bar/baz'); }); - test('_computeDiffLineURL', function() { - var comment = {line: 123, side: 'REVISION', patch_set: 10}; - var expected = '/c/<change>/<patch>/<file>#123'; - var actual = element._computeDiffLineURL('<file>', '<change>', '<patch>', + test('_computeDiffLineURL', () => { + const comment = {line: 123, side: 'REVISION', patch_set: 10}; + let expected = '/c/change/patch/file#123'; + let actual = element._computeDiffLineURL('file', 'change', 'patch', comment); assert.equal(actual, expected); comment.line = 321; comment.side = 'PARENT'; - expected = '/c/<change>/<patch>/<file>#b321'; - actual = element._computeDiffLineURL('<file>', '<change>', '<patch>', - comment); + expected = '/c/change/patch/file#b321'; + actual = element._computeDiffLineURL('file', 'change', 'patch', comment); }); - test('_computePatchDisplayName', function() { - var comment = {line: 123, side: 'REVISION', patch_set: 10}; + test('_computeDiffLineURL encoding', () => { + const comment = {line: 123, side: 'REVISION', patch_set: 10}; + const expected = '/c/123/2/x%252By.md#123'; + const actual = element._computeDiffLineURL('x+y.md', '123', '2', comment); + assert.equal(actual, expected); + }); + + test('_computePatchDisplayName', () => { + const comment = {line: 123, side: 'REVISION', patch_set: 10}; element.patchNum = 10; assert.equal(element._computePatchDisplayName(comment), '');
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html index f1d02f2..dec4e118 100644 --- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html +++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
@@ -15,10 +15,11 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-commit-info"> <template> - <style> + <style include="shared-styles"> :host { display: inline-block; }
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js index 5aa8601..c55e8c7 100644 --- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js +++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -31,13 +31,13 @@ }, }, - _isWebLink: function(link) { + _isWebLink(link) { // This is a whitelist of web link types that provide direct links to // the commit in the url property. return link.name === 'gitiles' || link.name === 'gitweb'; }, - _computeShowWebLink: function(change, commitInfo, serverConfig) { + _computeShowWebLink(change, commitInfo, serverConfig) { if (serverConfig.gitweb && serverConfig.gitweb.url && serverConfig.gitweb.type && serverConfig.gitweb.type.revision) { return true; @@ -47,8 +47,8 @@ return false; } - for (var i = 0; i < commitInfo.web_links.length; i++) { - if (this._isWebLink(commitInfo.web_links[i])) { + for (const link of commitInfo.web_links) { + if (this._isWebLink(link)) { return true; } } @@ -56,7 +56,7 @@ return false; }, - _computeWebLink: function(change, commitInfo, serverConfig) { + _computeWebLink(change, commitInfo, serverConfig) { if (!this._computeShowWebLink(change, commitInfo, serverConfig)) { return; } @@ -69,10 +69,10 @@ .replace('${commit}', commitInfo.commit); } - var webLink = null; - for (var i = 0; i < commitInfo.web_links.length; i++) { - if (this._isWebLink(commitInfo.web_links[i])) { - webLink = commitInfo.web_links[i].url; + let webLink = null; + for (const link of commitInfo.web_links) { + if (this._isWebLink(link)) { + webLink = link.url; break; } } @@ -88,7 +88,7 @@ return webLink; }, - _computeShortHash: function(commitInfo) { + _computeShortHash(commitInfo) { if (!commitInfo || !commitInfo.commit) { return; }
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html index c8faff5..bd6fdcb 100644 --- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html +++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-commit-info.html"> <script>void(0);</script> @@ -33,14 +32,14 @@ </test-fixture> <script> - suite('gr-commit-info tests', function() { - var element; + suite('gr-commit-info tests', () => { + let element; - setup(function() { + setup(() => { element = fixture('basic'); }); - test('no web link when unavailable', function() { + test('no web link when unavailable', () => { element.commitInfo = {}; element.serverConfig = {}; element.change = {labels: []}; @@ -49,7 +48,7 @@ element.commitInfo, element.serverConfig)); }); - test('use web link when available', function() { + test('use web link when available', () => { element.commitInfo = {web_links: [{name: 'gitweb', url: 'link-url'}]}; element.serverConfig = {}; @@ -59,9 +58,9 @@ element.serverConfig), '../../link-url'); }); - test('does not relativize web links that begin with scheme', function() { + test('does not relativize web links that begin with scheme', () => { element.commitInfo = { - web_links: [{name: 'gitweb', url: 'https://link-url'}] + web_links: [{name: 'gitweb', url: 'https://link-url'}], }; element.serverConfig = {}; @@ -71,7 +70,7 @@ element.serverConfig), 'https://link-url'); }); - test('use gitweb when available', function() { + test('use gitweb when available', () => { element.commitInfo = {commit: 'commit-sha'}; element.serverConfig = {gitweb: { url: 'url-base/', @@ -80,7 +79,7 @@ element.change = { project: 'project-name', labels: [], - current_revision: element.commitInfo.commit + current_revision: element.commitInfo.commit, }; assert.isOk(element._computeShowWebLink(element.change, @@ -90,10 +89,10 @@ element.serverConfig), 'url-base/xx project-name xx commit-sha xx'); }); - test('prefer gitweb when both are available', function() { + test('prefer gitweb when both are available', () => { element.commitInfo = { commit: 'commit-sha', - web_links: [{url: 'link-url'}] + web_links: [{url: 'link-url'}], }; element.serverConfig = {gitweb: { url: 'url-base/', @@ -102,20 +101,20 @@ element.change = { project: 'project-name', labels: [], - current_revision: element.commitInfo.commit + current_revision: element.commitInfo.commit, }; assert.isOk(element._computeShowWebLink(element.change, element.commitInfo, element.serverConfig)); - var link = element._computeWebLink(element.change, element.commitInfo, + const link = element._computeWebLink(element.change, element.commitInfo, element.serverConfig); assert.equal(link, 'url-base/xx project-name xx commit-sha xx'); assert.notEqual(link, '../../link-url'); }); - test('ignore web links that are neither gitweb nor gitiles', function() { + test('ignore web links that are neither gitweb nor gitiles', () => { element.commitInfo = { commit: 'commit-sha', web_links: [ @@ -126,7 +125,7 @@ { name: 'gitiles', url: 'https://link-url', - } + }, ], }; element.serverConfig = {};
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html index 481b124..f528876 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html
@@ -17,10 +17,11 @@ <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-confirm-abandon-dialog"> <template> - <style> + <style include="shared-styles"> :host { display: block; }
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js index e47f14f..074e39e 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
@@ -33,16 +33,16 @@ message: String, }, - resetFocus: function() { + resetFocus() { this.$.messageInput.textarea.focus(); }, - _handleConfirmTap: function(e) { + _handleConfirmTap(e) { e.preventDefault(); this.fire('confirm', {reason: this.message}, {bubbles: false}); }, - _handleCancelTap: function(e) { + _handleCancelTap(e) { e.preventDefault(); this.fire('cancel', null, {bubbles: false}); },
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html index ebc6533..ba8e1f1 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html
@@ -17,10 +17,11 @@ <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-confirm-cherrypick-dialog"> <template> - <style> + <style include="shared-styles"> :host { display: block; width: 30em;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js index 716e29c..48a3f76 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -41,8 +41,8 @@ '_computeMessage(changeStatus, commitNum, commitMessage)', ], - _computeMessage: function(changeStatus, commitNum, commitMessage) { - var newMessage = commitMessage; + _computeMessage(changeStatus, commitNum, commitMessage) { + let newMessage = commitMessage; if (changeStatus === 'MERGED') { newMessage += '(cherry picked from commit ' + commitNum + ')'; @@ -50,12 +50,12 @@ this.message = newMessage; }, - _handleConfirmTap: function(e) { + _handleConfirmTap(e) { e.preventDefault(); this.fire('confirm', null, {bubbles: false}); }, - _handleCancelTap: function(e) { + _handleCancelTap(e) { e.preventDefault(); this.fire('cancel', null, {bubbles: false}); },
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html index 3c1cf2b..107fb23 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-confirm-cherrypick-dialog.html"> <script>void(0);</script> @@ -33,42 +32,41 @@ </test-fixture> <script> - suite('gr-confirm-cherrypick-dialog tests', function() { - var element; + suite('gr-confirm-cherrypick-dialog tests', () => { + let element; - setup(function() { + setup(() => { element = fixture('basic'); }); - test('with merged change', function() { + test('with merged change', () => { element.changeStatus = 'MERGED'; element.commitMessage = 'message\n'; element.commitNum = '123'; element.branch = 'master'; flushAsynchronousOperations(); - var expectedMessage = 'message\n(cherry picked from commit 123)'; + const expectedMessage = 'message\n(cherry picked from commit 123)'; assert.equal(element.message, expectedMessage); }); - test('with unmerged change', function() { + test('with unmerged change', () => { element.changeStatus = 'OPEN'; element.commitMessage = 'message\n'; element.commitNum = '123'; element.branch = 'master'; flushAsynchronousOperations(); - var expectedMessage = 'message\n'; + const expectedMessage = 'message\n'; assert.equal(element.message, expectedMessage); }); - test('with updated commit message', function() { + test('with updated commit message', () => { element.changeStatus = 'OPEN'; element.commitMessage = 'message\n'; element.commitNum = '123'; element.branch = 'master'; - var myNewMessage = 'updated commit message'; + const myNewMessage = 'updated commit message'; element.message = myNewMessage; flushAsynchronousOperations(); - var expectedMessage = 'message\n'; assert.equal(element.message, myNewMessage); }); });
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html index b27e6ba..2772594 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html
@@ -16,10 +16,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-confirm-rebase-dialog"> <template> - <style> + <style include="shared-styles"> :host { display: block; width: 30em; @@ -75,7 +76,8 @@ disabled$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]" on-tap="_handleRebaseOnTip"> <label id="rebaseOnTipLabel" for="rebaseOnTipInput"> - Rebase on top of the [[branch]] branch<span hidden="[[!hasParent]]"> + Rebase on top of the [[branch]] + branch<span hidden$="[[!hasParent]]"> (breaks relation chain) </span> </label> @@ -90,7 +92,7 @@ type="radio" on-tap="_handleRebaseOnOther"> <label id="rebaseOnOtherLabel" for="rebaseOnOtherInput"> - Rebase on a specific change or ref <span hidden="[[!hasParent]]"> + Rebase on a specific change or ref <span hidden$="[[!hasParent]]"> (breaks relation chain) </span> </label>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js index 4ecb31f..6cb7a9b 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -40,29 +40,29 @@ '_updateSelectedOption(rebaseOnCurrent, hasParent)', ], - _displayParentOption: function(rebaseOnCurrent, hasParent) { + _displayParentOption(rebaseOnCurrent, hasParent) { return hasParent && rebaseOnCurrent; }, - _displayParentUpToDateMsg: function(rebaseOnCurrent, hasParent) { + _displayParentUpToDateMsg(rebaseOnCurrent, hasParent) { return hasParent && !rebaseOnCurrent; }, - _displayTipOption: function(rebaseOnCurrent, hasParent) { + _displayTipOption(rebaseOnCurrent, hasParent) { return !(!rebaseOnCurrent && !hasParent); }, - _handleConfirmTap: function(e) { + _handleConfirmTap(e) { e.preventDefault(); this.fire('confirm', null, {bubbles: false}); }, - _handleCancelTap: function(e) { + _handleCancelTap(e) { e.preventDefault(); this.fire('cancel', null, {bubbles: false}); }, - _handleRebaseOnOther: function(e) { + _handleRebaseOnOther(e) { this.$.parentInput.focus(); }, @@ -73,15 +73,15 @@ * rebased on top of the target branch. Leaving out the base implies that it * should be rebased on top of its current parent. */ - _handleRebaseOnTip: function(e) { + _handleRebaseOnTip(e) { this.base = ''; }, - _handleRebaseOnParent: function(e) { + _handleRebaseOnParent(e) { this.base = null; }, - _handleEnterChangeNumberTap: function(e) { + _handleEnterChangeNumberTap(e) { this.$.rebaseOnOtherInput.checked = true; }, @@ -89,7 +89,7 @@ * Sets the default radio button based on the state of the app and * the corresponding value to be submitted. */ - _updateSelectedOption: function(rebaseOnCurrent, hasParent) { + _updateSelectedOption(rebaseOnCurrent, hasParent) { if (this._displayParentOption(rebaseOnCurrent, hasParent)) { this.$.rebaseOnParentInput.checked = true; this._handleRebaseOnParent();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html index 37eb812..0ea7c49 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-confirm-rebase-dialog.html"> <script>void(0);</script> @@ -33,14 +32,14 @@ </test-fixture> <script> - suite('gr-confirm-rebase-dialog tests', function() { - var element; + suite('gr-confirm-rebase-dialog tests', () => { + let element; - setup(function() { + setup(() => { element = fixture('basic'); }); - test('controls with parent and rebase on current available', function() { + test('controls with parent and rebase on current available', () => { element.rebaseOnCurrent = true; element.hasParent = true; flushAsynchronousOperations(); @@ -51,7 +50,7 @@ assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden')); }); - test('controls with parent rebase on current not available', function() { + test('controls with parent rebase on current not available', () => { element.rebaseOnCurrent = false; element.hasParent = true; flushAsynchronousOperations(); @@ -62,7 +61,7 @@ assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden')); }); - test('controls without parent and rebase on current available', function() { + test('controls without parent and rebase on current available', () => { element.rebaseOnCurrent = true; element.hasParent = false; flushAsynchronousOperations(); @@ -73,7 +72,7 @@ assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden')); }); - test('controls without parent rebase on current not available', function() { + test('controls without parent rebase on current not available', () => { element.rebaseOnCurrent = false; element.hasParent = false; flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html index a38811f8..94fd4ad 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.html
@@ -17,10 +17,11 @@ <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-confirm-revert-dialog"> <template> - <style> + <style include="shared-styles"> :host { display: block; }
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js index 8f621f0..4a1bbb8 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -33,27 +33,26 @@ message: String, }, - populateRevertMessage: function(message, commitHash) { + populateRevertMessage(message, commitHash) { // Figure out what the revert title should be. - var originalTitle = message.split('\n')[0]; - var revertTitle = 'Revert "' + originalTitle + '"'; + const originalTitle = message.split('\n')[0]; + const revertTitle = `Revert "${originalTitle}"`; if (!commitHash) { alert('Unable to find the commit hash of this change.'); return; } - var revertCommitText = 'This reverts commit ' + commitHash + '.'; + const revertCommitText = `This reverts commit ${commitHash}.`; - this.message = revertTitle + '\n\n' + - revertCommitText + '\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; + this.message = `${revertTitle}\n\n${revertCommitText}\n\n` + + `Reason for revert: <INSERT REASONING HERE>\n`; }, - _handleConfirmTap: function(e) { + _handleConfirmTap(e) { e.preventDefault(); this.fire('confirm', null, {bubbles: false}); }, - _handleCancelTap: function(e) { + _handleCancelTap(e) { e.preventDefault(); this.fire('cancel', null, {bubbles: false}); },
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html index d5c459b..09ba6bb 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-confirm-revert-dialog.html"> <script>void(0);</script> @@ -33,62 +32,62 @@ </test-fixture> <script> - suite('gr-confirm-revert-dialog tests', function() { - var element; + suite('gr-confirm-revert-dialog tests', () => { + let element; - setup(function() { + setup(() => { element = fixture('basic'); }); - test('no match', function() { + test('no match', () => { assert.isNotOk(element.message); - var alertStub = sinon.stub(window, 'alert'); + const alertStub = sinon.stub(window, 'alert'); element.populateRevertMessage('not a commitHash in sight', undefined); assert.isTrue(alertStub.calledOnce); alertStub.restore(); }); - test('single line', function() { + test('single line', () => { assert.isNotOk(element.message); element.populateRevertMessage( 'one line commit\n\nChange-Id: abcdefg\n', 'abcd123'); - var expected = 'Revert "one line commit"\n\n' + - 'This reverts commit abcd123.\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; + const expected = 'Revert "one line commit"\n\n' + + 'This reverts commit abcd123.\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; assert.equal(element.message, expected); }); - test('multi line', function() { + test('multi line', () => { assert.isNotOk(element.message); element.populateRevertMessage( 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n', 'abcd123'); - var expected = 'Revert "many lines"\n\n' + - 'This reverts commit abcd123.\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; + const expected = 'Revert "many lines"\n\n' + + 'This reverts commit abcd123.\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; assert.equal(element.message, expected); }); - test('issue above change id', function() { + test('issue above change id', () => { assert.isNotOk(element.message); element.populateRevertMessage( 'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n', 'abcd123'); - var expected = 'Revert "much lines"\n\n' + - 'This reverts commit abcd123.\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; + const expected = 'Revert "much lines"\n\n' + + 'This reverts commit abcd123.\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; assert.equal(element.message, expected); }); - test('revert a revert', function() { + test('revert a revert', () => { assert.isNotOk(element.message); element.populateRevertMessage( 'Revert "one line commit"\n\nChange-Id: abcdefg\n', 'abcd123'); - var expected = 'Revert "Revert "one line commit""\n\n' + - 'This reverts commit abcd123.\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; + const expected = 'Revert "Revert "one line commit""\n\n' + + 'This reverts commit abcd123.\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; assert.equal(element.message, expected); }); });
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html index 0e97d36..18e958f 100644 --- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -16,75 +16,27 @@ <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> -<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-download-dialog"> <template> - <style> + <style include="shared-styles"> :host { display: block; padding: 1em; } - ul { - list-style: none; - margin-bottom: .5em; - } - li { - display: inline-block; - margin: 0; - padding: 0; - } - li gr-button { - margin-right: 1em; - } - label, - input { - display: block; - } - label { - font-weight: bold; - } - input { - font-family: var(--monospace-font-family); - font-size: inherit; - } - li[selected] gr-button { - color: #000; - font-weight: bold; - text-decoration: none; - } header { display: flex; - justify-content: space-between; - } - main { - border-bottom: 1px solid #ddd; - border-top: 1px solid #ddd; - padding: .5em; } footer { display: flex; justify-content: space-between; padding-top: .75em; } - .command { - display: flex; - flex-wrap: wrap; - margin-bottom: .5em; - width: 60em; - } - .command label { - flex: 0 0 100%; - } - .copyCommand { - flex-grow: 1; - margin-right: .3em; - } .closeButtonContainer { display: flex; - flex: 1; + flex: 0; justify-content: flex-end; } .patchFiles { @@ -99,41 +51,26 @@ .archives a:last-of-type { margin-right: 0; } + .title { + text-align: center; + flex: 1; + } </style> <header> - <ul hidden$="[[!_schemes.length]]" hidden> - <template is="dom-repeat" items="[[_schemes]]" as="scheme"> - <li selected$="[[_computeSchemeSelected(scheme, _selectedScheme)]]"> - <gr-button link data-scheme$="[[scheme]]" on-tap="_handleSchemeTap"> - [[scheme]] - </gr-button> - </li> - </template> - </ul> + <span class="title"> + Patch set [[patchNum]] of [[_computePatchSetQuantity(change.revisions)]] + </span> <span class="closeButtonContainer"> <gr-button id="closeButton" link on-tap="_handleCloseTap">Close</gr-button> </span> </header> - <main hidden$="[[!_schemes.length]]" hidden> - <template is="dom-repeat" - items="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]" - as="command"> - <div class="command"> - <label>[[command.title]]</label> - <input is="iron-input" - class="copyCommand" - type="text" - bind-value="[[command.command]]" - on-tap="_handleInputTap" - readonly> - <gr-button class="copyToClipboard" on-tap="_copyToClipboard"> - copy - </gr-button> - </div> - </template> - </main> + <gr-download-commands + id="downloadCommands" + commands="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]" + schemes="[[_schemes]]" + selected-scheme="{{_selectedScheme}}"></gr-download-commands> <footer> <div class="patchFiles"> <label>Patch file</label> @@ -157,7 +94,6 @@ </div> </div> </footer> - <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template> <script src="gr-download-dialog.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js index 41f6792..01a24c5 100644 --- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -27,15 +27,10 @@ change: Object, patchNum: String, config: Object, - loggedIn: { - type: Boolean, - value: false, - observer: '_loggedInChanged', - }, _schemes: { type: Array, - value: function() { return []; }, + value() { return []; }, computed: '_computeSchemes(change, patchNum)', observer: '_schemesChanged', }, @@ -50,68 +45,59 @@ Gerrit.RESTClientBehavior, ], - focus: function() { + focus() { if (this._schemes.length) { - this.$$('.copyToClipboard').focus(); + this.$.downloadCommands.focusOnCopy(); } else { this.$.download.focus(); } }, - getFocusStops: function() { - var links = this.$$('#archives').querySelectorAll('a'); + getFocusStops() { + const links = this.$$('#archives').querySelectorAll('a'); return { start: this.$.closeButton, end: links[links.length - 1], }; }, - _loggedInChanged: function(loggedIn) { - if (!loggedIn) { return; } - this.$.restAPI.getPreferences().then(function(prefs) { - if (prefs.download_scheme) { - // Note (issue 5180): normalize the download scheme with lower-case. - this._selectedScheme = prefs.download_scheme.toLowerCase(); - } - }.bind(this)); - }, - - _computeDownloadCommands: function(change, patchNum, _selectedScheme) { - var commandObj; - for (var rev in change.revisions) { - if (change.revisions[rev]._number == patchNum && + _computeDownloadCommands(change, patchNum, _selectedScheme) { + let commandObj; + for (const rev in change.revisions) { + if (change.revisions[rev]._number === parseInt(patchNum, 10) && change.revisions[rev].fetch.hasOwnProperty(_selectedScheme)) { commandObj = change.revisions[rev].fetch[_selectedScheme].commands; break; } } - var commands = []; - for (var title in commandObj) { + const commands = []; + for (const title in commandObj) { + if (!commandObj.hasOwnProperty(title)) { continue; } commands.push({ - title: title, + title, command: commandObj[title], }); } return commands; }, - _computeZipDownloadLink: function(change, patchNum) { + _computeZipDownloadLink(change, patchNum) { return this._computeDownloadLink(change, patchNum, true); }, - _computeZipDownloadFilename: function(change, patchNum) { + _computeZipDownloadFilename(change, patchNum) { return this._computeDownloadFilename(change, patchNum, true); }, - _computeDownloadLink: function(change, patchNum, zip) { + _computeDownloadLink(change, patchNum, zip) { return this.changeBaseURL(change._number, patchNum) + '/patch?' + (zip ? 'zip' : 'download'); }, - _computeDownloadFilename: function(change, patchNum, zip) { - var shortRev; - for (var rev in change.revisions) { - if (change.revisions[rev]._number == patchNum) { + _computeDownloadFilename(change, patchNum, zip) { + let shortRev; + for (const rev in change.revisions) { + if (change.revisions[rev]._number === parseInt(patchNum, 10)) { shortRev = rev.substr(0, 7); break; } @@ -119,15 +105,15 @@ return shortRev + '.diff.' + (zip ? 'zip' : 'base64'); }, - _computeArchiveDownloadLink: function(change, patchNum, format) { + _computeArchiveDownloadLink(change, patchNum, format) { return this.changeBaseURL(change._number, patchNum) + '/archive?format=' + format; }, - _computeSchemes: function(change, patchNum) { - for (var rev in change.revisions) { - if (change.revisions[rev]._number == patchNum) { - var fetch = change.revisions[rev].fetch; + _computeSchemes(change, patchNum) { + for (const rev of Object.values(change.revisions || {})) { + if (rev._number === parseInt(patchNum, 10)) { + const fetch = rev.fetch; if (fetch) { return Object.keys(fetch).sort(); } @@ -137,42 +123,21 @@ return []; }, - _computeSchemeSelected: function(scheme, selectedScheme) { - return scheme == selectedScheme; + _computePatchSetQuantity(revisions) { + if (!revisions) { return 0; } + return Object.keys(revisions).length; }, - _handleSchemeTap: function(e) { - e.preventDefault(); - var el = Polymer.dom(e).rootTarget; - this._selectedScheme = el.getAttribute('data-scheme'); - if (this.loggedIn) { - this.$.restAPI.savePreferences({download_scheme: this._selectedScheme}); - } - }, - - _handleInputTap: function(e) { - e.preventDefault(); - Polymer.dom(e).rootTarget.select(); - }, - - _handleCloseTap: function(e) { + _handleCloseTap(e) { e.preventDefault(); this.fire('close', null, {bubbles: false}); }, - _schemesChanged: function(schemes) { - if (schemes.length == 0) { return; } - if (schemes.indexOf(this._selectedScheme) == -1) { + _schemesChanged(schemes) { + if (schemes.length === 0) { return; } + if (!schemes.includes(this._selectedScheme)) { this._selectedScheme = schemes.sort()[0]; } }, - - _copyToClipboard: function(e) { - e.target.parentElement.querySelector('.copyCommand').select(); - document.execCommand('copy'); - getSelection().removeAllRanges(); - e.target.textContent = 'done'; - setTimeout(function() { e.target.textContent = 'copy'; }, 1000); - }, }); })();
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html index 0635d6d..b52363b 100644 --- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-download-dialog.html"> <script>void(0);</script> @@ -48,8 +47,8 @@ fetch: { repo: { commands: { - repo: 'repo download test-project 5/1' - } + repo: 'repo download test-project 5/1', + }, }, ssh: { commands: { @@ -69,8 +68,8 @@ 'Pull': 'git pull ' + 'ssh://andybons@localhost:29418/test-project ' + - 'refs/changes/05/5/1' - } + 'refs/changes/05/5/1', + }, }, http: { commands: { @@ -90,12 +89,12 @@ 'Pull': 'git pull ' + 'http://andybons@localhost:8080/a/test-project ' + - 'refs/changes/05/5/1' - } - } - } - } - } + 'refs/changes/05/5/1', + }, + }, + }, + }, + }, }; } @@ -106,193 +105,84 @@ '34685798fe548b6d17d1e8e5edc43a26d055cc72': { _number: 1, fetch: {}, - } - } + }, + }, }; } - suite('gr-download-dialog tests with no fetch options', function() { - var element; + suite('gr-download-dialog', () => { + let element; + let sandbox; - setup(function() { + setup(() => { element = fixture('basic'); - element.change = getChangeObjectNoFetch(); - element.patchNum = 1; - element.config = { - schemes: { - 'anonymous http': {}, - http: {}, - repo: {}, - ssh: {}, - }, - archives: ['tgz', 'tar', 'tbz2', 'txz'], - }; + sandbox = sinon.sandbox.create(); }); - test('focuses on first download link if no copy links', function() { - flushAsynchronousOperations(); - var focusStub = sinon.stub(element.$.download, 'focus'); - element.focus(); - assert.isTrue(focusStub.called); - focusStub.restore(); - }); - }); - - suite('gr-download-dialog tests', function() { - var element; - - setup(function() { - element = fixture('basic'); - element.change = getChangeObject(); - element.patchNum = 1; - element.config = { - schemes: { - 'anonymous http': {}, - http: {}, - repo: {}, - ssh: {}, - }, - archives: ['tgz', 'tar', 'tbz2', 'txz'], - }; + teardown(() => { + sandbox.restore(); }); - test('focuses on first copy link', function() { - flushAsynchronousOperations(); - var focusStub = sinon.stub(element.$$('.copyToClipboard'), 'focus'); - element.focus(); - flushAsynchronousOperations(); - assert.isTrue(focusStub.called); - focusStub.restore(); - }); - - test('copy to clipboard', function() { - flushAsynchronousOperations(); - var clipboardSpy = sinon.spy(element, '_copyToClipboard'); - var copyBtn = element.$$('.copyToClipboard'); - MockInteractions.tap(copyBtn); - assert.isTrue(clipboardSpy.called); - }); - - test('element visibility', function() { - assert.isFalse(element.$$('ul').hasAttribute('hidden')); - assert.isFalse(element.$$('main').hasAttribute('hidden')); - assert.isFalse(element.$$('.archivesContainer').hasAttribute('hidden')); - - element.set('config.archives', []); - assert.isTrue(element.$$('.archivesContainer').hasAttribute('hidden')); - }); - - test('computed fields', function() { - assert.equal(element._computeArchiveDownloadLink( - {_number: 123}, 2, 'tgz'), - '/changes/123/revisions/2/archive?format=tgz'); - }); - - test('close event', function(done) { - element.addEventListener('close', function() { - done(); - }); - MockInteractions.tap(element.$$('.closeButtonContainer gr-button')); - }); - - test('tab selection', function() { - flushAsynchronousOperations(); - var el = element.$$('[data-scheme="http"]').parentElement; - assert.isTrue(el.hasAttribute('selected')); - ['repo', 'ssh'].forEach(function(scheme) { - var el = element.$$('[data-scheme="' + scheme + '"]').parentElement; - assert.isFalse(el.hasAttribute('selected')); + suite('gr-download-dialog tests with no fetch options', () => { + setup(() => { + element.change = getChangeObjectNoFetch(); + element.patchNum = '1'; + element.config = { + schemes: { + 'anonymous http': {}, + 'http': {}, + 'repo': {}, + 'ssh': {}, + }, + archives: ['tgz', 'tar', 'tbz2', 'txz'], + }; }); - MockInteractions.tap(element.$$('[data-scheme="ssh"]')); - el = element.$$('[data-scheme="ssh"]').parentElement; - assert.isTrue(el.hasAttribute('selected')); - ['http', 'repo'].forEach(function(scheme) { - var el = element.$$('[data-scheme="' + scheme + '"]').parentElement; - assert.isFalse(el.hasAttribute('selected')); + test('focuses on first download link if no copy links', () => { + flushAsynchronousOperations(); + const focusStub = sandbox.stub(element.$.download, 'focus'); + element.focus(); + assert.isTrue(focusStub.called); + focusStub.restore(); }); }); - test('loads scheme from preferences w/o initial login', function(done) { - stub('gr-rest-api-interface', { - getPreferences: function() { - return Promise.resolve({download_scheme: 'repo'}); - }, + suite('gr-download-dialog with fetch options', () => { + setup(() => { + element.change = getChangeObject(); + element.patchNum = '1'; + element.config = { + schemes: { + 'anonymous http': {}, + 'http': {}, + 'repo': {}, + 'ssh': {}, + }, + archives: ['tgz', 'tar', 'tbz2', 'txz'], + }; }); - element.loggedIn = true; - - assert.isTrue(element.$.restAPI.getPreferences.called); - element.$.restAPI.getPreferences.lastCall.returnValue.then(function() { - assert.equal(element._selectedScheme, 'repo'); - done(); - }); - }); - }); - - suite('gr-download-dialog tests', function() { - var element; - - setup(function() { - stub('gr-rest-api-interface', { - getPreferences: function() { - return Promise.resolve({download_scheme: 'repo'}); - }, + test('focuses on first copy link', () => { + flushAsynchronousOperations(); + const focusStub = sinon.stub(element.$.downloadCommands, 'focusOnCopy'); + element.focus(); + flushAsynchronousOperations(); + assert.isTrue(focusStub.called); + focusStub.restore(); }); - element = fixture('loggedIn'); - element.change = getChangeObject(); - element.patchNum = 1; - element.config = { - schemes: { - 'anonymous http': {}, - http: {}, - repo: {}, - ssh: {}, - }, - archives: ['tgz', 'tar', 'tbz2', 'txz'], - }; - }); - - test('loads scheme from preferences', function(done) { - element.$.restAPI.getPreferences.lastCall.returnValue.then(function() { - assert.equal(element._selectedScheme, 'repo'); - done(); + test('computed fields', () => { + assert.equal(element._computeArchiveDownloadLink( + {_number: 123}, 2, 'tgz'), + '/changes/123/revisions/2/archive?format=tgz'); }); - }); - test('saves scheme to preferences', function() { - var savePrefsStub = sinon.stub(element.$.restAPI, 'savePreferences', - function() { return Promise.resolve(); }); - - Polymer.dom.flush(); - - var firstSchemeButton = element.$$('li gr-button[data-scheme]'); - - MockInteractions.tap(firstSchemeButton); - - assert.isTrue(savePrefsStub.called); - assert.equal(savePrefsStub.lastCall.args[0].download_scheme, - firstSchemeButton.getAttribute('data-scheme')); - }); - }); - - test('normalize scheme from preferences', function(done) { - stub('gr-rest-api-interface', { - getPreferences: function() { - return Promise.resolve({download_scheme: 'REPO'}); - }, - }); - element = fixture('loggedIn'); - element.change = getChangeObject(); - element.patchNum = 1; - element.config = { - schemes: {'anonymous http': {}, http: {}, repo: {}, ssh: {}}, - archives: ['tgz', 'tar', 'tbz2', 'txz'], - }; - element.$.restAPI.getPreferences.lastCall.returnValue.then(function() { - assert.equal(element._selectedScheme, 'repo'); - done(); + test('close event', done => { + element.addEventListener('close', () => { + done(); + }); + MockInteractions.tap(element.$$('.closeButtonContainer gr-button')); + }); }); }); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html index e324078..fdfda26 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -14,11 +14,10 @@ limitations under the License. --> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> <link rel="import" href="../../core/gr-reporting/gr-reporting.html"> <link rel="import" href="../../diff/gr-diff/gr-diff.html"> <link rel="import" href="../../diff/gr-diff-cursor/gr-diff-cursor.html"> @@ -27,14 +26,17 @@ <link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../../shared/gr-select/gr-select.html"> +<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-file-list"> <template> - <style> + <style include="shared-styles"> :host { display: block; } .row { + border-top: 1px solid #eee; display: flex; padding: .1em .25em; } @@ -71,6 +73,7 @@ background-color: #ebf5fb; } .path { + cursor: pointer; flex: 1; padding-left: .35em; text-decoration: none; @@ -146,6 +149,7 @@ box-shadow: 0 1px 3px rgba(0, 0, 0, .3); display: block; margin: .25em 0 1em; + overflow-x: auto; } .patchSetSelect { max-width: 8em; @@ -221,10 +225,11 @@ <option value="PARENT">Base</option> <template is="dom-repeat" - items="[[_computePatchSets(revisions.*, patchRange.*)]]" + items="[[computeAllPatchSets(change)]]" as="patchNum"> - <option value$="[[patchNum.num]]" - disabled$="[[_computePatchSetDisabled(patchNum.num, patchRange.patchNum)]]"> + <option + disabled$="[[_computePatchSetDisabled(patchNum.num, patchRange.patchNum)]]" + value$="[[patchNum.num]]"> [[patchNum.num]] [[patchNum.desc]] </option> @@ -233,97 +238,109 @@ </label> </div> </header> - <template is="dom-repeat" - items="[[_shownFiles]]" - as="file" - initial-count="[[_fileListIncrement]]"> - <div class="file-row row"> - <div class="reviewed" hidden$="[[!_loggedIn]]" hidden> - <input type="checkbox" checked="[[file.isReviewed]]" - data-path$="[[file.__path]]" on-change="_handleReviewedChange" - class="reviewed" aria-label="Reviewed checkbox"> - </div> - <div class$="[[_computeClass('status', file.__path)]]" - tabindex="0" - aria-label$="[[_computeFileStatusLabel(file.status)]]"> - [[_computeFileStatus(file.status)]] - </div> - <a class$="[[_computePathClass(file.__path, _expandedFilePaths.*)]]" - href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]" - on-tap="_handleFileTap"> - <div title$="[[_computeFileDisplayName(file.__path)]]" - class="fullFileName"> - [[_computeFileDisplayName(file.__path)]] + <div on-tap="_handleFileListTap"> + <template is="dom-repeat" + items="[[_shownFiles]]" + id="files" + as="file" + initial-count="[[fileListIncrement]]" + target-framerate="1"> + <div class="file-row row" data-path$="[[file.__path]]" tabindex="-1"> + <div class="reviewed" hidden$="[[!_loggedIn]]" hidden> + <input type="checkbox" checked="[[file.isReviewed]]" + class="reviewed" aria-label="Reviewed checkbox"> </div> - <div title$="[[_computeFileDisplayName(file.__path)]]" - class="truncatedFileName"> - [[_computeTruncatedFileDisplayName(file.__path)]] + <div class$="[[_computeClass('status', file.__path)]]" + tabindex="0" + aria-label$="[[_computeFileStatusLabel(file.status)]]"> + [[_computeFileStatus(file.status)]] </div> - <div class="oldPath" hidden$="[[!file.old_path]]" hidden - title$="[[file.old_path]]"> - [[file.old_path]] - </div> - </a> - <div class="comments desktop"> - <span class="drafts"> - [[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]] + <span + data-url="[[_computeDiffURL(change, patchRange, file.__path)]]" + class$="[[_computePathClass(file.__path, _expandedFilePaths.*)]]"> + <a href$="[[_computeDiffURL(change, patchRange, file.__path)]]"> + <span title$="[[_computeFileDisplayName(file.__path)]]" + class="fullFileName"> + [[_computeFileDisplayName(file.__path)]] + </span> + <span title$="[[_computeFileDisplayName(file.__path)]]" + class="truncatedFileName"> + [[_computeTruncatedFileDisplayName(file.__path)]] + </span> + </a> + <div class="oldPath" hidden$="[[!file.old_path]]" hidden + title$="[[file.old_path]]"> + [[file.old_path]] + </div> </span> - [[_computeCommentsString(comments, patchRange.patchNum, file.__path)]] - [[_computeUnresolvedString(comments, drafts, patchRange.patchNum, file.__path)]] - </div> - <div class="comments mobile"> - <span class="drafts"> - [[_computeDraftsStringMobile(drafts, patchRange.patchNum, + <div class="comments desktop"> + <span class="drafts"> + [[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]] + </span> + [[_computeCommentsString(comments, patchRange.patchNum, file.__path)]] + [[_computeUnresolvedString(comments, drafts, patchRange.patchNum, file.__path)]] + </div> + <div class="comments mobile"> + <span class="drafts"> + [[_computeDraftsStringMobile(drafts, patchRange.patchNum, + file.__path)]] + </span> + [[_computeCommentsStringMobile(comments, patchRange.patchNum, file.__path)]] - </span> - [[_computeCommentsStringMobile(comments, patchRange.patchNum, - file.__path)]] + </div> + <div class$="[[_computeClass('stats', file.__path)]]"> + <span + class="added" + tabindex="0" + aria-label$="[[file.lines_inserted]] lines added" + hidden$=[[file.binary]]> + +[[file.lines_inserted]] + </span> + <span + class="removed" + tabindex="0" + aria-label$="[[file.lines_deleted]] lines removed" + hidden$=[[file.binary]]> + -[[file.lines_deleted]] + </span> + <span class$="[[_computeBinaryClass(file.size_delta)]]" + hidden$=[[!file.binary]]> + [[_formatBytes(file.size_delta)]] + [[_formatPercentage(file.size, file.size_delta)]] + </span> + </div> + <div class="show-hide" hidden$="[[_userPrefs.expand_inline_diffs]]"> + <label class="show-hide" data-path$="[[file.__path]]" + data-expand=true> + <input type="checkbox" class="show-hide" + checked$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]" + data-path$="[[file.__path]]" data-expand=true> + [[_computeShowHideText(file.__path, _expandedFilePaths.*)]] + </label> + </div> </div> - <div class$="[[_computeClass('stats', file.__path)]]"> - <span - class="added" - tabindex="0" - aria-label$="[[file.lines_inserted]] lines added" - hidden$=[[file.binary]]> - +[[file.lines_inserted]] - </span> - <span - class="removed" - tabindex="0" - aria-label$="[[file.lines_deleted]] lines removed" - hidden$=[[file.binary]]> - -[[file.lines_deleted]] - </span> - <span class$="[[_computeBinaryClass(file.size_delta)]]" - hidden$=[[!file.binary]]> - [[_formatBytes(file.size_delta)]] - [[_formatPercentage(file.size, file.size_delta)]] - </span> - </div> - <div class="show-hide" hidden$="[[_userPrefs.expand_inline_diffs]]"> - <label class="show-hide"> - <input type="checkbox" class="show-hide" - checked$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]" - data-path$="[[file.__path]]" - on-change="_handleHiddenChange"> - [[_computeShowHideText(file.__path, _expandedFilePaths.*)]] - </label> - </div> - </div> - <gr-diff - no-auto-render - hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]" - project="[[change.project]]" - commit="[[change.current_revision]]" - change-num="[[changeNum]]" - patch-range="[[patchRange]]" - path="[[file.__path]]" - prefs="[[_diffPrefs]]" - project-config="[[projectConfig]]" - view-mode="[[_getDiffViewMode(diffViewMode, _userPrefs)]]"></gr-diff> - </template> - <div class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]"> - <div class="total-stats" hidden$="[[_hideChangeTotals]]"> + <template is="dom-if" + if="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]"> + <gr-diff + no-auto-render + display-line="[[_displayLine]]" + inline-index=[[index]] + hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]" + change-num="[[changeNum]]" + patch-range="[[patchRange]]" + path="[[file.__path]]" + prefs="[[diffPrefs]]" + project-config="[[projectConfig]]" + on-line-selected="_onLineSelected" + no-render-on-prefs-change + view-mode="[[_getDiffViewMode(diffViewMode, _userPrefs)]]"></gr-diff> + </template> + </template> + </div> + <div + class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]" + hidden$="[[_hideChangeTotals]]"> + <div class="total-stats"> <span class="added" tabindex="0" @@ -338,8 +355,10 @@ </span> </div> </div> - <div class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]"> - <div class="total-stats" hidden$="[[_hideBinaryChangeTotals]]"> + <div + class$="row totalChanges [[_computeExpandInlineClass(_userPrefs)]]" + hidden$="[[_hideBinaryChangeTotals]]"> + <div class="total-stats"> <span class="added" aria-label="Total lines added"> [[_formatBytes(_patchChange.size_delta_inserted)]] [[_formatPercentage(_patchChange.total_size, @@ -355,23 +374,33 @@ <gr-button class="fileListButton" id="incrementButton" - hidden$="[[_computeFileListButtonHidden(_numFilesShown, _files)]]" + hidden$="[[_computeFileListButtonHidden(numFilesShown, _files)]]" link on-tap="_incrementNumFilesShown"> - [[_computeIncrementText(_numFilesShown, _files)]] + [[_computeIncrementText(numFilesShown, _files)]] </gr-button> - <gr-button - class="fileListButton" - id="showAllButton" - hidden$="[[_computeFileListButtonHidden(_numFilesShown, _files)]]" - link on-tap="_showAllFiles"> - [[_computeShowAllText(_files)]] - </gr-button> + <gr-tooltip-content + has-tooltip="[[_computeWarnShowAll(_files)]]" + show-icon="[[_computeWarnShowAll(_files)]]" + title$="[[_computeShowAllWarning(_files)]]"> + <gr-button + class="fileListButton" + id="showAllButton" + hidden$="[[_computeFileListButtonHidden(numFilesShown, _files)]]" + link on-tap="_showAllFiles"> + [[_computeShowAllText(_files)]] + </gr-button><!-- + --></gr-tooltip-content> + <gr-diff-preferences + id="diffPreferences" + prefs="{{diffPrefs}}" + local-prefs="{{_localPrefs}}"></gr-diff-preferences> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-storage id="storage"></gr-storage> <gr-diff-cursor id="diffCursor"></gr-diff-cursor> <gr-cursor-manager id="fileCursor" scroll-behavior="keep-visible" + focus-on-move cursor-target-class="selected"></gr-cursor-manager> <gr-reporting id="reporting"></gr-reporting> </template>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js index e91c28dd..7607a44 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -15,12 +15,12 @@ 'use strict'; // Maximum length for patch set descriptions. - var PATCH_DESC_MAX_LENGTH = 500; + const PATCH_DESC_MAX_LENGTH = 500; + const WARN_SHOW_ALL_THRESHOLD = 1000; + const COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; + const MERGE_LIST_PATH = '/MERGE_LIST'; - var COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; - var MERGE_LIST_PATH = '/MERGE_LIST'; - - var FileStatus = { + const FileStatus = { A: 'Added', C: 'Copied', D: 'Deleted', @@ -48,17 +48,18 @@ }, keyEventTarget: { type: Object, - value: function() { return document.body; }, + value() { return document.body; }, }, change: Object, diffViewMode: { type: String, notify: true, + observer: '_updateDiffPreferences', }, _files: { type: Array, observer: '_filesChanged', - value: function() { return []; }, + value() { return []; }, }, _loggedIn: { type: Boolean, @@ -66,26 +67,26 @@ }, _reviewed: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _diffAgainst: String, - _diffPrefs: Object, + diffPrefs: { + type: Object, + notify: true, + observer: '_updateDiffPreferences', + }, _userPrefs: Object, _localPrefs: Object, _showInlineDiffs: Boolean, - _numFilesShown: { + numFilesShown: { type: Number, - value: 75, + notify: true, }, _patchChange: { type: Object, computed: '_calculatePatchChange(_files)', }, - _fileListIncrement: { - type: Number, - readOnly: true, - value: 75, - }, + fileListIncrement: Number, _hideChangeTotals: { type: Boolean, computed: '_shouldHideChangeTotals(_patchChange)', @@ -96,7 +97,7 @@ }, _shownFiles: { type: Array, - computed: '_computeFilesShown(_numFilesShown, _files.*)', + computed: '_computeFilesShown(numFilesShown, _files.*)', }, // Caps the number of files that can be shown and have the 'show diffs' / // 'hide diffs' buttons still be functional. @@ -107,15 +108,14 @@ }, _expandedFilePaths: { type: Array, - value: function() { return []; }, + value() { return []; }, }, + _displayLine: Boolean, }, behaviors: [ - Gerrit.BaseUrlBehavior, Gerrit.KeyboardShortcutBehavior, Gerrit.PatchSetBehavior, - Gerrit.URLEncodingBehavior, ], observers: [ @@ -133,62 +133,66 @@ 'c': '_handleCKey', '[': '_handleLeftBracketKey', ']': '_handleRightBracketKey', - 'o enter': '_handleEnterKey', + 'o': '_handleOKey', 'n': '_handleNKey', 'p': '_handlePKey', 'shift+a': '_handleCapitalAKey', + 'esc': '_handleEscKey', }, - reload: function() { + reload() { if (!this.changeNum || !this.patchRange.patchNum) { return Promise.resolve(); } this._collapseAllDiffs(); - var promises = []; - var _this = this; + const promises = []; - promises.push(this._getFiles().then(function(files) { - _this._files = files; + promises.push(this._getFiles().then(files => { + this._files = files; })); - promises.push(this._getLoggedIn().then(function(loggedIn) { - return _this._loggedIn = loggedIn; - }).then(function(loggedIn) { + promises.push(this._getLoggedIn().then(loggedIn => { + return this._loggedIn = loggedIn; + }).then(loggedIn => { if (!loggedIn) { return; } - return _this._getReviewedFiles().then(function(reviewed) { - _this._reviewed = reviewed; + return this._getReviewedFiles().then(reviewed => { + this._reviewed = reviewed; }); })); this._localPrefs = this.$.storage.getPreferences(); - promises.push(this._getDiffPreferences().then(function(prefs) { - this._diffPrefs = prefs; - }.bind(this))); + promises.push(this._getDiffPreferences().then(prefs => { + this.diffPrefs = prefs; + })); - promises.push(this._getPreferences().then(function(prefs) { + promises.push(this._getPreferences().then(prefs => { this._userPrefs = prefs; if (!this.diffViewMode) { this.set('diffViewMode', prefs.default_diff_view); } - }.bind(this))); + })); }, get diffs() { return Polymer.dom(this.root).querySelectorAll('gr-diff'); }, - _calculatePatchChange: function(files) { - var filesNoCommitMsg = files.filter(function(files) { + openDiffPrefs() { + this.$.diffPreferences.open(); + }, + + _calculatePatchChange(files) { + const filesNoCommitMsg = files.filter(files => { return files.__path !== '/COMMIT_MSG'; }); - return filesNoCommitMsg.reduce(function(acc, obj) { - var inserted = obj.lines_inserted ? obj.lines_inserted : 0; - var deleted = obj.lines_deleted ? obj.lines_deleted : 0; - var total_size = (obj.size && obj.binary) ? obj.size : 0; - var size_delta_inserted = + return filesNoCommitMsg.reduce((acc, obj) => { + const inserted = obj.lines_inserted ? obj.lines_inserted : 0; + const deleted = obj.lines_deleted ? obj.lines_deleted : 0; + const total_size = (obj.size && obj.binary) ? obj.size : 0; + const size_delta_inserted = obj.binary && obj.size_delta > 0 ? obj.size_delta : 0; - var size_delta_deleted = + const size_delta_deleted = obj.binary && obj.size_delta < 0 ? obj.size_delta : 0; return { @@ -202,40 +206,22 @@ size_delta_deleted: 0, total_size: 0}); }, - _getDiffPreferences: function() { + _getDiffPreferences() { return this.$.restAPI.getDiffPreferences(); }, - _getPreferences: function() { + _getPreferences() { return this.$.restAPI.getPreferences(); }, - _computePatchSets: function(revisionRecord) { - var revisions = revisionRecord.base; - var patchNums = []; - for (var commit in revisions) { - if (revisions.hasOwnProperty(commit)) { - patchNums.push({ - num: revisions[commit]._number, - desc: revisions[commit].description, - }); - } - } - return patchNums.sort(function(a, b) { return a.num - b.num; }); - }, - - _computePatchSetDisabled: function(patchNum, currentPatchNum) { + _computePatchSetDisabled(patchNum, currentPatchNum) { return parseInt(patchNum, 10) >= parseInt(currentPatchNum, 10); }, - _handleHiddenChange: function(e) { - this._togglePathExpanded(e.model.file.__path); - }, - - _togglePathExpanded: function(path) { + _togglePathExpanded(path) { // Is the path in the list of expanded diffs? IF so remove it, otherwise // add it to the list. - var pathIndex = this._expandedFilePaths.indexOf(path); + const pathIndex = this._expandedFilePaths.indexOf(path); if (pathIndex === -1) { this.push('_expandedFilePaths', path); } else { @@ -243,79 +229,93 @@ } }, - _togglePathExpandedByIndex: function(index) { + _togglePathExpandedByIndex(index) { this._togglePathExpanded(this._files[index].__path); }, - _handlePatchChange: function(e) { - var patchRange = Object.assign({}, this.patchRange); + _handlePatchChange(e) { + const patchRange = Object.assign({}, this.patchRange); patchRange.basePatchNum = Polymer.dom(e).rootTarget.value; - page.show(this.encodeURL('/c/' + this.changeNum + '/' + - this._patchRangeStr(patchRange), true)); + + Gerrit.Nav.navigateToChange(this.change, patchRange.patchNum, + this._getBasePatchNum(patchRange)); }, - _forEachDiff: function(fn) { - var diffs = this.diffs; - for (var i = 0; i < diffs.length; i++) { + _updateDiffPreferences() { + if (!this.diffs.length) { return; } + // Re-render all expanded diffs sequentially. + const timerName = 'Update ' + this._expandedFilePaths.length + + ' diffs with new prefs'; + this._renderInOrder(this._expandedFilePaths, this.diffs, + this._expandedFilePaths.length) + .then(() => { + this.$.reporting.timeEnd(timerName); + this.$.diffCursor.handleDiffUpdate(); + }); + }, + + _forEachDiff(fn) { + const diffs = this.diffs; + for (let i = 0; i < diffs.length; i++) { fn(diffs[i]); } }, - _expandAllDiffs: function(e) { + _expandAllDiffs(e) { this._showInlineDiffs = true; // Find the list of paths that are in the file list, but not in the // expanded list. - var newPaths = []; - var path; - for (var i = 0; i < this._shownFiles.length; i++) { + const newPaths = []; + let path; + for (let i = 0; i < this._shownFiles.length; i++) { path = this._shownFiles[i].__path; - if (this._expandedFilePaths.indexOf(path) === -1) { + if (!this._expandedFilePaths.includes(path)) { newPaths.push(path); } } - this.splice.apply(this, ['_expandedFilePaths', 0, 0].concat(newPaths)); + this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths)); }, - _collapseAllDiffs: function(e) { + _collapseAllDiffs(e) { this._showInlineDiffs = false; this._expandedFilePaths = []; this.$.diffCursor.handleDiffUpdate(); }, - _computeCommentsString: function(comments, patchNum, path) { + _computeCommentsString(comments, patchNum, path) { return this._computeCountString(comments, patchNum, path, 'comment'); }, - _computeDraftsString: function(drafts, patchNum, path) { + _computeDraftsString(drafts, patchNum, path) { return this._computeCountString(drafts, patchNum, path, 'draft'); }, - _computeDraftsStringMobile: function(drafts, patchNum, path) { - var draftCount = this._computeCountString(drafts, patchNum, path); + _computeDraftsStringMobile(drafts, patchNum, path) { + const draftCount = this._computeCountString(drafts, patchNum, path); return draftCount ? draftCount + 'd' : ''; }, - _computeCommentsStringMobile: function(comments, patchNum, path) { - var commentCount = this._computeCountString(comments, patchNum, path); + _computeCommentsStringMobile(comments, patchNum, path) { + const commentCount = this._computeCountString(comments, patchNum, path); return commentCount ? commentCount + 'c' : ''; }, - _getCommentsForPath: function(comments, patchNum, path) { - return (comments[path] || []).filter(function(c) { + getCommentsForPath(comments, patchNum, path) { + return (comments[path] || []).filter(c => { return parseInt(c.patch_set, 10) === parseInt(patchNum, 10); }); }, - _computeCountString: function(comments, patchNum, path, opt_noun) { + _computeCountString(comments, patchNum, path, opt_noun) { if (!comments) { return ''; } - var patchComments = this._getCommentsForPath(comments, patchNum, path); - var num = patchComments.length; + const patchComments = this.getCommentsForPath(comments, patchNum, path); + const num = patchComments.length; if (num === 0) { return ''; } if (!opt_noun) { return num; } - var output = num + ' ' + opt_noun + (num > 1 ? 's' : ''); + const output = num + ' ' + opt_noun + (num > 1 ? 's' : ''); return output; }, @@ -329,15 +329,21 @@ * @param {string} path * @return {string} */ - _computeUnresolvedString: function(comments, drafts, patchNum, path) { - comments = this._getCommentsForPath(comments, patchNum, path); - drafts = this._getCommentsForPath(drafts, patchNum, path); + _computeUnresolvedString(comments, drafts, patchNum, path) { + const unresolvedNum = this.computeUnresolvedNum( + comments, drafts, patchNum, path); + return unresolvedNum === 0 ? '' : '(' + unresolvedNum + ' unresolved)'; + }, + + computeUnresolvedNum(comments, drafts, patchNum, path) { + comments = this.getCommentsForPath(comments, patchNum, path); + drafts = this.getCommentsForPath(drafts, patchNum, path); comments = comments.concat(drafts); // Create an object where every comment ID is the key of an unresolved // comment. - var idMap = comments.reduce(function(acc, comment) { + const idMap = comments.reduce((acc, comment) => { if (comment.unresolved) { acc[comment.id] = true; } @@ -345,30 +351,25 @@ }, {}); // Set false for the comments that are marked as parents. - comments.forEach(function(comment) { + for (const comment of comments) { idMap[comment.in_reply_to] = false; - }); + } // The unresolved comments are the comments that still have true. - var unresolvedLeaves = Object.keys(idMap).filter(function(key) { + const unresolvedLeaves = Object.keys(idMap).filter(key => { return idMap[key]; }); - return unresolvedLeaves.length === 0 ? - '' : '(' + unresolvedLeaves.length + ' unresolved)'; + return unresolvedLeaves.length; }, - _computeReviewed: function(file, _reviewed) { - return _reviewed.indexOf(file.__path) !== -1; + _computeReviewed(file, _reviewed) { + return _reviewed.includes(file.__path); }, - _handleReviewedChange: function(e) { - this._reviewFile(Polymer.dom(e).rootTarget.getAttribute('data-path')); - }, - - _reviewFile: function(path) { - var index = this._reviewed.indexOf(path); - var reviewed = index !== -1; + _reviewFile(path) { + const index = this._reviewed.indexOf(path); + const reviewed = index !== -1; if (reviewed) { this.splice('_reviewed', index, 1); } else { @@ -378,43 +379,71 @@ this._saveReviewedState(path, !reviewed); }, - _saveReviewedState: function(path, reviewed) { + _saveReviewedState(path, reviewed) { return this.$.restAPI.saveFileReviewed(this.changeNum, this.patchRange.patchNum, path, reviewed); }, - _getLoggedIn: function() { + _getLoggedIn() { return this.$.restAPI.getLoggedIn(); }, - _getReviewedFiles: function() { + _getReviewedFiles() { return this.$.restAPI.getReviewedFiles(this.changeNum, this.patchRange.patchNum); }, - _getFiles: function() { + _getFiles() { return this.$.restAPI.getChangeFilesAsSpeciallySortedArray( - this.changeNum, this.patchRange).then(function(files) { + this.changeNum, this.patchRange).then(files => { // Append UI-specific properties. - return files.map(function(file) { + return files.map(file => { return file; }); }); }, - _handleFileTap: function(e) { + /** + * Handle all events from the file list dom-repeat so event handleers don't + * have to get registered for potentially very long lists. + */ + _handleFileListTap(e) { + // Traverse upwards to find the row element if the target is not the row. + let row = e.target; + while (!row.classList.contains('row') && row.parentElement) { + row = row.parentElement; + } + const path = row.dataset.path; + + // Handle checkbox mark as reviewed. + if (e.target.classList.contains('reviewed')) { + return this._reviewFile(path); + } + // If the user prefers to expand inline diffs rather than opening the diff // view, intercept the click event. - if (e.detail.sourceEvent.metaKey || e.detail.sourceEvent.ctrlKey) { - return; + if (!path || e.detail.sourceEvent.metaKey || + e.detail.sourceEvent.ctrlKey) { + return; } - if (this._userPrefs && this._userPrefs.expand_inline_diffs) { + + if (e.target.dataset.expand || + this._userPrefs && this._userPrefs.expand_inline_diffs) { e.preventDefault(); - this._handleHiddenChange(e); + this._togglePathExpanded(path); + return; + } + + // If we clicked the row but not the link, then simulate a click on the + // anchor. + if (e.target.classList.contains('path') || + e.target.classList.contains('oldPath')) { + const a = row.querySelector('a'); + if (a) { a.click(); } } }, - _handleShiftLeftKey: function(e) { + _handleShiftLeftKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } if (!this._showInlineDiffs) { return; } @@ -422,7 +451,7 @@ this.$.diffCursor.moveLeft(); }, - _handleShiftRightKey: function(e) { + _handleShiftRightKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } if (!this._showInlineDiffs) { return; } @@ -430,7 +459,7 @@ this.$.diffCursor.moveRight(); }, - _handleIKey: function(e) { + _handleIKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e) || this.$.fileCursor.index === -1) { return; } @@ -439,41 +468,48 @@ this._togglePathExpandedByIndex(this.$.fileCursor.index); }, - _handleCapitalIKey: function(e) { + _handleCapitalIKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); this._toggleInlineDiffs(); }, - _handleDownKey: function(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } + _handleDownKey(e) { + if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { + return; + } + e.preventDefault(); if (this._showInlineDiffs) { this.$.diffCursor.moveDown(); + this._displayLine = true; } else { this.$.fileCursor.next(); this.selectedIndex = this.$.fileCursor.index; } }, - _handleUpKey: function(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } + _handleUpKey(e) { + if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { + return; + } e.preventDefault(); if (this._showInlineDiffs) { this.$.diffCursor.moveUp(); + this._displayLine = true; } else { this.$.fileCursor.previous(); this.selectedIndex = this.$.fileCursor.index; } }, - _handleCKey: function(e) { + _handleCKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } - var isRangeSelected = this.diffs.some(function(diff) { + const isRangeSelected = this.diffs.some(diff => { return diff.isRangeSelected(); }, this); if (this._showInlineDiffs && !isRangeSelected) { @@ -482,72 +518,79 @@ } }, - _handleLeftBracketKey: function(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } + _handleLeftBracketKey(e) { + // Check for meta key to avoid overriding native chrome shortcut. + if (this.shouldSuppressKeyboardShortcut(e) || + this.getKeyboardEvent(e).metaKey) { return; } e.preventDefault(); this._openSelectedFile(this._files.length - 1); }, - _handleRightBracketKey: function(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } + _handleRightBracketKey(e) { + // Check for meta key to avoid overriding native chrome shortcut. + if (this.shouldSuppressKeyboardShortcut(e) || + this.getKeyboardEvent(e).metaKey) { return; } e.preventDefault(); this._openSelectedFile(0); }, - _handleEnterKey: function(e) { + _handleOKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } - // Use native handling if an anchor is selected. @see Issue 5754 - if (e.detail && e.detail.keyboardEvent && e.detail.keyboardEvent.target && - e.detail.keyboardEvent.target.tagName === 'A') { return; } - e.preventDefault(); if (this._showInlineDiffs) { this._openCursorFile(); + } else if (this._userPrefs && this._userPrefs.expand_inline_diffs) { + if (this.$.fileCursor.index === -1) { return; } + this._togglePathExpandedByIndex(this.$.fileCursor.index); } else { this._openSelectedFile(); } }, - _handleNKey: function(e) { + _handleNKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } + this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) { + return; + } if (!this._showInlineDiffs) { return; } e.preventDefault(); - if (e.shiftKey) { + if (this.isModifierPressed(e, 'shiftKey')) { this.$.diffCursor.moveToNextCommentThread(); } else { this.$.diffCursor.moveToNextChunk(); } }, - _handlePKey: function(e) { + _handlePKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } + this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) { + return; + } if (!this._showInlineDiffs) { return; } e.preventDefault(); - if (e.shiftKey) { + if (this.isModifierPressed(e, 'shiftKey')) { this.$.diffCursor.moveToPreviousCommentThread(); } else { this.$.diffCursor.moveToPreviousChunk(); } }, - _handleCapitalAKey: function(e) { + _handleCapitalAKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); - this._forEachDiff(function(diff) { + this._forEachDiff(diff => { diff.toggleLeftDiff(); }); }, - _toggleInlineDiffs: function() { + _toggleInlineDiffs() { if (this._showInlineDiffs) { this._collapseAllDiffs(); } else { @@ -555,53 +598,54 @@ } }, - _openCursorFile: function() { - var diff = this.$.diffCursor.getTargetDiffElement(); - page.show(this._computeDiffURL(diff.changeNum, diff.patchRange, - diff.path)); + _openCursorFile() { + const diff = this.$.diffCursor.getTargetDiffElement(); + Gerrit.Nav.navigateToDiff(this.change, diff.path, + diff.patchRange.patchNum, this._getBasePatchNum(this.patchRange)); }, - _openSelectedFile: function(opt_index) { + _openSelectedFile(opt_index) { if (opt_index != null) { this.$.fileCursor.setCursorAtIndex(opt_index); } - page.show(this._computeDiffURL(this.changeNum, this.patchRange, - this._files[this.$.fileCursor.index].__path)); + if (!this._files[this.$.fileCursor.index]) { return; } + Gerrit.Nav.navigateToDiff(this.change, + this._files[this.$.fileCursor.index].__path, this.patchRange.patchNum, + this._getBasePatchNum(this.patchRange)); }, - _addDraftAtTarget: function() { - var diff = this.$.diffCursor.getTargetDiffElement(); - var target = this.$.diffCursor.getTargetLineElement(); + _addDraftAtTarget() { + const diff = this.$.diffCursor.getTargetDiffElement(); + const target = this.$.diffCursor.getTargetLineElement(); if (diff && target) { diff.addDraftAtLine(target); } }, - _shouldHideChangeTotals: function(_patchChange) { + _shouldHideChangeTotals(_patchChange) { return _patchChange.inserted === 0 && _patchChange.deleted === 0; }, - _shouldHideBinaryChangeTotals: function(_patchChange) { + _shouldHideBinaryChangeTotals(_patchChange) { return _patchChange.size_delta_inserted === 0 && _patchChange.size_delta_deleted === 0; }, - _computeFileStatus: function(status) { + _computeFileStatus(status) { return status || 'M'; }, - _computeDiffURL: function(changeNum, patchRange, path) { - return this.encodeURL(this.getBaseUrl() + '/c/' + changeNum + '/' + - this._patchRangeStr(patchRange) + '/' + path, true); + _computeDiffURL(change, patchRange, path) { + return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum, + this._getBasePatchNum(patchRange)); }, - _patchRangeStr: function(patchRange) { - return patchRange.basePatchNum !== 'PARENT' ? - patchRange.basePatchNum + '..' + patchRange.patchNum : - patchRange.patchNum + ''; + _getBasePatchNum(patchRange) { + return patchRange.basePatchNum === 'PARENT' ? + undefined : patchRange.basePatchNum; }, - _computeFileDisplayName: function(path) { + _computeFileDisplayName(path) { if (path === COMMIT_MESSAGE_PATH) { return 'Commit message'; } else if (path === MERGE_LIST_PATH) { @@ -610,65 +654,66 @@ return path; }, - _computeTruncatedFileDisplayName: function(path) { + _computeTruncatedFileDisplayName(path) { return util.truncatePath(this._computeFileDisplayName(path)); }, - _formatBytes: function(bytes) { + _formatBytes(bytes) { if (bytes == 0) return '+/-0 B'; - var bits = 1024; - var decimals = 1; - var sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; - var exponent = Math.floor(Math.log(Math.abs(bytes)) / Math.log(bits)); - var prepend = bytes > 0 ? '+' : ''; + const bits = 1024; + const decimals = 1; + const sizes = + ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + const exponent = Math.floor(Math.log(Math.abs(bytes)) / Math.log(bits)); + const prepend = bytes > 0 ? '+' : ''; return prepend + parseFloat((bytes / Math.pow(bits, exponent)) .toFixed(decimals)) + ' ' + sizes[exponent]; }, - _formatPercentage: function(size, delta) { - var oldSize = size - delta; + _formatPercentage(size, delta) { + const oldSize = size - delta; if (oldSize === 0) { return ''; } - var percentage = Math.round(Math.abs(delta * 100 / oldSize)); + const percentage = Math.round(Math.abs(delta * 100 / oldSize)); return '(' + (delta > 0 ? '+' : '-') + percentage + '%)'; }, - _computeBinaryClass: function(delta) { + _computeBinaryClass(delta) { if (delta === 0) { return; } return delta >= 0 ? 'added' : 'removed'; }, - _computeClass: function(baseClass, path) { - var classes = [baseClass]; + _computeClass(baseClass, path) { + const classes = [baseClass]; if (path === COMMIT_MESSAGE_PATH || path === MERGE_LIST_PATH) { classes.push('invisible'); } return classes.join(' '); }, - _computeExpandInlineClass: function(userPrefs) { + _computeExpandInlineClass(userPrefs) { return userPrefs.expand_inline_diffs ? 'expandInline' : ''; }, - _computePathClass: function(path, expandedFilesRecord) { + _computePathClass(path, expandedFilesRecord) { return this._isFileExpanded(path, expandedFilesRecord) ? 'path expanded' : 'path'; }, - _computeShowHideText: function(path, expandedFilesRecord) { + _computeShowHideText(path, expandedFilesRecord) { return this._isFileExpanded(path, expandedFilesRecord) ? '▼' : '◀'; }, - _computeFilesShown: function(numFilesShown, files) { + _computeFilesShown(numFilesShown, files) { return files.base.slice(0, numFilesShown); }, - _setReviewedFiles: function(shownFiles, files, reviewedRecord, loggedIn) { + _setReviewedFiles(shownFiles, files, reviewedRecord, loggedIn) { if (!loggedIn) { return; } - var reviewed = reviewedRecord.base; - var fileReviewed; - for (var i = 0; i < files.length; i++) { + const reviewed = reviewedRecord.base; + let fileReviewed; + for (let i = 0; i < files.length; i++) { fileReviewed = this._computeReviewed(files[i], reviewed); this._files[i].isReviewed = fileReviewed; if (i < shownFiles.length) { @@ -677,45 +722,56 @@ } }, - _filesChanged: function() { - this.async(function() { - var diffElements = Polymer.dom(this.root).querySelectorAll('gr-diff'); + _updateDiffCursor() { + const diffElements = Polymer.dom(this.root).querySelectorAll('gr-diff'); - // Overwrite the cursor's list of diffs: - this.$.diffCursor.splice.apply(this.$.diffCursor, - ['diffs', 0, this.$.diffCursor.diffs.length].concat(diffElements)); - - var files = Polymer.dom(this.root).querySelectorAll('.file-row'); - this.$.fileCursor.stops = files; - this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true); - }.bind(this), 1); + // Overwrite the cursor's list of diffs: + this.$.diffCursor.splice( + ...['diffs', 0, this.$.diffCursor.diffs.length].concat(diffElements)); }, - _incrementNumFilesShown: function() { - this._numFilesShown += this._fileListIncrement; + _filesChanged() { + Polymer.dom.flush(); + const files = Polymer.dom(this.root).querySelectorAll('.file-row'); + this.$.fileCursor.stops = files; + this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true); }, - _computeFileListButtonHidden: function(numFilesShown, files) { + _incrementNumFilesShown() { + this.numFilesShown += this.fileListIncrement; + }, + + _computeFileListButtonHidden(numFilesShown, files) { return numFilesShown >= files.length; }, - _computeIncrementText: function(numFilesShown, files) { + _computeIncrementText(numFilesShown, files) { if (!files) { return ''; } - var text = - Math.min(this._fileListIncrement, files.length - numFilesShown); + const text = + Math.min(this.fileListIncrement, files.length - numFilesShown); return 'Show ' + text + ' more'; }, - _computeShowAllText: function(files) { + _computeShowAllText(files) { if (!files) { return ''; } return 'Show all ' + files.length + ' files'; }, - _showAllFiles: function() { - this._numFilesShown = this._files.length; + _computeWarnShowAll(files) { + return files.length > WARN_SHOW_ALL_THRESHOLD; }, - _updateSelected: function(patchRange) { + _computeShowAllWarning(files) { + if (!this._computeWarnShowAll(files)) { return ''; } + return 'Warning: showing all ' + files.length + + ' files may take several seconds.'; + }, + + _showAllFiles() { + this.numFilesShown = this._files.length; + }, + + _updateSelected(patchRange) { this._diffAgainst = patchRange.basePatchNum; }, @@ -730,7 +786,7 @@ * * @return {String} */ - _getDiffViewMode: function(diffViewMode, userPrefs) { + _getDiffViewMode(diffViewMode, userPrefs) { if (diffViewMode) { return diffViewMode; } else if (userPrefs) { @@ -739,25 +795,30 @@ return 'SIDE_BY_SIDE'; }, - _fileListActionsVisible: function(shownFilesRecord, + _fileListActionsVisible(shownFilesRecord, maxFilesForBulkActions) { return shownFilesRecord.base.length <= maxFilesForBulkActions; }, - _computePatchSetDescription: function(revisions, patchNum) { - var rev = this.getRevisionByPatchNum(revisions, patchNum); + _computePatchSetDescription(revisions, patchNum) { + const rev = this.getRevisionByPatchNum(revisions, patchNum); return (rev && rev.description) ? rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; }, - _computeFileStatusLabel: function(status) { - var statusCode = this._computeFileStatus(status); + _computeFileStatusLabel(status) { + const statusCode = this._computeFileStatus(status); return FileStatus.hasOwnProperty(statusCode) ? FileStatus[statusCode] : 'Status Unknown'; }, - _isFileExpanded: function(path, expandedFilesRecord) { - return expandedFilesRecord.base.indexOf(path) !== -1; + _isFileExpanded(path, expandedFilesRecord) { + return expandedFilesRecord.base.includes(path); + }, + + _onLineSelected(e, detail) { + this.$.diffCursor.moveToLineNumber(detail.number, detail.side, + detail.path); }, /** @@ -767,25 +828,30 @@ * one. * @param {splice} record The splice record in the expanded paths list. */ - _expandedPathsChanged: function(record) { + _expandedPathsChanged(record) { if (!record) { return; } // Find the paths introduced by the new index splices: - var newPaths = record.indexSplices - .map(function(splice) { + const newPaths = record.indexSplices + .map(splice => { return splice.object.slice(splice.index, splice.index + splice.addedCount); }) - .reduce(function(acc, paths) { return acc.concat(paths); }, []); + .reduce((acc, paths) => { return acc.concat(paths); }, []); - var timerName = 'Expand ' + newPaths.length + ' diffs'; + const timerName = 'Expand ' + newPaths.length + ' diffs'; this.$.reporting.time(timerName); + // Required so that the newly created diff view is included in this.diffs. + Polymer.dom.flush(); + this._renderInOrder(newPaths, this.diffs, newPaths.length) - .then(function() { + .then(() => { this.$.reporting.timeEnd(timerName); this.$.diffCursor.handleDiffUpdate(); - }.bind(this)); + }); + this._updateDiffCursor(); + this.$.diffCursor.handleDiffUpdate(); }, /** @@ -798,21 +864,21 @@ * is used to generate log messages. * @return {!Promise} */ - _renderInOrder: function(paths, diffElements, initialCount) { + _renderInOrder(paths, diffElements, initialCount) { if (!paths.length) { console.log('Finished expanding', initialCount, 'diff(s)'); return Promise.resolve(); } console.log('Expanding diff', 1 + initialCount - paths.length, 'of', initialCount, ':', paths[0]); - var diffElem = this._findDiffByPath(paths[0], diffElements); - var promises = [diffElem.reload()]; + const diffElem = this._findDiffByPath(paths[0], diffElements); + const promises = [diffElem.reload()]; if (this._isLoggedIn) { promises.push(this._reviewFile(paths[0])); } - return Promise.all(promises).then(function() { + return Promise.all(promises).then(() => { return this._renderInOrder(paths.slice(1), diffElements, initialCount); - }.bind(this)); + }); }, /** @@ -821,12 +887,19 @@ * @param {!NodeList<!GrDiffElement>} diffElements * @return {!GrDiffElement} */ - _findDiffByPath: function(path, diffElements) { - for (var i = 0; i < diffElements.length; i++) { + _findDiffByPath(path, diffElements) { + for (let i = 0; i < diffElements.length; i++) { if (diffElements[i].path === path) { return diffElements[i]; } } }, + + _handleEscKey(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + e.preventDefault(); + this._displayLine = false; + }, }); })();
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html index bd4619f..7cd96e6 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -20,10 +20,11 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../bower_components/page/page.js"></script> <script src="../../../scripts/util.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html"> <link rel="import" href="gr-file-list.html"> <script>void(0);</script> @@ -41,36 +42,47 @@ </test-fixture> <script> - suite('gr-file-list tests', function() { - var element; - var sandbox; - var saveStub; + suite('gr-file-list tests', () => { + let element; + let sandbox; + let saveStub; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(true); }, - getPreferences: function() { return Promise.resolve({}); }, - fetchJSON: function() { return Promise.resolve({}); }, + getLoggedIn() { return Promise.resolve(true); }, + getPreferences() { return Promise.resolve({}); }, + fetchJSON() { return Promise.resolve({}); }, }); stub('gr-date-formatter', { - _loadTimeFormat: function() { return Promise.resolve(''); }, + _loadTimeFormat() { return Promise.resolve(''); }, }); stub('gr-diff', { - reload: function() { return Promise.resolve(); }, + reload() { return Promise.resolve(); }, }); element = fixture('basic'); + element.numFilesShown = 200; saveStub = sandbox.stub(element, '_saveReviewedState', - function() { return Promise.resolve(); }); + () => { return Promise.resolve(); }); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('get file list', function(done) { - var getChangeFilesStub = sandbox.stub(element.$.restAPI, 'getChangeFiles', - function() { + test('correct number of files are shown', () => { + element._files = _.times(500, i => { + return {__path: '/file' + i, lines_inserted: 9}; + }); + flushAsynchronousOperations(); + assert.equal( + Polymer.dom(element.root).querySelectorAll('.file-row').length, + element.numFilesShown); + }); + + test('get file list', done => { + const getChangeFilesStub = sandbox.stub(element.$.restAPI, 'getChangeFiles', + () => { return Promise.resolve({ '/COMMIT_MSG': {lines_inserted: 9}, 'tags.html': {lines_deleted: 123}, @@ -78,8 +90,8 @@ }); }); - element._getFiles().then(function(files) { - var filenames = files.map(function(f) { return f.__path; }); + element._getFiles().then(files => { + const filenames = files.map(f => { return f.__path; }); assert.deepEqual(filenames, ['/COMMIT_MSG', 'about.txt', 'tags.html']); assert.deepEqual(files[0], { lines_inserted: 9, @@ -102,7 +114,7 @@ }); }); - test('calculate totals for patch number', function() { + test('calculate totals for patch number', () => { element._files = [ {__path: '/COMMIT_MSG', lines_inserted: 9}, { @@ -177,7 +189,7 @@ assert.isFalse(element._hideChangeTotals); }); - test('binary only files', function() { + test('binary only files', () => { element._files = [ {__path: '/COMMIT_MSG', lines_inserted: 9}, {__path: 'file_binary', binary: true, size_delta: 10, size: 100}, @@ -194,7 +206,7 @@ assert.isTrue(element._hideChangeTotals); }); - test('binary and regular files', function() { + test('binary and regular files', () => { element._files = [ {__path: '/COMMIT_MSG', lines_inserted: 9}, {__path: 'file_binary', binary: true, size_delta: 10, size: 100}, @@ -213,64 +225,64 @@ assert.isFalse(element._hideChangeTotals); }); - test('_formatBytes function', function() { - var table = { - 64: '+64 B', - 1023: '+1023 B', - 1024: '+1 KiB', - 4096: '+4 KiB', - 1073741824: '+1 GiB', + test('_formatBytes function', () => { + const table = { + '64': '+64 B', + '1023': '+1023 B', + '1024': '+1 KiB', + '4096': '+4 KiB', + '1073741824': '+1 GiB', '-64': '-64 B', '-1023': '-1023 B', '-1024': '-1 KiB', '-4096': '-4 KiB', '-1073741824': '-1 GiB', - 0: '+/-0 B', + '0': '+/-0 B', }; - for (var bytes in table) { + for (const bytes in table) { if (table.hasOwnProperty(bytes)) { assert.equal(element._formatBytes(bytes), table[bytes]); } } }); - test('_formatPercentage function', function() { - var table = [ - { size: 100, + test('_formatPercentage function', () => { + const table = [ + {size: 100, delta: 100, display: '', }, - { size: 195060, + {size: 195060, delta: 64, display: '(+0%)', }, - { size: 195060, + {size: 195060, delta: -64, display: '(-0%)', }, - { size: 394892, + {size: 394892, delta: -7128, display: '(-2%)', }, - { size: 90, + {size: 90, delta: -10, display: '(-10%)', }, - { size: 110, + {size: 110, delta: 10, display: '(+10%)', }, ]; - table.forEach(function(item) { + for (const item of table) { assert.equal(element._formatPercentage( item.size, item.delta), item.display); - }); + } }); - suite('keyboard shortcuts', function() { - setup(function() { + suite('keyboard shortcuts', () => { + setup(() => { element._files = [ {__path: '/COMMIT_MSG'}, {__path: 'file_added_in_rev2.txt'}, @@ -281,15 +293,16 @@ basePatchNum: 'PARENT', patchNum: '2', }; + element.change = {_number: 42}; element.$.fileCursor.setCursorAtIndex(0); }); - test('toggle left diff via shortcut', function() { - var toggleLeftDiffStub = sandbox.stub(); + test('toggle left diff via shortcut', () => { + const toggleLeftDiffStub = sandbox.stub(); // Property getter cannot be stubbed w/ sandbox due to a bug in Sinon. // https://github.com/sinonjs/sinon/issues/781 - var diffsStub = sinon.stub(element, 'diffs', { - get: function() { + const diffsStub = sinon.stub(element, 'diffs', { + get() { return [{toggleLeftDiff: toggleLeftDiffStub}]; }, }); @@ -298,33 +311,39 @@ diffsStub.restore(); }); - test('keyboard shortcuts', function() { + test('keyboard shortcuts', () => { flushAsynchronousOperations(); - var items = Polymer.dom(element.root).querySelectorAll('.file-row'); + const items = Polymer.dom(element.root).querySelectorAll('.file-row'); element.$.fileCursor.stops = items; element.$.fileCursor.setCursorAtIndex(0); assert.equal(items.length, 3); assert.isTrue(items[0].classList.contains('selected')); assert.isFalse(items[1].classList.contains('selected')); assert.isFalse(items[2].classList.contains('selected')); + // j with a modifier should not move the cursor. + MockInteractions.pressAndReleaseKeyOn(element, 74, 'shift', 'j'); + assert.equal(element.$.fileCursor.index, 0); MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); assert.equal(element.$.fileCursor.index, 1); assert.equal(element.selectedIndex, 1); MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); - var showStub = sandbox.stub(page, 'show'); + const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); assert.equal(element.$.fileCursor.index, 2); assert.equal(element.selectedIndex, 2); - MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter'); - assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'), - 'Should navigate to /c/42/2/myfile.txt'); + + // k with a modifier should not move the cursor. + MockInteractions.pressAndReleaseKeyOn(element, 75, 'shift', 'k'); + assert.equal(element.$.fileCursor.index, 2); MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); assert.equal(element.$.fileCursor.index, 1); assert.equal(element.selectedIndex, 1); MockInteractions.pressAndReleaseKeyOn(element, 79, null, 'o'); - assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'), + + assert(navStub.lastCall.calledWith(element.change, + 'file_added_in_rev2.txt', '2'), 'Should navigate to /c/42/2/file_added_in_rev2.txt'); MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); @@ -334,10 +353,11 @@ assert.equal(element.selectedIndex, 0); }); - test('i key shows/hides selected inline diff', function() { + test('i key shows/hides selected inline diff', () => { sandbox.stub(element, '_expandedPathsChanged'); flushAsynchronousOperations(); - element.$.fileCursor.stops = element.diffs; + const files = Polymer.dom(element.root).querySelectorAll('.file-row'); + element.$.fileCursor.stops = files; element.$.fileCursor.setCursorAtIndex(0); MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i'); flushAsynchronousOperations(); @@ -352,58 +372,80 @@ MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i'); flushAsynchronousOperations(); - for (var index in element.diffs) { + for (const index in element.diffs) { + if (!element.diffs.hasOwnProperty(index)) { continue; } assert.include(element._expandedFilePaths, element.diffs[index].path); } MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i'); flushAsynchronousOperations(); - for (var index in element.diffs) { + for (const index in element.diffs) { + if (!element.diffs.hasOwnProperty(index)) { continue; } assert.notInclude(element._expandedFilePaths, element.diffs[index].path); } }); - test('_handleEnterKey navigates', function() { - sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false); - sandbox.stub(element, 'modifierPressed').returns(false); - var expandStub = sandbox.stub(element, '_openCursorFile'); - var navStub = sandbox.stub(element, '_openSelectedFile'); - var e = new CustomEvent('fake-keyboard-event'); - sinon.stub(e, 'preventDefault'); - element._showInlineDiffs = false; - element._handleEnterKey(e); - assert.isTrue(e.preventDefault.called); - assert.isTrue(navStub.called); - assert.isFalse(expandStub.called); - }); + suite('_handleOKey', () => { + let interact; - test('_handleEnterKey expands', function() { - sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false); - sandbox.stub(element, 'modifierPressed').returns(false); - var expandStub = sandbox.stub(element, '_openCursorFile'); - var navStub = sandbox.stub(element, '_openSelectedFile'); - var e = new CustomEvent('fake-keyboard-event'); - sinon.stub(e, 'preventDefault'); - element._showInlineDiffs = true; - element._handleEnterKey(e); - assert.isTrue(e.preventDefault.called); - assert.isFalse(navStub.called); - assert.isTrue(expandStub.called); - }); + setup(() => { + sandbox.stub(element, 'shouldSuppressKeyboardShortcut') + .returns(false); + sandbox.stub(element, 'modifierPressed').returns(false); + const openCursorStub = sandbox.stub(element, '_openCursorFile'); + const openSelectedStub = sandbox.stub(element, '_openSelectedFile'); + const expandStub = sandbox.stub(element, '_togglePathExpanded'); - test('_handleEnterKey noop when anchor focused', function() { - sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false); - sandbox.stub(element, 'modifierPressed').returns(false); - var e = new CustomEvent('fake-keyboard-event', - {detail: {keyboardEvent: {target: document.createElement('a')}}}); - sinon.stub(e, 'preventDefault'); - element._handleEnterKey(e); - assert.isFalse(e.preventDefault.called); + interact = function(opt_payload) { + openCursorStub.reset(); + openSelectedStub.reset(); + expandStub.reset(); + + const e = new CustomEvent('fake-keyboard-event', opt_payload); + sinon.stub(e, 'preventDefault'); + element._handleOKey(e); + assert.isTrue(e.preventDefault.called); + const result = {}; + if (openCursorStub.called) { + result.opened_cursor = true; + } + if (openSelectedStub.called) { + result.opened_selected = true; + } + if (expandStub.called) { + result.expanded = true; + } + return result; + }; + }); + + test('open from selected file', () => { + element._showInlineDiffs = false; + assert.deepEqual(interact(), {opened_selected: true}); + }); + + test('open from diff cursor', () => { + element._showInlineDiffs = true; + assert.deepEqual(interact(), {opened_cursor: true}); + + // "Show diffs" mode overrides userPrefs.expand_inline_diffs + element._userPrefs = {expand_inline_diffs: true}; + assert.deepEqual(interact(), {opened_cursor: true}); + }); + + test('expand when user prefers', () => { + element._showInlineDiffs = false; + assert.deepEqual(interact(), {opened_selected: true}); + element._userPrefs = {}; + assert.deepEqual(interact(), {opened_selected: true}); + element._userPrefs.expand_inline_diffs = true; + assert.deepEqual(interact(), {expanded: true}); + }); }); }); - test('comment filtering', function() { - var comments = { + test('comment filtering', () => { + const comments = { '/COMMIT_MSG': [ {patch_set: 1, message: 'Done', updated: '2017-02-08 16:40:49'}, {patch_set: 1, message: 'oh hay', updated: '2017-02-09 16:40:49'}, @@ -439,7 +481,7 @@ }, ], }; - var drafts = { + const drafts = { 'unresolved.file': [ { patch_set: 2, @@ -471,13 +513,13 @@ '1d'); assert.equal( element._computeCountString(comments, '1', - 'file_added_in_rev2.txt', 'comment'), ''); + 'file_added_in_rev2.txt', 'comment'), ''); assert.equal( element._computeCommentsStringMobile(comments, '1', - 'file_added_in_rev2.txt'), ''); + 'file_added_in_rev2.txt'), ''); assert.equal( element._computeDraftsStringMobile(comments, '1', - 'file_added_in_rev2.txt'), ''); + 'file_added_in_rev2.txt'), ''); assert.equal( element._computeCountString(comments, '2', '/COMMIT_MSG', 'comment'), '1 comment'); @@ -498,20 +540,24 @@ '2d'); assert.equal( element._computeCountString(comments, '2', - 'file_added_in_rev2.txt', 'comment'), ''); + 'file_added_in_rev2.txt', 'comment'), ''); assert.equal(element._computeCountString(comments, '2', 'unresolved.file', 'comment'), '3 comments'); assert.equal( element._computeUnresolvedString(comments, [], 2, 'myfile.txt'), ''); assert.equal( + element.computeUnresolvedNum(comments, [], 2, 'myfile.txt'), 0); + assert.equal( element._computeUnresolvedString(comments, [], 2, 'unresolved.file'), '(1 unresolved)'); assert.equal( + element.computeUnresolvedNum(comments, [], 2, 'unresolved.file'), 1); + assert.equal( element._computeUnresolvedString(comments, drafts, 2, - 'unresolved.file'), ''); + 'unresolved.file'), ''); }); - test('computed properties', function() { + test('computed properties', () => { assert.equal(element._computeFileStatus('A'), 'A'); assert.equal(element._computeFileStatus(undefined), 'M'); assert.equal(element._computeFileStatus(null), 'M'); @@ -530,7 +576,7 @@ {expand_inline_diffs: false}), ''); }); - test('file review status', function() { + test('file review status', () => { element._files = [ {__path: '/COMMIT_MSG'}, {__path: 'file_added_in_rev2.txt'}, @@ -546,13 +592,13 @@ element.$.fileCursor.setCursorAtIndex(0); flushAsynchronousOperations(); - var fileRows = + const fileRows = Polymer.dom(element.root).querySelectorAll('.row:not(.header)'); - var commitMsg = fileRows[0].querySelector( + const commitMsg = fileRows[0].querySelector( 'input.reviewed[type="checkbox"]'); - var fileAdded = fileRows[1].querySelector( + const fileAdded = fileRows[1].querySelector( 'input.reviewed[type="checkbox"]'); - var myFile = fileRows[2].querySelector( + const myFile = fileRows[2].querySelector( 'input.reviewed[type="checkbox"]'); assert.isTrue(commitMsg.checked); @@ -565,15 +611,15 @@ assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true)); }); - test('patch set from revisions', function() { - var expected = [ + test('patch set from revisions', () => { + const expected = [ {num: 1, desc: 'test'}, {num: 2, desc: 'test'}, {num: 3, desc: 'test'}, {num: 4, desc: 'test'}, ]; - var patchNums = element._computePatchSets({ - base: { + const patchNums = element.computeAllPatchSets({ + revisions: { rev3: {_number: 3, description: 'test'}, rev1: {_number: 1, description: 'test'}, rev4: {_number: 4, description: 'test'}, @@ -581,41 +627,34 @@ }, }); assert.equal(patchNums.length, expected.length); - for (var i = 0; i < expected.length; i++) { + for (let i = 0; i < expected.length; i++) { assert.deepEqual(patchNums[i], expected[i]); } }); - test('patch range string', function() { - assert.equal( - element._patchRangeStr({basePatchNum: 'PARENT', patchNum: '1'}), - '1'); - assert.equal( - element._patchRangeStr({basePatchNum: '1', patchNum: '3'}), - '1..3'); - }); - - test('diff against dropdown', function(done) { - var showStub = sandbox.stub(page, 'show'); + test('diff against dropdown', done => { + const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); element.changeNum = '42'; element.patchRange = { basePatchNum: 'PARENT', patchNum: '3', }; - element.revisions = { - rev1: {_number: 1}, - rev2: {_number: 2}, - rev3: {_number: 3}, + element.change = { + revisions: { + rev1: {_number: 1}, + rev2: {_number: 2}, + rev3: {_number: 3}, + }, }; - flush(function() { - var selectEl = element.$.patchChange; + flush(() => { + const selectEl = element.$.patchChange; assert.equal(selectEl.value, 'PARENT'); assert.isTrue(element.$$('option[value="3"]').hasAttribute('disabled')); - selectEl.addEventListener('change', function() { + selectEl.addEventListener('change', () => { assert.equal(selectEl.value, '2'); - assert(showStub.lastCall.calledWithExactly('/c/42/2..3'), + assert(navStub.lastCall.calledWithExactly(element.change, '3', '2'), 'Should navigate to /c/42/2..3'); - showStub.restore(); + navStub.restore(); done(); }); selectEl.value = '2'; @@ -623,7 +662,7 @@ }); }); - test('checkbox shows/hides diff inline', function() { + test('checkbox shows/hides diff inline', () => { element._files = [ {__path: 'myfile.txt'}, ]; @@ -635,35 +674,20 @@ element.$.fileCursor.setCursorAtIndex(0); sandbox.stub(element, '_expandedPathsChanged'); flushAsynchronousOperations(); - var fileRows = + const fileRows = Polymer.dom(element.root).querySelectorAll('.row:not(.header)'); - var showHideCheck = fileRows[0].querySelector( + // Because the label surrounds the input, the tap event is triggered + // there first. + const showHideLabel = fileRows[0].querySelector('label.show-hide'); + const showHideCheck = fileRows[0].querySelector( 'input.show-hide[type="checkbox"]'); assert.isNotOk(showHideCheck.checked); - MockInteractions.tap(showHideCheck); + MockInteractions.tap(showHideLabel); assert.isOk(showHideCheck.checked); assert.notEqual(element._expandedFilePaths.indexOf('myfile.txt'), -1); }); - test('path should be properly escaped', function() { - element._files = [ - {__path: 'foo bar/my+file.txt%'}, - ]; - element.changeNum = '42'; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '2', - }; - flushAsynchronousOperations(); - // Slashes should be preserved, and spaces should be translated to `+`. - // @see Issue 4255 regarding double-encoding. - // @see Issue 4577 regarding more readable URLs. - assert.equal( - element.$$('a').getAttribute('href'), - '/c/42/2/foo+bar/my%252Bfile.txt%2525'); - }); - - test('diff mode correctly toggles the diffs', function() { + test('diff mode correctly toggles the diffs', () => { element._files = [ {__path: 'myfile.txt'}, ]; @@ -672,26 +696,35 @@ basePatchNum: 'PARENT', patchNum: '2', }; + sandbox.spy(element, '_updateDiffPreferences'); element.$.fileCursor.setCursorAtIndex(0); flushAsynchronousOperations(); - var diffDisplay = element.diffs[0]; + + // Tap on a file to generate the diff. + const row = Polymer.dom(element.root) + .querySelectorAll('.row:not(.header) label.show-hide')[0]; + + MockInteractions.tap(row); + flushAsynchronousOperations(); + const diffDisplay = element.diffs[0]; element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'}; assert.equal(element.diffViewMode, 'SIDE_BY_SIDE'); assert.equal(diffDisplay.viewMode, 'SIDE_BY_SIDE'); element.set('diffViewMode', 'UNIFIED_DIFF'); assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF'); + assert.isTrue(element._updateDiffPreferences.called); }); - test('diff mode selector initializes from preferences', function() { - var resolvePrefs; - var prefsPromise = new Promise(function(resolve) { + test('diff mode selector initializes from preferences', () => { + let resolvePrefs; + const prefsPromise = new Promise(resolve => { resolvePrefs = resolve; }); sandbox.stub(element, '_getPreferences').returns(prefsPromise); // Attach a new gr-file-list so we can intercept the preferences fetch. - var view = document.createElement('gr-file-list'); - var select = view.$.modeSelect; + const view = document.createElement('gr-file-list'); + const select = view.$.modeSelect; fixture('blank').appendChild(view); flushAsynchronousOperations(); @@ -705,8 +738,8 @@ document.getElementById('blank').restore(); }); - test('show/hide diffs disabled for large amounts of files', function(done) { - var computeSpy = sandbox.spy(element, '_fileListActionsVisible'); + test('show/hide diffs disabled for large amounts of files', done => { + const computeSpy = sandbox.spy(element, '_fileListActionsVisible'); element._files = []; element.changeNum = '42'; element.patchRange = { @@ -714,27 +747,27 @@ patchNum: '2', }; element.$.fileCursor.setCursorAtIndex(0); - flush(function() { + flush(() => { assert.isTrue(computeSpy.lastCall.returnValue); - var arr = []; - _.times(element._maxFilesForBulkActions + 1, function() { + const arr = []; + _.times(element._maxFilesForBulkActions + 1, () => { arr.push({__path: 'myfile.txt'}); }); element._files = arr; - element._numFilesShown = arr.length; + element.numFilesShown = arr.length; assert.isFalse(computeSpy.lastCall.returnValue); done(); }); }); - test('expanded attribute not set on path when not expanded', function() { + test('expanded attribute not set on path when not expanded', () => { element._files = [ {__path: '/COMMIT_MSG'}, ]; assert.isNotOk(element.$$('.expanded')); }); - test('_getDiffViewMode', function() { + test('_getDiffViewMode', () => { // No user prefs or diff view mode set. assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); // User prefs but no diff view mode set. @@ -747,7 +780,7 @@ assert.equal(element._getDiffViewMode( element.diffViewMode, element._userPrefs), 'SIDE_BY_SIDE'); }); - test('expand_inline_diffs user preference', function() { + test('expand_inline_diffs user preference', () => { element._files = [ {__path: '/COMMIT_MSG'}, ]; @@ -758,30 +791,30 @@ }; sandbox.stub(element, '_expandedPathsChanged'); flushAsynchronousOperations(); - var commitMsgFile = Polymer.dom(element.root) + const commitMsgFile = Polymer.dom(element.root) .querySelectorAll('.row:not(.header) a')[0]; // Remove href attribute so the app doesn't route to a diff view commitMsgFile.removeAttribute('href'); - var hiddenChangeSpy = sandbox.spy(element, '_handleHiddenChange'); + const togglePathSpy = sandbox.spy(element, '_togglePathExpanded'); MockInteractions.tap(commitMsgFile); flushAsynchronousOperations(); - assert(hiddenChangeSpy.notCalled, 'file is opened as diff view'); + assert(togglePathSpy.notCalled, 'file is opened as diff view'); assert.isNotOk(element.$$('.expanded')); element._userPrefs = {expand_inline_diffs: true}; flushAsynchronousOperations(); MockInteractions.tap(commitMsgFile); flushAsynchronousOperations(); - assert(hiddenChangeSpy.calledOnce, 'file is expanded'); + assert(togglePathSpy.calledOnce, 'file is expanded'); assert.isOk(element.$$('.expanded')); }); - test('_togglePathExpanded', function() { - var path = 'path/to/my/file.txt'; + test('_togglePathExpanded', () => { + const path = 'path/to/my/file.txt'; element.files = [{__path: path}]; - var renderStub = sandbox.stub(element, '_renderInOrder') + const renderStub = sandbox.stub(element, '_renderInOrder') .returns(Promise.resolve()); assert.equal(element._expandedFilePaths.length, 0); @@ -797,111 +830,384 @@ assert.notInclude(element._expandedFilePaths, path); }); - test('_expandedPathsChanged', function(done) { + test('_expandedPathsChanged', done => { sandbox.stub(element, '_reviewFile'); - var path = 'path/to/my/file.txt'; - var diffs = [{ - path: path, - reload: function() { + const path = 'path/to/my/file.txt'; + const diffs = [{ + path, + reload() { done(); }, }]; - var diffsStub = sinon.stub(element, 'diffs', { - get: function() { return diffs; }, + sinon.stub(element, 'diffs', { + get() { return diffs; }, }); element.push('_expandedFilePaths', path); }); - suite('_handleFileTap', function() { + suite('_handleFileListTap', () => { function testForModifier(modifier) { - var e = {preventDefault: function() {}}; + const e = {preventDefault() {}}; e.detail = {sourceEvent: {}}; + e.target = { + dataset: {path: '/test'}, + classList: element.classList, + }; + e.detail.sourceEvent[modifier] = true; - var hiddenChangeStub = sandbox.stub(element, '_handleHiddenChange'); - element._userPrefs = { expand_inline_diffs: true }; + const togglePathStub = sandbox.stub(element, '_togglePathExpanded'); + element._userPrefs = {expand_inline_diffs: true}; - element._handleFileTap(e); - assert.isFalse(hiddenChangeStub.called); + element._handleFileListTap(e); + assert.isFalse(togglePathStub.called); e.detail.sourceEvent[modifier] = false; - element._handleFileTap(e); - assert.equal(hiddenChangeStub.callCount, 1); + element._handleFileListTap(e); + assert.equal(togglePathStub.callCount, 1); - element._userPrefs = { expand_inline_diffs: false }; - element._handleFileTap(e); - assert.equal(hiddenChangeStub.callCount, 1); + element._userPrefs = {expand_inline_diffs: false}; + element._handleFileListTap(e); + assert.equal(togglePathStub.callCount, 1); } - test('_handleFileTap meta', function() { + test('_handleFileListTap meta', () => { testForModifier('metaKey'); }); - test('_handleFileTap ctrl', function() { + test('_handleFileListTap ctrl', () => { testForModifier('ctrlKey'); }); }); - test('_renderInOrder', function(done) { - var reviewStub = sandbox.stub(element, '_reviewFile'); - var callCount = 0; - var diffs = [{ + test('_renderInOrder', done => { + const reviewStub = sandbox.stub(element, '_reviewFile'); + let callCount = 0; + const diffs = [{ path: 'p0', - reload: function() { + reload() { assert.equal(callCount++, 2); return Promise.resolve(); }, }, { path: 'p1', - reload: function() { + reload() { assert.equal(callCount++, 1); return Promise.resolve(); }, }, { path: 'p2', - reload: function() { + reload() { assert.equal(callCount++, 0); return Promise.resolve(); }, }]; element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3) - .then(function() { - assert.isFalse(reviewStub.called); - done(); - }); + .then(() => { + assert.isFalse(reviewStub.called); + done(); + }); }); - test('_renderInOrder logged in', function(done) { + test('_renderInOrder logged in', done => { element._isLoggedIn = true; - var reviewStub = sandbox.stub(element, '_reviewFile'); - var callCount = 0; - var diffs = [{ + const reviewStub = sandbox.stub(element, '_reviewFile'); + let callCount = 0; + const diffs = [{ path: 'p0', - reload: function() { + reload() { assert.equal(reviewStub.callCount, 2); assert.equal(callCount++, 2); return Promise.resolve(); }, }, { path: 'p1', - reload: function() { + reload() { assert.equal(reviewStub.callCount, 1); assert.equal(callCount++, 1); return Promise.resolve(); }, }, { path: 'p2', - reload: function() { + reload() { assert.equal(reviewStub.callCount, 0); assert.equal(callCount++, 0); return Promise.resolve(); }, }]; element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3) - .then(function() { - assert.equal(reviewStub.callCount, 3); - done(); - }); + .then(() => { + assert.equal(reviewStub.callCount, 3); + done(); + }); }); }); + + suite('gr-file-list inline diff tests', () => { + let element; + let sandbox; + + const setupDiff = function(diff) { + const mock = document.createElement('mock-diff-response'); + diff._diff = mock.diffResponse; + diff._comments = { + left: [], + right: [], + }; + diff.prefs = { + context: 10, + tab_size: 8, + font_size: 12, + line_length: 100, + cursor_blink_rate: 0, + line_wrapping: false, + intraline_difference: true, + show_line_endings: true, + show_tabs: true, + show_whitespace_errors: true, + syntax_highlighting: true, + auto_hide_diff_table_header: true, + theme: 'DEFAULT', + ignore_whitespace: 'IGNORE_NONE', + }; + diff._renderDiffTable(); + }; + + const renderAndGetNewDiffs = function(index) { + const diffs = + Polymer.dom(element.root).querySelectorAll('gr-diff'); + + for (let i = index; i < diffs.length; i++) { + setupDiff(diffs[i]); + } + + element._updateDiffCursor(); + element.$.diffCursor.handleDiffUpdate(); + return diffs; + }; + + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, + getPreferences() { return Promise.resolve({}); }, + }); + stub('gr-date-formatter', { + _loadTimeFormat() { return Promise.resolve(''); }, + }); + stub('gr-diff', { + reload() { return Promise.resolve(); }, + }); + element = fixture('basic'); + element.numFilesShown = 75; + element.selectedIndex = 0; + element._files = [ + {__path: '/COMMIT_MSG', lines_inserted: 9}, + { + __path: 'file_added_in_rev2.txt', + lines_inserted: 1, + lines_deleted: 1, + size_delta: 10, + size: 100, + }, + { + __path: 'myfile.txt', + lines_inserted: 1, + lines_deleted: 1, + size_delta: 10, + size: 100, + }, + ]; + element._reviewed = ['/COMMIT_MSG', 'myfile.txt']; + element._loggedIn = true; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + sandbox.stub(window, 'fetch', () => { + return Promise.resolve(); + }); + flushAsynchronousOperations(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('cursor with individually opened files', () => { + MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i'); + flushAsynchronousOperations(); + let diffs = renderAndGetNewDiffs(0); + const diffStops = diffs[0].getCursorStops(); + + // 1 diff should be rendered. + assert.equal(diffs.length, 1); + + // No line number is selected. + assert.isFalse(diffStops[10].classList.contains('target-row')); + + // Tapping content on a line selects the line number. + MockInteractions.tap(Polymer.dom( + diffStops[10]).querySelectorAll('.contentText')[0]); + flushAsynchronousOperations(); + assert.isTrue(diffStops[10].classList.contains('target-row')); + + // Keyboard shortcuts are still moving the file cursor, not the diff + // cursor. + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); + flushAsynchronousOperations(); + assert.isTrue(diffStops[10].classList.contains('target-row')); + assert.isFalse(diffStops[11].classList.contains('target-row')); + + // The file cusor is now at 1. + assert.equal(element.$.fileCursor.index, 1); + MockInteractions.pressAndReleaseKeyOn(element, 73, null, 'i'); + flushAsynchronousOperations(); + + diffs = renderAndGetNewDiffs(1); + // Two diffs should be rendered. + assert.equal(diffs.length, 2); + const diffStopsFirst = diffs[0].getCursorStops(); + const diffStopsSecond = diffs[1].getCursorStops(); + + // The line on the first diff is stil selected + assert.isTrue(diffStopsFirst[10].classList.contains('target-row')); + assert.isFalse(diffStopsSecond[10].classList.contains('target-row')); + }); + + test('cursor with toggle all files', () => { + MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'i'); + flushAsynchronousOperations(); + + const diffs = renderAndGetNewDiffs(0); + const diffStops = diffs[0].getCursorStops(); + + // 1 diff should be rendered. + assert.equal(diffs.length, 3); + + // No line number is selected. + assert.isFalse(diffStops[10].classList.contains('target-row')); + + // Tapping content on a line selects the line number. + MockInteractions.tap(Polymer.dom( + diffStops[10]).querySelectorAll('.contentText')[0]); + flushAsynchronousOperations(); + assert.isTrue(diffStops[10].classList.contains('target-row')); + + // Keyboard shortcuts are still moving the file cursor, not the diff + // cursor. + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); + flushAsynchronousOperations(); + assert.isFalse(diffStops[10].classList.contains('target-row')); + assert.isTrue(diffStops[11].classList.contains('target-row')); + + // The file cusor is still at 0. + assert.equal(element.$.fileCursor.index, 0); + }); + + suite('n key presses', () => { + let nKeySpy; + let nextCommentStub; + let nextChunkStub; + let fileRows; + setup(() => { + nKeySpy = sandbox.spy(element, '_handleNKey'); + nextCommentStub = sandbox.stub(element.$.diffCursor, + 'moveToNextCommentThread'); + nextChunkStub = sandbox.stub(element.$.diffCursor, + 'moveToNextChunk'); + fileRows = + Polymer.dom(element.root).querySelectorAll('.row:not(.header)'); + }); + test('n key with all files expanded and no shift key', () => { + MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'i'); + flushAsynchronousOperations(); + + // Handle N key should return before calling diff cursor functions. + MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n'); + assert.isTrue(nKeySpy.called); + assert.isFalse(nextCommentStub.called); + + // This is also called in diffCursor.moveToFirstChunk. + assert.equal(nextChunkStub.callCount, 1); + assert.isFalse(!!element._showInlineDiffs); + }); + + test('n key with all files expanded and shift key', () => { + MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, null, 'i'); + flushAsynchronousOperations(); + + MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n'); + assert.isTrue(nKeySpy.called); + assert.isFalse(nextCommentStub.called); + + // This is also called in diffCursor.moveToFirstChunk. + assert.equal(nextChunkStub.callCount, 1); + assert.isFalse(!!element._showInlineDiffs); + }); + + test('n key without all files expanded and shift key', () => { + MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, 'shift', 'i'); + flushAsynchronousOperations(); + + MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n'); + assert.isTrue(nKeySpy.called); + assert.isFalse(nextCommentStub.called); + + // This is also called in diffCursor.moveToFirstChunk. + assert.equal(nextChunkStub.callCount, 2); + assert.isTrue(element._showInlineDiffs); + }); + + test('n key without all files expanded and no shift key', () => { + MockInteractions.pressAndReleaseKeyOn(fileRows[0], 73, 'shift', 'i'); + flushAsynchronousOperations(); + + MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n'); + assert.isTrue(nKeySpy.called); + assert.isTrue(nextCommentStub.called); + + // This is also called in diffCursor.moveToFirstChunk. + assert.equal(nextChunkStub.callCount, 1); + assert.isTrue(element._showInlineDiffs); + }); + }); + + test('_openSelectedFile behavior', () => { + const _files = element._files; + element.set('_files', []); + const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); + // Noop when there are no files. + element._openSelectedFile(); + assert.isFalse(navStub.called); + + element.set('_files', _files); + flushAsynchronousOperations(); + // Navigates when a file is selected. + element._openSelectedFile(); + assert.isTrue(navStub.called); + }); + + test('_displayLine', () => { + sandbox.stub(element, 'shouldSuppressKeyboardShortcut', () => false); + sandbox.stub(element, 'modifierPressed', () => false); + element._showInlineDiffs = true; + const mockEvent = {preventDefault() {}}; + + element._displayLine = false; + element._handleDownKey(mockEvent); + assert.isTrue(element._displayLine); + + element._displayLine = false; + element._handleUpKey(mockEvent); + assert.isTrue(element._displayLine); + + element._displayLine = true; + element._handleEscKey(mockEvent); + assert.isFalse(element._displayLine); + }); + }); + + a11ySuite('basic'); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html new file mode 100644 index 0000000..36e2bc7 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.html
@@ -0,0 +1,118 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-selector/iron-selector.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-label-score-row"> + <template> + <style include="shared-styles"> + .labelContainer { + margin-bottom: .5em; + } + .labelContainer:last-child { + margin-bottom: 0; + } + .labelName { + display: inline-block; + margin-right: .5em; + min-width: 7em; + text-align: right; + white-space: nowrap; + width: 25%; + } + .labelMessage { + color: #666; + } + .placeholder::before { + content: ' '; + } + .selectedValueText { + color: #666; + font-style: italic; + margin-bottom: .5em; + margin-left: calc(25% + .5em); + } + .selectedValueText.hidden { + display: none; + } + iron-selector > gr-button:first-of-type { + border-bottom-left-radius: 2px; + border-top-left-radius: 2px; + } + iron-selector > gr-button:last-of-type { + border-bottom-right-radius: 2px; + border-top-right-radius: 2px; + } + iron-selector > gr-button.iron-selected { + background-color: #ddd; + } + gr-button { + min-width: 40px; + } + .placeholder { + display: inline-block; + width: 40px; + } + @media only screen and (max-width: 25em) { + .labelName { + margin: 0; + text-align: center; + width: 100%; + } + .selectedValueText { + display: none; + } + } + </style> + <div class="labelContainer"> + <span class="labelName">[[label.name]]</span> + <template is="dom-repeat" + items="[[_computeBlankItems(permittedLabels, label.name, 'start')]]" + as="value"> + <span class="placeholder" data-label$="[[label.name]]"></span> + </template> + <iron-selector + attr-for-selected="value" + selected="[[_computeLabelValue(labels, permittedLabels, label)]]" + hidden$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]" + on-selected-item-changed="_setSelectedValueText"> + <template is="dom-repeat" + items="[[_computePermittedLabelValues(permittedLabels, label.name)]]" + as="value"> + <gr-button has-tooltip value$="[[value]]" + title$="[[_computeLabelValueTitle(labels, label.name, value)]]"> + [[value]]</gr-button> + </template> + </iron-selector> + <template is="dom-repeat" + items="[[_computeBlankItems(permittedLabels, label.name, 'end')]]" + as="value"> + <span class="placeholder" data-label$="[[label.name]]"></span> + </template> + <span class="labelMessage" + hidden$="[[_computeAnyPermittedLabelValues(permittedLabels, label.name)]]"> + You don't have permission to edit this label. + </span> + <div class$="selectedValueText [[_computeHiddenClass(permittedLabels, label.name)]]"> + <span id="selectedValueLabel">[[_selectedValueText]]</span> + </div> + </div> + </template> + <script src="gr-label-score-row.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js new file mode 100644 index 0000000..6021d77 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -0,0 +1,104 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-label-score-row', + properties: { + label: Object, + labels: Object, + name: { + type: String, + reflectToAttribute: true, + }, + permittedLabels: Object, + labelValues: Object, + _selectedValueText: { + type: String, + value: 'No value selected', + }, + }, + + get selectedItem() { + if (!this._ironSelector) { return; } + return this._ironSelector.selectedItem; + }, + + get selectedValue() { + if (!this._ironSelector) { return; } + return this._ironSelector.selected; + }, + + setSelectedValue(value) { + // The selector may not be present if it’s not at the latest patch set. + if (!this._ironSelector) { return; } + this._ironSelector.select(value); + }, + + get _ironSelector() { + return this.$$('iron-selector'); + }, + + _computeBlankItems(permittedLabels, label, side) { + if (!permittedLabels || !permittedLabels[label]) { return []; } + const startPosition = this.labelValues[parseInt( + permittedLabels[label][0])]; + if (side === 'start') { + return new Array(startPosition); + } + const endPosition = this.labelValues[parseInt( + permittedLabels[label][permittedLabels[label].length - 1])]; + return new Array(Object.keys(this.labelValues).length - endPosition - 1); + }, + + _computeLabelValue(labels, permittedLabels, label) { + if (!labels[label.name]) { return null; } + const labelValue = label.value; + const len = permittedLabels[label.name] != null ? + permittedLabels[label.name].length : 0; + for (let i = 0; i < len; i++) { + const val = permittedLabels[label.name][i]; + if (val === labelValue) { + return val; + } + } + return null; + }, + + _setSelectedValueText(e) { + // Needed because when the selected item changes, it first changes to + // nothing and then to the new item. + if (!e.target.selectedItem) { return; } + this._selectedValueText = e.target.selectedItem.getAttribute('title'); + }, + + _computeAnyPermittedLabelValues(permittedLabels, label) { + return permittedLabels.hasOwnProperty(label); + }, + + _computeHiddenClass(permittedLabels, label) { + return !this._computeAnyPermittedLabelValues(permittedLabels, label) ? + 'hidden' : ''; + }, + + _computePermittedLabelValues(permittedLabels, label) { + return permittedLabels[label]; + }, + + _computeLabelValueTitle(labels, label, value) { + return labels[label] && labels[label].values[value]; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html new file mode 100644 index 0000000..4245cf5 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -0,0 +1,256 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-label-score-row</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-label-score-row.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-label-score-row></gr-label-score-row> + </template> +</test-fixture> + +<script> + suite('gr-label-row-score tests', () => { + let element; + let sandbox; + + setup(done => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.labels = { + 'Code-Review': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + value: 1, + all: [{ + _account_id: 123, + value: 1, + }], + }, + 'Verified': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + value: 1, + all: [{ + _account_id: 123, + value: 1, + }], + }, + }; + + element.permittedLabels = { + 'Code-Review': [ + '-2', + '-1', + ' 0', + '+1', + '+2', + ], + 'Verified': [ + '-1', + ' 0', + '+1', + ], + }; + + element.labelValues = {'0': 2, '1': 3, '2': 4, '-2': 0, '-1': 1}; + + element.label = { + name: 'Verified', + value: '+1', + }; + + flush(done); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('label picker', () => { + assert.ok(element.$$('iron-selector')); + MockInteractions.tap(element.$$( + 'gr-button[value="-1"]')); + flushAsynchronousOperations(); + assert.strictEqual(element.selectedValue, '-1'); + assert.strictEqual(element.selectedItem + .textContent.trim(), '-1'); + assert.strictEqual( + element.$.selectedValueLabel.textContent.trim(), 'bad'); + }); + + test('correct item is selected', () => { + // 1 should be the value of the selected item + assert.strictEqual(element.$$('iron-selector').selected, '+1'); + assert.strictEqual( + element.$$('iron-selector').selectedItem + .textContent.trim(), '+1'); + assert.strictEqual( + element.$.selectedValueLabel.textContent.trim(), 'good'); + }); + + test('do not display tooltips on touch devices', () => { + const verifiedBtn = element.$$( + 'iron-selector > gr-button[value="-1"]'); + + // On touch devices, tooltips should not be shown. + verifiedBtn._isTouchDevice = true; + verifiedBtn._handleShowTooltip(); + assert.isNotOk(verifiedBtn._tooltip); + verifiedBtn._handleHideTooltip(); + assert.isNotOk(verifiedBtn._tooltip); + + // On other devices, tooltips should be shown. + verifiedBtn._isTouchDevice = false; + verifiedBtn._handleShowTooltip(); + assert.isOk(verifiedBtn._tooltip); + verifiedBtn._handleHideTooltip(); + assert.isNotOk(verifiedBtn._tooltip); + }); + + test('_computeLabelValue', () => { + assert.strictEqual(element._computeLabelValue(element.labels, + element.permittedLabels, + element.label), '+1'); + }); + + test('_computeBlankItems', () => { + element.labelValues = { + '-2': 0, + '-1': 1, + '0': 2, + '1': 3, + '2': 4, + }; + + assert.strictEqual(element._computeBlankItems(element.permittedLabels, + 'Code-Review').length, 0); + + assert.strictEqual(element._computeBlankItems(element.permittedLabels, + 'Verified').length, 1); + }); + + test('changes in label score are reflected in the DOM', () => { + element.labels = { + 'Code-Review': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + }, + 'Verified': { + values: { + ' 0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + }, + }; + const selector = element.$$('iron-selector'); + element.set('label', {name: 'Verified', value: ' 0'}); + flushAsynchronousOperations(); + assert.strictEqual(selector.selected, ' 0'); + assert.strictEqual( + element.$.selectedValueLabel.textContent.trim(), 'No score'); + }); + + test('without permitted labels', () => { + element.permittedLabels = { + Verified: [ + '-1', + ' 0', + '+1', + ], + }; + flushAsynchronousOperations(); + assert.isOk(element.$$('iron-selector')); + assert.isFalse(element.$$('iron-selector').hidden); + + element.permittedLabels = {}; + flushAsynchronousOperations(); + assert.isOk(element.$$('iron-selector')); + assert.isTrue(element.$$('iron-selector').hidden); + }); + test('asymetrical labels', () => { + element.permittedLabels = { + 'Code-Review': [ + '-2', + '-1', + ' 0', + '+1', + '+2', + ], + 'Verified': [ + ' 0', + '+1', + ], + }; + flushAsynchronousOperations(); + assert.strictEqual(element.$$('iron-selector') + .items.length, 2); + assert.strictEqual(Polymer.dom(element.root). + querySelectorAll('.placeholder').length, 3); + + element.permittedLabels = { + 'Code-Review': [ + ' 0', + '+1', + ], + 'Verified': [ + '-2', + '-1', + ' 0', + '+1', + '+2', + ], + }; + flushAsynchronousOperations(); + assert.strictEqual(element.$$('iron-selector') + .items.length, 5); + assert.strictEqual(Polymer.dom(element.root). + querySelectorAll('.placeholder').length, 0); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html new file mode 100644 index 0000000..2532c77 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.html
@@ -0,0 +1,50 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../gr-label-score-row/gr-label-score-row.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-label-scores"> + <template> + <style include="shared-styles"> + .mergedMessage { + font-style: italic; + text-align: center; + width: 100%; + } + @media only screen and (max-width: 25em) { + :host { + text-align: center; + } + } + </style> + <template is="dom-repeat" items="[[_labels]]" as="label"> + <gr-label-score-row + label="[[label]]" + name="[[label.name]]" + labels="[[change.labels]]" + permitted-labels="[[permittedLabels]]" + label-values="[[_labelValues]]"></gr-label-score-row> + </template> + <div class="mergedMessage" + hidden$="[[!_changeIsMerged(change.status)]]"> + Because this change has been merged, votes may not be decreased. + </div> + </template> + <script src="gr-label-scores.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js new file mode 100644 index 0000000..3b53a99 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
@@ -0,0 +1,114 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-label-scores', + properties: { + _labels: { + type: Array, + computed: '_computeLabels(change.labels.*, account)', + }, + permittedLabels: { + type: Object, + observer: '_computeColumns', + }, + change: Object, + _labelValues: Object, + }, + + getLabelValues() { + const labels = {}; + for (const label in this.permittedLabels) { + if (!this.permittedLabels.hasOwnProperty(label)) { continue; } + + const selectorEl = this.$$(`gr-label-score-row[name="${label}"]`); + if (!selectorEl) { continue; } + + // The user may have not voted on this label. + if (!selectorEl.selectedItem) { continue; } + + const selectedVal = parseInt(selectorEl.selectedValue, 10); + + // Only send the selection if the user changed it. + let prevVal = this._getVoteForAccount(this.change.labels, label, + this.account); + if (prevVal !== null) { + prevVal = parseInt(prevVal, 10); + } + if (selectedVal !== prevVal) { + labels[label] = selectedVal; + } + } + return labels; + }, + + _getStringLabelValue(labels, labelName, numberValue) { + for (const k in labels[labelName].values) { + if (parseInt(k, 10) === numberValue) { + return k; + } + } + return numberValue; + }, + + _getVoteForAccount(labels, labelName, account) { + const votes = labels[labelName]; + if (votes.all && votes.all.length > 0) { + for (let i = 0; i < votes.all.length; i++) { + if (votes.all[i]._account_id == account._account_id) { + return this._getStringLabelValue( + labels, labelName, votes.all[i].value); + } + } + } + return null; + }, + + _computeLabels(labelRecord) { + const labelsObj = labelRecord.base; + if (!labelsObj) { return []; } + return Object.keys(labelsObj).sort().map(key => { + return { + name: key, + value: this._getVoteForAccount(labelsObj, key, this.account), + }; + }); + }, + + _computeColumns(permittedLabels) { + const labels = Object.keys(permittedLabels); + const values = {}; + for (const label of labels) { + for (const value of permittedLabels[label]) { + values[parseInt(value, 10)] = true; + } + } + + const orderedValues = Object.keys(values).sort((a, b) => { + return a - b; + }); + + for (let i = 0; i < orderedValues.length; i++) { + values[orderedValues[i]] = i; + } + this._labelValues = values; + }, + + _changeIsMerged(changeStatus) { + return changeStatus === 'MERGED'; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html new file mode 100644 index 0000000..e3a0d8c --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
@@ -0,0 +1,175 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-label-scores</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-label-scores.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-label-scores></gr-label-scores> + </template> +</test-fixture> + +<script> + suite('gr-label-scores tests', () => { + let element; + let sandbox; + + setup(done => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(false); }, + }); + element = fixture('basic'); + element.change = { + _number: '123', + labels: { + 'Code-Review': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + value: 1, + all: [{ + _account_id: 123, + value: 1, + }], + }, + 'Verified': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + value: 1, + all: [{ + _account_id: 123, + value: 1, + }], + }, + }, + }; + + element.account = { + _account_id: 123, + }; + + element.permittedLabels = { + 'Code-Review': [ + '-2', + '-1', + ' 0', + '+1', + '+2', + ], + 'Verified': [ + '-1', + ' 0', + '+1', + ], + }; + flush(done); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('get and set label scores', () => { + for (const label in element.permittedLabels) { + if (element.permittedLabels.hasOwnProperty(label)) { + const row = element.$$('gr-label-score-row[name="' + label + '"]'); + row.setSelectedValue(-1); + } + } + assert.deepEqual(element.getLabelValues(), { + 'Code-Review': -1, + 'Verified': -1, + }); + }); + + test('_getVoteForAccount', () => { + const labelName = 'Code-Review'; + assert.strictEqual(element._getVoteForAccount( + element.change.labels, labelName, element.account), + '+1'); + }); + + test('_computeColumns', () => { + element._computeColumns(element.permittedLabels); + assert.deepEqual(element._labelValues, { + '-2': 0, + '-1': 1, + '0': 2, + '1': 3, + '2': 4, + }); + }); + + test('changes in label score are reflected in _labels', () => { + element.change = { + _number: '123', + labels: { + 'Code-Review': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + }, + 'Verified': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + }, + }, + }; + assert.deepEqual(element._labels [ + {name: 'Code-Review', value: null}, + {name: 'Verified', value: null} + ]); + element.set(['change', 'labels', 'Verified', 'all'], + [{_account_id: 123, value: 1}]); + assert.deepEqual(element._labels, [ + {name: 'Code-Review', value: null}, + {name: 'Verified', value: '+1'}, + ]); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.html b/polygerrit-ui/app/elements/change/gr-message/gr-message.html index 831914e..d30a888 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -20,12 +20,13 @@ <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> <link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <link rel="import" href="../gr-comment-list/gr-comment-list.html"> <dom-module id="gr-message"> <template> - <style> + <style include="shared-styles"> :host { border-top: 1px solid #ddd; display: block; @@ -35,6 +36,9 @@ :host(.expanded) { cursor: auto; } + :host > div { + padding: 0 var(--default-horizontal-margin); + } gr-avatar { position: absolute; left: var(--default-horizontal-margin); @@ -45,7 +49,7 @@ display: flex; white-space: nowrap; } - .showAvatar.expanded .contentContainer { + .contentContainer { margin-left: calc(var(--default-horizontal-margin) + 2.5em); padding: 10px 0; } @@ -75,14 +79,14 @@ font-weight: bold; } .message { - max-width: 80ch; + --gr-formatted-text-prose-max-width: 80ch; } .collapsed .message { max-width: none; overflow: hidden; text-overflow: ellipsis; } - .collapsed .name, + .collapsed .author, .collapsed .content, .collapsed .message, .collapsed .updateCategory, @@ -108,11 +112,11 @@ .collapsed .date { position: static; } - .collapsed .name { + .collapsed .author { color: var(--default-text-color); margin-right: .4em; } - .expanded .name { + .expanded .author { cursor: pointer; } .date { @@ -124,11 +128,23 @@ .replyContainer { padding: .5em 0 1em; } + .positiveVote { + box-shadow: inset 0 4.4em #d4ffd4; + } + .negativeVote { + box-shadow: inset 0 4.4em #ffd4d4; + } </style> - <div class$="[[_computeClass(_expanded, showAvatar)]]"> + <div class$="[[_computeClass(_expanded, showAvatar, message)]]"> <gr-avatar account="[[author]]" image-size="100"></gr-avatar> <div class="contentContainer"> - <div class="name" on-tap="_handleNameTap">[[author.name]]</div> + <div class="author" on-tap="_handleAuthorTap"> + <span hidden$="[[!showOnBehalfOf]]"> + <span class="name">[[message.real_author.name]]</span> + on behalf of + </span> + <span class="name">[[author.name]]</span> + </div> <template is="dom-if" if="[[message.message]]"> <div class="content"> <div class="message hideOnOpen">[[message.message]]</div>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js index e782943..c196051 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -14,6 +14,10 @@ (function() { 'use strict'; + const CI_LABELS = ['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue']; + const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+: /; + const LABEL_TITLE_SCORE_PATTERN = /([A-Za-z0-9-]+)([+-]\d+)/; + Polymer({ is: 'gr-message', @@ -30,7 +34,7 @@ */ listeners: { - 'tap': '_handleTap', + tap: '_handleTap', }, properties: { @@ -62,6 +66,10 @@ type: Boolean, computed: '_computeShowAvatar(author, config)', }, + showOnBehalfOf: { + type: Boolean, + computed: '_computeShowOnBehalfOf(message)', + }, showReplyButton: { type: Boolean, computed: '_computeShowReplyButton(message, _loggedIn)', @@ -82,16 +90,16 @@ '_updateExpandedClass(message.expanded)', ], - ready: function() { - this.$.restAPI.getConfig().then(function(config) { + ready() { + this.$.restAPI.getConfig().then(config => { this.config = config; - }.bind(this)); - this.$.restAPI.getLoggedIn().then(function(loggedIn) { + }); + this.$.restAPI.getLoggedIn().then(loggedIn => { this._loggedIn = loggedIn; - }.bind(this)); + }); }, - _updateExpandedClass: function(expanded) { + _updateExpandedClass(expanded) { if (expanded) { this.classList.add('expanded'); } else { @@ -99,19 +107,25 @@ } }, - _computeAuthor: function(message) { + _computeAuthor(message) { return message.author || message.updated_by; }, - _computeShowAvatar: function(author, config) { + _computeShowAvatar(author, config) { return !!(author && config && config.plugin && config.plugin.has_avatars); }, - _computeShowReplyButton: function(message, loggedIn) { + _computeShowOnBehalfOf(message) { + const author = message.author || message.updated_by; + return !!(author && message.real_author && + author._account_id != message.real_author._account_id); + }, + + _computeShowReplyButton(message, loggedIn) { return !!message.message && loggedIn; }, - _computeExpanded: function(expanded) { + _computeExpanded(expanded) { return expanded; }, @@ -120,54 +134,86 @@ * should be true or not, then _expanded is set to true if there are * inline comments (otherwise false). */ - _commentsChanged: function(value) { + _commentsChanged(value) { if (this.message && this.message.expanded === undefined) { this.set('message.expanded', Object.keys(value || {}).length > 0); } }, - _handleTap: function(e) { + _handleTap(e) { if (this.message.expanded) { return; } e.stopPropagation(); this.set('message.expanded', true); }, - _handleNameTap: function(e) { + _handleAuthorTap(e) { if (!this.message.expanded) { return; } e.stopPropagation(); this.set('message.expanded', false); }, - _computeIsAutomated: function(message) { + _computeIsAutomated(message) { return !!(message.reviewer || - (message.tag && message.tag.indexOf('autogenerated') === 0)); + message.type === 'REVIEWER_UPDATE' || + (message.tag && message.tag.startsWith('autogenerated'))); }, - _computeIsHidden: function(hideAutomated, isAutomated) { + _computeIsHidden(hideAutomated, isAutomated) { return hideAutomated && isAutomated; }, - _computeIsReviewerUpdate: function(event) { + _computeIsReviewerUpdate(event) { return event.type === 'REVIEWER_UPDATE'; }, - _computeClass: function(expanded, showAvatar) { - var classes = []; + _isMessagePositive(message) { + if (!message.message) { return null; } + const line = message.message.split('\n', 1)[0]; + const patchSetPrefix = PATCH_SET_PREFIX_PATTERN; + if (!line.match(patchSetPrefix)) { return null;} + const scoresRaw = line.split(patchSetPrefix)[1]; + if (!scoresRaw) { return null; } + const scores = scoresRaw.split(' '); + if (!scores.length) { return null; } + const {min, max} = scores + .map(s => s.match(LABEL_TITLE_SCORE_PATTERN)) + .filter(ms => ms && ms.length === 3) + .filter(([, label]) => !CI_LABELS.includes(label)) + .map(([, , score]) => score) + .map(s => parseInt(s, 10)) + .reduce(({min, max}, s) => + ({min: (s < min ? s : min), max: (s > max ? s : max)}), + {min: 0, max: 0}); + if (max - min === 0) { + return 0; + } else { + return (max + min) > 0 ? 1 : -1; + } + }, + + _computeClass(expanded, showAvatar, message) { + const classes = []; classes.push(expanded ? 'expanded' : 'collapsed'); classes.push(showAvatar ? 'showAvatar' : 'hideAvatar'); + const scoreQuality = this._isMessagePositive(message); + if (scoreQuality === 1) { + classes.push('positiveVote'); + } else if (scoreQuality === -1) { + classes.push('negativeVote'); + } return classes.join(' '); }, - _computeMessageHash: function(message) { + _computeMessageHash(message) { return '#message-' + message.id; }, - _handleLinkTap: function(e) { + _handleLinkTap(e) { e.preventDefault(); this.fire('scroll-to', {message: this.message}, {bubbles: false}); - var hash = this._computeMessageHash(this.message); + const hash = this._computeMessageHash(this.message); // Don't add the hash to the window history if it's already there. // Otherwise you mess up expected back button behavior. if (window.location.hash == hash) { return; } @@ -176,7 +222,7 @@ page.show(window.location.pathname + hash, null, false); }, - _handleReplyTap: function(e) { + _handleReplyTap(e) { e.preventDefault(); this.fire('reply', {message: this.message}); },
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html index 89c7173..07f13dc 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-message.html"> <script>void(0);</script> @@ -33,30 +32,32 @@ </test-fixture> <script> - suite('gr-message tests', function() { - var element; + suite('gr-message tests', () => { + let element; - setup(function() { + setup(done => { stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(false); }, + getLoggedIn() { return Promise.resolve(false); }, + getConfig() { return Promise.resolve({}); }, }); element = fixture('basic'); + flush(done); }); - test('reply event', function(done) { + test('reply event', done => { element.message = { - 'id': '47c43261_55aa2c41', - 'author': { - '_account_id': 1115495, - 'name': 'Andrew Bonventre', - 'email': 'andybons@chromium.org', + id: '47c43261_55aa2c41', + author: { + _account_id: 1115495, + name: 'Andrew Bonventre', + email: 'andybons@chromium.org', }, - 'date': '2016-01-12 20:24:49.448000000', - 'message': 'Uploaded patch set 1.', - '_revision_number': 1 + date: '2016-01-12 20:24:49.448000000', + message: 'Uploaded patch set 1.', + _revision_number: 1, }; - element.addEventListener('reply', function(e) { + element.addEventListener('reply', e => { assert.deepEqual(e.detail.message, element.message); done(); }); @@ -64,38 +65,38 @@ MockInteractions.tap(element.$$('.replyContainer gr-button')); }); - test('reviewer update', function() { - var author = { + test('reviewer update', () => { + const author = { _account_id: 1115495, name: 'Andrew Bonventre', email: 'andybons@chromium.org', }; - var reviewer = { + const reviewer = { _account_id: 123456, name: 'Foo Bar', email: 'barbar@chromium.org', }; element.message = { id: 0xDEADBEEF, - author: author, - reviewer: reviewer, + author, + reviewer, date: '2016-01-12 20:24:49.448000000', type: 'REVIEWER_UPDATE', updates: [ { message: 'Added to CC:', reviewers: [reviewer], - } + }, ], }; flushAsynchronousOperations(); - var content = element.$$('.contentContainer'); + const content = element.$$('.contentContainer'); assert.isOk(content); assert.strictEqual(element.$$('gr-account-chip').account, reviewer); - assert.equal(author.name, element.$$('.name').textContent); + assert.equal(author.name, element.$$('.author > .name').textContent); }); - test('autogenerated prefix hiding', function() { + test('autogenerated prefix hiding', () => { element.message = { tag: 'autogenerated:gerrit:test', updated: '2016-01-12 20:24:49.448000000', @@ -109,7 +110,7 @@ assert.isTrue(element.hidden); }); - test('reviewer message treated as autogenerated', function() { + test('reviewer message treated as autogenerated', () => { element.message = { tag: 'autogenerated:gerrit:test', updated: '2016-01-12 20:24:49.448000000', @@ -124,7 +125,22 @@ assert.isTrue(element.hidden); }); - test('tag that is not autogenerated prefix does not hide', function() { + test('batch reviewer message treated as autogenerated', () => { + element.message = { + type: 'REVIEWER_UPDATE', + updated: '2016-01-12 20:24:49.448000000', + reviewer: {}, + }; + + assert.isTrue(element.isAutomated); + assert.isFalse(element.hidden); + + element.hideAutomated = true; + + assert.isTrue(element.hidden); + }); + + test('tag that is not autogenerated prefix does not hide', () => { element.message = { tag: 'something', updated: '2016-01-12 20:24:49.448000000', @@ -138,12 +154,62 @@ assert.isFalse(element.hidden); }); - test('reply button hidden unless logged in', function() { - var message = { - 'message': 'Uploaded patch set 1.', + test('reply button hidden unless logged in', () => { + const message = { + message: 'Uploaded patch set 1.', }; assert.isFalse(element._computeShowReplyButton(message, false)); assert.isTrue(element._computeShowReplyButton(message, true)); }); + + test('_computeShowOnBehalfOf', () => { + const message = { + message: '...', + }; + assert.isNotOk(element._computeShowOnBehalfOf(message)); + message.author = {_account_id: 1115495}; + assert.isNotOk(element._computeShowOnBehalfOf(message)); + message.real_author = {_account_id: 1115495}; + assert.isNotOk(element._computeShowOnBehalfOf(message)); + message.real_author._account_id = 123456; + assert.isOk(element._computeShowOnBehalfOf(message)); + message.updated_by = message.author; + delete message.author; + assert.isOk(element._computeShowOnBehalfOf(message)); + delete message.updated_by; + assert.isNotOk(element._computeShowOnBehalfOf(message)); + }); + + ['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'].forEach(label => { + test(`${label} ignored for color voting`, () => { + element.message = { + author: {}, + expanded: false, + message: `Patch Set 1: ${label}+1`, + }; + assert.isNotOk( + Polymer.dom(element.root).querySelector('.negativeVote')); + assert.isNotOk( + Polymer.dom(element.root).querySelector('.positiveVote')); + }); + }); + + test('negative vote', () => { + element.message = { + author: {}, + expanded: false, + message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Ready+1', + }; + assert.isOk(Polymer.dom(element.root).querySelector('.negativeVote')); + }); + + test('positive vote', () => { + element.message = { + author: {}, + expanded: false, + message: 'Patch Set 1: Verified-1 Code-Review+2 Trybot-Ready-1', + }; + assert.isOk(Polymer.dom(element.root).querySelector('.positiveVote')); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html index 14361f4..95ca60e 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.html
@@ -18,10 +18,11 @@ <link rel="import" href="../../core/gr-reporting/gr-reporting.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../gr-message/gr-message.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-messages-list"> <template> - <style> + <style include="shared-styles"> :host, .messageListControls { display: block; @@ -32,8 +33,7 @@ margin-bottom: .35em; } .header, - #messageControlsContainer, - gr-message { + #messageControlsContainer { padding: 0 var(--default-horizontal-margin); } .highlighted { @@ -77,11 +77,14 @@ <gr-button id="oldMessagesBtn" link on-tap="_handleShowAllTap"> [[_computeNumMessagesText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]] </gr-button> - / - <gr-button id="incrementMessagesBtn" link - on-tap="_handleIncrementShownMessages"> - [[_computeIncrementText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]] - </gr-button> + <span + hidden$="[[_computeIncrementHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]"> + / + <gr-button id="incrementMessagesBtn" link + on-tap="_handleIncrementShownMessages"> + [[_computeIncrementText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]] + </gr-button> + </span> </span> <template is="dom-repeat"
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js index 0d58d96..ebdb4dc 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -14,10 +14,10 @@ (function() { 'use strict'; - var MAX_INITIAL_SHOWN_MESSAGES = 20; - var MESSAGES_INCREMENT = 5; + const MAX_INITIAL_SHOWN_MESSAGES = 20; + const MESSAGES_INCREMENT = 5; - var ReportingEvent = { + const ReportingEvent = { SHOW_ALL: 'show-all-messages', SHOW_MORE: 'show-more-messages', }; @@ -29,11 +29,11 @@ changeNum: Number, messages: { type: Array, - value: function() { return []; }, + value() { return []; }, }, reviewerUpdates: { type: Array, - value: function() { return []; }, + value() { return []; }, }, comments: Object, projectConfig: Object, @@ -64,34 +64,35 @@ */ _visibleMessages: { type: Array, - value: function() { return []; }, + value() { return []; }, }, }, - scrollToMessage: function(messageID) { - var el = this.$$('[data-message-id="' + messageID + '"]'); + scrollToMessage(messageID) { + let el = this.$$('[data-message-id="' + messageID + '"]'); // If the message is hidden, expand the hidden messages back to that // point. if (!el) { - for (var index = 0; index < this._processedMessages.length; index++) { + let index; + for (index = 0; index < this._processedMessages.length; index++) { if (this._processedMessages[index].id === messageID) { break; } } if (index === this._processedMessages.length) { return; } - var newMessages = this._processedMessages.slice(index, + const newMessages = this._processedMessages.slice(index, -this._visibleMessages.length); // Add newMessages to the beginning of _visibleMessages. - this.splice.apply(this, ['_visibleMessages', 0, 0].concat(newMessages)); + this.splice(...['_visibleMessages', 0, 0].concat(newMessages)); // Allow the dom-repeat to stamp. Polymer.dom.flush(); el = this.$$('[data-message-id="' + messageID + '"]'); } el.set('message.expanded', true); - var top = el.offsetTop; - for (var offsetParent = el.offsetParent; + let top = el.offsetTop; + for (let offsetParent = el.offsetParent; offsetParent; offsetParent = offsetParent.offsetParent) { top += offsetParent.offsetTop; @@ -100,20 +101,20 @@ this._highlightEl(el); }, - _isAutomated: function(message) { + _isAutomated(message) { return !!(message.reviewer || - (message.tag && message.tag.indexOf('autogenerated') === 0)); + (message.tag && message.tag.startsWith('autogenerated'))); }, - _computeItems: function(messages, reviewerUpdates) { + _computeItems(messages, reviewerUpdates) { messages = messages || []; reviewerUpdates = reviewerUpdates || []; - var mi = 0; - var ri = 0; - var result = []; - var mDate; - var rDate; - for (var i = 0; i < messages.length; i++) { + let mi = 0; + let ri = 0; + let result = []; + let mDate; + let rDate; + for (let i = 0; i < messages.length; i++) { messages[i]._index = i; } @@ -139,8 +140,8 @@ return result; }, - _expandedChanged: function(exp) { - for (var i = 0; i < this._processedMessages.length; i++) { + _expandedChanged(exp) { + for (let i = 0; i < this._processedMessages.length; i++) { this._processedMessages[i].expanded = exp; if (i < this._visibleMessages.length) { this.set(['_visibleMessages', i, 'expanded'], exp); @@ -148,11 +149,11 @@ } }, - _highlightEl: function(el) { - var highlightedEls = + _highlightEl(el) { + const highlightedEls = Polymer.dom(this.root).querySelectorAll('.highlighted'); - for (var i = 0; i < highlightedEls.length; i++) { - highlightedEls[i].classList.remove('highlighted'); + for (const highlighedEl of highlightedEls) { + highlighedEl.classList.remove('highlighted'); } function handleAnimationEnd() { el.removeEventListener('animationend', handleAnimationEnd); @@ -165,39 +166,40 @@ /** * @param {boolean} expand */ - handleExpandCollapse: function(expand) { + handleExpandCollapse(expand) { this._expanded = expand; }, - _handleExpandCollapseTap: function(e) { + _handleExpandCollapseTap(e) { e.preventDefault(); this.handleExpandCollapse(!this._expanded); }, - _handleAutomatedMessageToggleTap: function(e) { + _handleAutomatedMessageToggleTap(e) { e.preventDefault(); this._hideAutomated = !this._hideAutomated; }, - _handleScrollTo: function(e) { + _handleScrollTo(e) { this.scrollToMessage(e.detail.message.id); }, - _hasAutomatedMessages: function(messages) { - for (var i = 0; messages && i < messages.length; i++) { - if (this._isAutomated(messages[i])) { + _hasAutomatedMessages(messages) { + if (!messages) { return false; } + for (const message of messages) { + if (this._isAutomated(message)) { return true; } } return false; }, - _computeExpandCollapseMessage: function(expanded) { + _computeExpandCollapseMessage(expanded) { return expanded ? 'Collapse all' : 'Expand all'; }, - _computeAutomatedToggleText: function(hideAutomated) { + _computeAutomatedToggleText(hideAutomated) { return hideAutomated ? 'Show all messages' : 'Show comments only'; }, @@ -209,18 +211,18 @@ * @param {!Object} message * @return {!Object} Hash of arrays of comments, filename as key. */ - _computeCommentsForMessage: function(comments, message) { + _computeCommentsForMessage(comments, message) { if (message._index === undefined || !comments || !this.messages) { return []; } - var messages = this.messages || []; - var index = message._index; - var authorId = message.author && message.author._account_id; - var mDate = util.parseDate(message.date).getTime(); + const messages = this.messages || []; + const index = message._index; + const authorId = message.author && message.author._account_id; + const mDate = util.parseDate(message.date).getTime(); // NB: Messages array has oldest messages first. - var nextMDate; + let nextMDate; if (index > 0) { - for (var i = index - 1; i >= 0; i--) { + for (let i = index - 1; i >= 0; i--) { if (messages[i] && messages[i].author && messages[i].author._account_id === authorId) { nextMDate = util.parseDate(messages[i].date).getTime(); @@ -228,15 +230,16 @@ } } } - var msgComments = {}; - for (var file in comments) { - var fileComments = comments[file]; - for (var i = 0; i < fileComments.length; i++) { + const msgComments = {}; + for (const file in comments) { + if (!comments.hasOwnProperty(file)) { continue; } + const fileComments = comments[file]; + for (let i = 0; i < fileComments.length; i++) { if (fileComments[i].author && fileComments[i].author._account_id !== authorId) { continue; } - var cDate = util.parseDate(fileComments[i].updated).getTime(); + const cDate = util.parseDate(fileComments[i].updated).getTime(); if (cDate <= mDate) { if (nextMDate && cDate <= nextMDate) { continue; @@ -255,12 +258,12 @@ * remaining in the list and the number of messages needed to display five * more visible messages in the list. */ - _getDelta: function(visibleMessages, messages, hideAutomated) { - var delta = MESSAGES_INCREMENT; - var msgsRemaining = messages.length - visibleMessages.length; + _getDelta(visibleMessages, messages, hideAutomated) { + let delta = MESSAGES_INCREMENT; + const msgsRemaining = messages.length - visibleMessages.length; if (hideAutomated) { - var counter = 0; - var i; + let counter = 0; + let i; for (i = msgsRemaining; i > 0 && counter < MESSAGES_INCREMENT; i--) { if (!this._isAutomated(messages[i - 1])) { counter++; } } @@ -273,7 +276,7 @@ * Gets the number of messages that would be visible, but do not currently * exist in _visibleMessages. */ - _numRemaining: function(visibleMessages, messages, hideAutomated) { + _numRemaining(visibleMessages, messages, hideAutomated) { if (hideAutomated) { return this._getHumanMessages(messages).length - this._getHumanMessages(visibleMessages).length; @@ -281,20 +284,20 @@ return messages.length - visibleMessages.length; }, - _computeIncrementText: function(visibleMessages, messages, hideAutomated) { - var delta = this._getDelta(visibleMessages, messages, hideAutomated); + _computeIncrementText(visibleMessages, messages, hideAutomated) { + let delta = this._getDelta(visibleMessages, messages, hideAutomated); delta = Math.min( this._numRemaining(visibleMessages, messages, hideAutomated), delta); return 'Show ' + Math.min(MESSAGES_INCREMENT, delta) + ' more'; }, - _getHumanMessages: function(messages) { - return messages.filter(function(msg) { + _getHumanMessages(messages) { + return messages.filter(msg => { return !this._isAutomated(msg); - }.bind(this)); + }); }, - _computeShowHideTextHidden: function(visibleMessages, messages, + _computeShowHideTextHidden(visibleMessages, messages, hideAutomated) { if (hideAutomated) { messages = this._getHumanMessages(messages); @@ -303,29 +306,37 @@ return visibleMessages.length >= messages.length; }, - _handleShowAllTap: function() { + _handleShowAllTap() { this._visibleMessages = this._processedMessages; this.$.reporting.reportInteraction(ReportingEvent.SHOW_ALL); }, - _handleIncrementShownMessages: function() { - var delta = this._getDelta(this._visibleMessages, this._processedMessages, - this._hideAutomated); - var len = this._visibleMessages.length; - var newMessages = this._processedMessages.slice(-(len + delta), -len); + _handleIncrementShownMessages() { + const delta = this._getDelta(this._visibleMessages, + this._processedMessages, this._hideAutomated); + const len = this._visibleMessages.length; + const newMessages = this._processedMessages.slice(-(len + delta), -len); // Add newMessages to the beginning of _visibleMessages - this.splice.apply(this, ['_visibleMessages', 0, 0].concat(newMessages)); + this.splice(...['_visibleMessages', 0, 0].concat(newMessages)); this.$.reporting.reportInteraction(ReportingEvent.SHOW_MORE); }, - _processedMessagesChanged: function(messages) { + _processedMessagesChanged(messages) { this._visibleMessages = messages.slice(-MAX_INITIAL_SHOWN_MESSAGES); }, - _computeNumMessagesText: function(visibleMessages, messages, + _computeNumMessagesText(visibleMessages, messages, hideAutomated) { - var total = this._numRemaining(visibleMessages, messages, hideAutomated); + const total = + this._numRemaining(visibleMessages, messages, hideAutomated); return total === 1 ? 'Show 1 message' : 'Show all ' + total + ' messages'; }, + + _computeIncrementHidden(visibleMessages, messages, + hideAutomated) { + const total = + this._numRemaining(visibleMessages, messages, hideAutomated); + return total <= this._getDelta(visibleMessages, messages, hideAutomated); + }, }); })();
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html index cdca365..c47d63f 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-messages-list.html"> <script>void(0);</script> @@ -34,9 +33,9 @@ <script> - var randomMessage = function(opt_params) { - var params = opt_params || {}; - var author1 = { + const randomMessage = function(opt_params) { + const params = opt_params || {}; + const author1 = { _account_id: 1115495, name: 'Andrew Bonventre', email: 'andybons@chromium.org', @@ -50,24 +49,24 @@ }; }; - var randomAutomated = function(opt_params) { + const randomAutomated = function(opt_params) { return Object.assign({tag: 'autogenerated:gerrit:replace'}, randomMessage(opt_params)); }; - suite('gr-messages-list tests', function() { - var element; - var messages; - var sandbox; + suite('gr-messages-list tests', () => { + let element; + let messages; + let sandbox; - var getMessages = function() { + const getMessages = function() { return Polymer.dom(element.root).querySelectorAll('gr-message'); }; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, - getLoggedIn: function() { return Promise.resolve(false); }, + getConfig() { return Promise.resolve({}); }, + getLoggedIn() { return Promise.resolve(false); }, }); sandbox = sinon.sandbox.create(); element = fixture('basic'); @@ -76,24 +75,24 @@ flushAsynchronousOperations(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('show some old messages', function() { + test('show some old messages', () => { assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); element.messages = _.times(26, randomMessage); flushAsynchronousOperations(); assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); assert.equal(getMessages().length, 20); - assert.equal(element.$.incrementMessagesBtn.innerText, + assert.equal(element.$.incrementMessagesBtn.innerText.trim(), 'Show 5 more'); MockInteractions.tap(element.$.incrementMessagesBtn); flushAsynchronousOperations(); assert.equal(getMessages().length, 25); - assert.equal(element.$.incrementMessagesBtn.innerText, + assert.equal(element.$.incrementMessagesBtn.innerText.trim(), 'Show 1 more'); MockInteractions.tap(element.$.incrementMessagesBtn); flushAsynchronousOperations(); @@ -102,7 +101,7 @@ assert.equal(getMessages().length, 26); }); - test('show all old messages', function() { + test('show all old messages', () => { assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); element.messages = _.times(26, randomMessage); flushAsynchronousOperations(); @@ -117,7 +116,7 @@ assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); }); - test('message count respects automated', function() { + test('message count respects automated', () => { element.messages = _.times(10, randomAutomated) .concat(_.times(11, randomMessage)); flushAsynchronousOperations(); @@ -130,7 +129,7 @@ assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); }); - test('message count still respects non-automated on toggle', function() { + test('message count still respects non-automated on toggle', () => { element.messages = _.times(10, randomMessage) .concat(_.times(11, randomAutomated)); flushAsynchronousOperations(); @@ -144,7 +143,7 @@ assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); }); - test('show all messages respects expand', function() { + test('show all messages respects expand', () => { element.messages = _.times(10, randomAutomated) .concat(_.times(11, randomMessage)); flushAsynchronousOperations(); @@ -152,10 +151,10 @@ MockInteractions.tap(element.$$('#collapse-messages')); // Expand all. flushAsynchronousOperations(); - var messages = getMessages(); + let messages = getMessages(); assert.equal(messages.length, 20); - for (var i = 0; i < messages.length; i++) { - assert.isTrue(messages[i]._expanded); + for (const message of messages) { + assert.isTrue(message._expanded); } MockInteractions.tap(element.$.oldMessagesBtn); @@ -163,12 +162,12 @@ messages = getMessages(); assert.equal(messages.length, 21); - for (var i = 0; i < messages.length; i++) { - assert.isTrue(messages[i]._expanded); + for (const message of messages) { + assert.isTrue(message._expanded); } }); - test('show all messages respects collapse', function() { + test('show all messages respects collapse', () => { element.messages = _.times(10, randomAutomated) .concat(_.times(11, randomMessage)); flushAsynchronousOperations(); @@ -177,10 +176,10 @@ MockInteractions.tap(element.$$('#collapse-messages')); // Collapse all. flushAsynchronousOperations(); - var messages = getMessages(); + let messages = getMessages(); assert.equal(messages.length, 20); - for (var i = 0; i < messages.length; i++) { - assert.isFalse(messages[i]._expanded); + for (const message of messages) { + assert.isFalse(message._expanded); } MockInteractions.tap(element.$.oldMessagesBtn); @@ -188,37 +187,37 @@ messages = getMessages(); assert.equal(messages.length, 21); - for (var i = 0; i < messages.length; i++) { - assert.isFalse(messages[i]._expanded); + for (const message of messages) { + assert.isFalse(message._expanded); } }); - test('expand/collapse all', function() { - var allMessageEls = getMessages(); - for (var i = 0; i < allMessageEls.length; i++) { - allMessageEls[i]._expanded = false; + test('expand/collapse all', () => { + let allMessageEls = getMessages(); + for (const message of allMessageEls) { + message._expanded = false; } MockInteractions.tap(allMessageEls[1]); assert.isTrue(allMessageEls[1]._expanded); MockInteractions.tap(element.$$('#collapse-messages')); allMessageEls = getMessages(); - for (var i = 0; i < allMessageEls.length; i++) { - assert.isTrue(allMessageEls[i]._expanded); + for (const message of allMessageEls) { + assert.isTrue(message._expanded); } MockInteractions.tap(element.$$('#collapse-messages')); allMessageEls = getMessages(); - for (var i = 0; i < allMessageEls.length; i++) { - assert.isFalse(allMessageEls[i]._expanded); + for (const message of allMessageEls) { + assert.isFalse(message._expanded); } }); - test('expand/collapse from external keypress', function() { + test('expand/collapse from external keypress', () => { MockInteractions.tap(element.$$('#collapse-messages')); - var allMessageEls = getMessages(); - for (var i = 0; i < allMessageEls.length; i++) { - assert.isTrue(allMessageEls[i]._expanded); + let allMessageEls = getMessages(); + for (const message of allMessageEls) { + assert.isTrue(message._expanded); } // Expand/collapse all text also changes. @@ -227,36 +226,35 @@ MockInteractions.tap(element.$$('#collapse-messages')); allMessageEls = getMessages(); - for (var i = 0; i < allMessageEls.length; i++) { - assert.isFalse(allMessageEls[i]._expanded); + for (const message of allMessageEls) { + assert.isFalse(message._expanded); } // Expand/collapse all text also changes. assert.equal(element.$$('#collapse-messages').textContent.trim(), 'Expand all'); }); - test('hide messages does not appear when no automated messages', - function() { + test('hide messages does not appear when no automated messages', () => { assert.isOk(element.$$('#automatedMessageToggleContainer[hidden]')); }); - test('scroll to message', function() { - var allMessageEls = getMessages(); - for (var i = 0; i < allMessageEls.length; i++) { - allMessageEls[i].set('message.expanded', false); + test('scroll to message', () => { + const allMessageEls = getMessages(); + for (const message of allMessageEls) { + message.set('message.expanded', false); } - var scrollToStub = sandbox.stub(window, 'scrollTo'); - var highlightStub = sandbox.stub(element, '_highlightEl'); + const scrollToStub = sandbox.stub(window, 'scrollTo'); + const highlightStub = sandbox.stub(element, '_highlightEl'); element.scrollToMessage('invalid'); - for (var i = 0; i < allMessageEls.length; i++) { - assert.isFalse(allMessageEls[i]._expanded, - 'expected gr-message ' + i + ' to not be expanded'); + for (const message of allMessageEls) { + assert.isFalse(message._expanded, + 'expected gr-message to not be expanded'); } - var messageID = messages[1].id; + const messageID = messages[1].id; element.scrollToMessage(messageID); assert.isTrue( element.$$('[data-message-id="' + messageID + '"]')._expanded); @@ -265,15 +263,15 @@ assert.isTrue(highlightStub.calledOnce); }); - test('scroll to message offscreen', function() { - var scrollToStub = sandbox.stub(window, 'scrollTo'); - var highlightStub = sandbox.stub(element, '_highlightEl'); + test('scroll to message offscreen', () => { + const scrollToStub = sandbox.stub(window, 'scrollTo'); + const highlightStub = sandbox.stub(element, '_highlightEl'); element.messages = _.times(25, randomMessage); flushAsynchronousOperations(); assert.isFalse(scrollToStub.called); assert.isFalse(highlightStub.called); - var messageID = element.messages[1].id; + const messageID = element.messages[1].id; element.scrollToMessage(messageID); assert.isTrue(scrollToStub.calledOnce); assert.isTrue(highlightStub.calledOnce); @@ -282,13 +280,13 @@ element.$$('[data-message-id="' + messageID + '"]')._expanded); }); - test('messages', function() { - var author = { + test('messages', () => { + const author = { _account_id: 42, name: 'Marvin the Paranoid Android', email: 'marvin@sirius.org', }; - var comments = { + const comments = { file1: [ { message: 'message text', @@ -309,7 +307,7 @@ line: 42, id: '450a935e_0f1c05db', patch_set: 2, - author: author, + author, }, { message: 'message text', @@ -318,7 +316,7 @@ line: 62, id: '6505d749_10ed44b2', patch_set: 2, - author: author, + author, }, ], file2: [ @@ -329,18 +327,18 @@ line: 132, id: '450a935e_4f260d25', patch_set: 2, - author: author, + author, }, ], }; - var messages = [].concat( + const messages = [].concat( randomMessage(), { _index: 5, _revision_number: 4, message: 'Uploaded patch set 4.', date: '2016-09-28 13:36:33.000000000', - author: author, + author, id: '8c19ccc949c6d482b061be6a28e10782abf0e7af', }, { @@ -348,18 +346,18 @@ _revision_number: 4, message: 'Patch Set 4:\n\n(6 comments)', date: '2016-09-28 13:36:33.000000000', - author: author, + author, id: 'e7bfdbc842f6b6d8064bc68e0f52b673f40c0ca5', } ); element.comments = comments; element.messages = messages; - var isAuthor = function(author, message) { + const isAuthor = function(author, message) { return message.author._account_id === author._account_id; }; - var isMarvin = isAuthor.bind(null, author); + const isMarvin = isAuthor.bind(null, author); flushAsynchronousOperations(); - var messageElements = getMessages(); + const messageElements = getMessages(); assert.equal(messageElements.length, messages.length); assert.deepEqual(messageElements[1].message, messages[1]); assert.deepEqual(messageElements[2].message, messages[2]); @@ -370,8 +368,8 @@ assert.deepEqual(messageElements[2].comments, {}); }); - test('messages without author do not throw', function() { - var comments = { + test('messages without author do not throw', () => { + const comments = { file1: [ { message: 'message text', @@ -386,7 +384,7 @@ }, }, ]}; - var messages = [{ + const messages = [{ _index: 5, _revision_number: 4, message: 'Uploaded patch set 4.', @@ -396,31 +394,43 @@ element.messages = messages; element.comments = comments; flushAsynchronousOperations(); - var messageEls = getMessages(); + const messageEls = getMessages(); assert.equal(messageEls.length, 1); assert.equal(messageEls[0].message.message, messages[0].message); }); + + test('hide increment text if increment >= total remaining', () => { + // Test with stubbed return values, as _numRemaining and _getDelta have + // their own tests. + sandbox.stub(element, '_getDelta').returns(5); + const remainingStub = sandbox.stub(element, '_numRemaining').returns(6); + assert.isFalse(element._computeIncrementHidden(null, null, null)); + remainingStub.restore(); + + sandbox.stub(element, '_numRemaining').returns(4); + assert.isTrue(element._computeIncrementHidden(null, null, null)); + }); }); - suite('gr-messages-list automate tests', function() { - var element; - var messages; + suite('gr-messages-list automate tests', () => { + let element; + let messages; - var getMessages = function() { + const getMessages = function() { return Polymer.dom(element.root).querySelectorAll('gr-message'); }; - var getHiddenMessages = function() { + const getHiddenMessages = function() { return Polymer.dom(element.root).querySelectorAll('gr-message[hidden]'); }; - var randomMessageReviewer = { + const randomMessageReviewer = { reviewer: {}, }; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, - getLoggedIn: function() { return Promise.resolve(false); }, + getConfig() { return Promise.resolve({}); }, + getLoggedIn() { return Promise.resolve(false); }, }); element = fixture('basic'); messages = _.times(2, randomAutomated); @@ -429,19 +439,19 @@ flushAsynchronousOperations(); }); - test('hide autogenerated button is not hidden', function() { + test('hide autogenerated button is not hidden', () => { assert.isNotOk(element.$$('#automatedMessageToggle[hidden]')); }); - test('autogenerated messages are not hidden initially', function() { - var allHiddenMessageEls = getHiddenMessages(); + test('autogenerated messages are not hidden initially', () => { + const allHiddenMessageEls = getHiddenMessages(); - //There are no hidden messages. + // There are no hidden messages. assert.isFalse(!!allHiddenMessageEls.length); }); - test('autogenerated messages hidden after hide button tap', function() { - var allHiddenMessageEls = getHiddenMessages(); + test('autogenerated messages hidden after hide button tap', () => { + let allHiddenMessageEls = getHiddenMessages(); element._hideAutomated = false; MockInteractions.tap(element.$.automatedMessageToggle); @@ -453,19 +463,19 @@ assert.equal(allHiddenMessageEls.length, allMessageEls.length); }); - test('autogenerated messages not hidden after show button tap', function() { - var allHiddenMessageEls = getHiddenMessages(); + test('autogenerated messages not hidden after show button tap', () => { + let allHiddenMessageEls = getHiddenMessages(); element._hideAutomated = true; MockInteractions.tap(element.$.automatedMessageToggle); allHiddenMessageEls = getHiddenMessages(); - //Autogenerated messages are now hidden. + // Autogenerated messages are now hidden. assert.isFalse(!!allHiddenMessageEls.length); }); - test('_getDelta', function() { - var messages = [randomMessage()]; + test('_getDelta', () => { + let messages = [randomMessage()]; assert.equal(element._getDelta([], messages, false), 1); assert.equal(element._getDelta([], messages, true), 1); @@ -478,18 +488,18 @@ .concat(_.times(2, randomAutomated)) .concat(_.times(3, randomMessage)); - var dummyArr = _.times(2, randomMessage); + const dummyArr = _.times(2, randomMessage); assert.equal(element._getDelta(dummyArr, messages, false), 5); assert.equal(element._getDelta(dummyArr, messages, true), 7); }); - test('_getHumanMessages', function() { + test('_getHumanMessages', () => { assert.equal( element._getHumanMessages(_.times(5, randomAutomated)).length, 0); assert.equal( element._getHumanMessages(_.times(5, randomMessage)).length, 5); - var messages = _.shuffle(_.times(5, randomMessage) + let messages = _.shuffle(_.times(5, randomMessage) .concat(_.times(5, randomAutomated))); messages = element._getHumanMessages(messages); assert.equal(messages.length, 5);
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html index 8eacd48..bfe0668 100644 --- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html +++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -18,10 +18,11 @@ <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-related-changes-list"> <template> - <style> + <style include="shared-styles"> :host { display: block; } @@ -82,7 +83,7 @@ .mobile { display: none; } - @media screen and (max-width: 50em) { + @media screen and (max-width: 60em) { .mobile { display: block; } @@ -94,8 +95,7 @@ } } </style> - <div hidden$="[[!loading]]">Loading...</div> - <div hidden$="[[loading]]"> + <div> <hr class="mobile"> <section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden> <h4>Relation chain</h4> @@ -105,7 +105,8 @@ as="related"> <div class$="rightIndent [[_computeChangeContainerClass(change, related)]]"> <a href$="[[_computeChangeURL(related._change_number, related._revision_number)]]" - class$="[[_computeLinkClass(related)]]"> + class$="[[_computeLinkClass(related)]]" + title$="[[related.commit.subject]]"> [[related.commit.subject]] </a> <span class$="[[_computeChangeStatusClass(related)]]"> @@ -119,7 +120,8 @@ <template is="dom-repeat" items="[[_submittedTogether]]" as="change"> <div> <a href$="[[_computeChangeURL(change._number)]]" - class$="[[_computeLinkClass(change)]]"> + class$="[[_computeLinkClass(change)]]" + title$="[[change.project]]: [[change.branch]]: [[change.subject]]"> [[change.project]]: [[change.branch]]: [[change.subject]] </a> </div> @@ -130,7 +132,8 @@ <template is="dom-repeat" items="[[_sameTopic]]" as="change"> <div> <a href$="[[_computeChangeURL(change._number)]]" - class$="[[_computeLinkClass(change)]]"> + class$="[[_computeLinkClass(change)]]" + title$="[[change.project]]: [[change.branch]]: [[change.subject]]"> [[change.project]]: [[change.branch]]: [[change.subject]] </a> </div> @@ -141,7 +144,8 @@ <template is="dom-repeat" items="[[_conflicts]]" as="change"> <div> <a href$="[[_computeChangeURL(change._number)]]" - class$="[[_computeLinkClass(change)]]"> + class$="[[_computeLinkClass(change)]]" + title$="[[change.subject]]"> [[change.subject]] </a> </div> @@ -152,14 +156,16 @@ <template is="dom-repeat" items="[[_cherryPicks]]" as="change"> <div> <a href$="[[_computeChangeURL(change._number)]]" - class$="[[_computeLinkClass(change)]]"> + class$="[[_computeLinkClass(change)]]" + title$="[[change.branch]]: [[change.subject]]"> [[change.branch]]: [[change.subject]] </a> </div> </template> </section> </div> + <div hidden$="[[!loading]]">Loading...</div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template> <script src="gr-related-changes-list.js"></script> -</dom-module> +</dom-module> \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js index 55a0bce..fe35c7f 100644 --- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js +++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -39,11 +39,26 @@ computed: '_computeConnectedRevisions(change, patchNum, ' + '_relatedResponse.changes)', }, - _relatedResponse: Object, - _submittedTogether: Array, - _conflicts: Array, - _cherryPicks: Array, - _sameTopic: Array, + _relatedResponse: { + type: Object, + value() { return {changes: []}; }, + }, + _submittedTogether: { + type: Array, + value() { return []; }, + }, + _conflicts: { + type: Array, + value() { return []; }, + }, + _cherryPicks: { + type: Array, + value() { return []; }, + }, + _sameTopic: { + type: Array, + value() { return []; }, + }, }, behaviors: [ @@ -56,52 +71,52 @@ '_conflicts, _cherryPicks, _sameTopic)', ], - clear: function() { + clear() { this.loading = true; + this.hidden = true; }, - reload: function() { + reload() { if (!this.change || !this.patchNum) { return Promise.resolve(); } this.loading = true; - var promises = [ - this._getRelatedChanges().then(function(response) { + const promises = [ + this._getRelatedChanges().then(response => { this._relatedResponse = response; this.hasParent = this._calculateHasParent(this.change.change_id, - response.changes); - - }.bind(this)), - this._getSubmittedTogether().then(function(response) { + response.changes); + }), + this._getSubmittedTogether().then(response => { this._submittedTogether = response; - }.bind(this)), - this._getCherryPicks().then(function(response) { + }), + this._getCherryPicks().then(response => { this._cherryPicks = response; - }.bind(this)), + }), ]; // Get conflicts if change is open and is mergeable. if (this.changeIsOpen(this.change.status) && this.change.mergeable) { - promises.push(this._getConflicts().then(function(response) { + promises.push(this._getConflicts().then(response => { this._conflicts = response; - }.bind(this))); + })); } - promises.push(this._getServerConfig().then(function(config) { + promises.push(this._getServerConfig().then(config => { if (this.change.topic && !config.change.submit_whole_topic) { - return this._getChangesWithSameTopic().then(function(response) { + return this._getChangesWithSameTopic().then(response => { this._sameTopic = response; - }.bind(this)); + }); } else { this._sameTopic = []; } return this._sameTopic; - }.bind(this))); + })); - return Promise.all(promises).then(function() { + return Promise.all(promises).then(() => { this.loading = false; - }.bind(this)); + }); }, /** @@ -112,62 +127,62 @@ * @param {Array} relatedChanges * @return {Boolean} */ - _calculateHasParent: function(currentChangeId, relatedChanges) { + _calculateHasParent(currentChangeId, relatedChanges) { return relatedChanges.length > 0 && relatedChanges[relatedChanges.length - 1].change_id !== currentChangeId; }, - _getRelatedChanges: function() { + _getRelatedChanges() { return this.$.restAPI.getRelatedChanges(this.change._number, this.patchNum); }, - _getSubmittedTogether: function() { + _getSubmittedTogether() { return this.$.restAPI.getChangesSubmittedTogether(this.change._number); }, - _getServerConfig: function() { + _getServerConfig() { return this.$.restAPI.getConfig(); }, - _getConflicts: function() { + _getConflicts() { return this.$.restAPI.getChangeConflicts(this.change._number); }, - _getCherryPicks: function() { + _getCherryPicks() { return this.$.restAPI.getChangeCherryPicks(this.change.project, this.change.change_id, this.change._number); }, - _getChangesWithSameTopic: function() { + _getChangesWithSameTopic() { return this.$.restAPI.getChangesWithSameTopic(this.change.topic); }, - _computeChangeURL: function(changeNum, patchNum) { - var urlStr = this.getBaseUrl() + '/c/' + changeNum; + _computeChangeURL(changeNum, patchNum) { + let urlStr = this.getBaseUrl() + '/c/' + changeNum; if (patchNum != null) { urlStr += '/' + patchNum; } return urlStr; }, - _computeChangeContainerClass: function(currentChange, relatedChange) { - var classes = ['changeContainer']; + _computeChangeContainerClass(currentChange, relatedChange) { + const classes = ['changeContainer']; if (relatedChange.change_id === currentChange.change_id) { classes.push('thisChange'); } return classes.join(' '); }, - _computeLinkClass: function(change) { + _computeLinkClass(change) { if (change.status == this.ChangeStatus.ABANDONED) { return 'strikethrough'; } }, - _computeChangeStatusClass: function(change) { - var classes = ['status']; + _computeChangeStatusClass(change) { + const classes = ['status']; if (change._revision_number != change._current_revision_number) { classes.push('notCurrent'); } else if (this._isIndirectAncestor(change)) { @@ -180,7 +195,7 @@ return classes.join(' '); }, - _computeChangeStatus: function(change) { + _computeChangeStatus(change) { switch (change.status) { case this.ChangeStatus.MERGED: return 'Merged'; @@ -199,41 +214,42 @@ return ''; }, - _resultsChanged: function(related, submittedTogether, conflicts, + _resultsChanged(related, submittedTogether, conflicts, cherryPicks, sameTopic) { - var results = [ + const results = [ related, submittedTogether, conflicts, cherryPicks, - sameTopic + sameTopic, ]; - for (var i = 0; i < results.length; i++) { + for (let i = 0; i < results.length; i++) { if (results[i].length > 0) { this.hidden = false; + this.fire('update', null, {bubbles: false}); return; } } this.hidden = true; }, - _isIndirectAncestor: function(change) { - return this._connectedRevisions.indexOf(change.commit.commit) == -1; + _isIndirectAncestor(change) { + return !this._connectedRevisions.includes(change.commit.commit); }, - _computeConnectedRevisions: function(change, patchNum, relatedChanges) { - var connected = []; - var changeRevision; - for (var rev in change.revisions) { + _computeConnectedRevisions(change, patchNum, relatedChanges) { + const connected = []; + let changeRevision; + for (const rev in change.revisions) { if (change.revisions[rev]._number == patchNum) { changeRevision = rev; } } - var commits = relatedChanges.map(function(c) { return c.commit; }); - var pos = commits.length - 1; + const commits = relatedChanges.map(c => { return c.commit; }); + let pos = commits.length - 1; while (pos >= 0) { - var commit = commits[pos].commit; + const commit = commits[pos].commit; connected.push(commit); if (commit == changeRevision) { break; @@ -241,8 +257,8 @@ pos--; } while (pos >= 0) { - for (var i = 0; i < commits[pos].parents.length; i++) { - if (connected.indexOf(commits[pos].parents[i].commit) != -1) { + for (let i = 0; i < commits[pos].parents.length; i++) { + if (connected.includes(commits[pos].parents[i].commit)) { connected.push(commits[pos].commit); break; }
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html index 78c4cf4..6336433 100644 --- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-related-changes-list.html"> <script>void(0);</script> @@ -33,21 +32,21 @@ </test-fixture> <script> - suite('gr-related-changes-list tests', function() { - var element; - var sandbox; + suite('gr-related-changes-list tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { element = fixture('basic'); sandbox = sinon.sandbox.create(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('connected revisions', function() { - var change = { + test('connected revisions', () => { + const change = { revisions: { 'e3c6d60783bfdec9ebae7dcfec4662360433449e': { _number: 1, @@ -69,18 +68,18 @@ }, '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': { _number: 4, - } - } + }, + }, }; - var patchNum = 7; - var relatedChanges = [ + let patchNum = 7; + let relatedChanges = [ { commit: { commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8', parents: [ { - commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' - } + commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', + }, ], }, }, @@ -89,8 +88,8 @@ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', parents: [ { - commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb' - } + commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', + }, ], }, }, @@ -99,8 +98,8 @@ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', parents: [ { - commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae' - } + commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae', + }, ], }, }, @@ -109,8 +108,8 @@ commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae', parents: [ { - commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907' - } + commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', + }, ], }, }, @@ -119,8 +118,8 @@ commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', parents: [ { - commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce' - } + commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce', + }, ], }, }, @@ -129,14 +128,14 @@ commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce', parents: [ { - commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75' - } + commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75', + }, ], }, - } + }, ]; - var connectedChanges = + let connectedChanges = element._computeConnectedRevisions(change, patchNum, relatedChanges); assert.deepEqual(connectedChanges, [ '613bc4f81741a559c6667ac08d71dcc3348f73ce', @@ -155,8 +154,8 @@ commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8', parents: [ { - commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd' - } + commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', + }, ], }, }, @@ -165,8 +164,8 @@ commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', parents: [ { - commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb' - } + commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', + }, ], }, }, @@ -175,8 +174,8 @@ commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', parents: [ { - commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae' - } + commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae', + }, ], }, }, @@ -185,8 +184,8 @@ commit: 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b', parents: [ { - commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6' - } + commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', + }, ], }, }, @@ -195,8 +194,8 @@ commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', parents: [ { - commit: 'af815dac54318826b7f1fa468acc76349ffc588e' - } + commit: 'af815dac54318826b7f1fa468acc76349ffc588e', + }, ], }, }, @@ -205,11 +204,11 @@ commit: 'af815dac54318826b7f1fa468acc76349ffc588e', parents: [ { - commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c' - } + commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c', + }, ], }, - } + }, ]; connectedChanges = @@ -222,9 +221,9 @@ ]); }); - test('_computeChangeContainerClass', function() { - var change1 = {change_id: 123}; - var change2 = {change_id: 456}; + test('_computeChangeContainerClass', () => { + const change1 = {change_id: 123}; + const change2 = {change_id: 456}; assert.notEqual(element._computeChangeContainerClass( change1, change1).indexOf('thisChange'), -1); @@ -232,26 +231,25 @@ change1, change2).indexOf('thisChange'), -1); }); - suite('get conflicts tests', function() { - var element; - var conflictsStub; + suite('get conflicts tests', () => { + let element; + let conflictsStub; - setup(function() { + setup(() => { element = fixture('basic'); - sandbox.stub(element, '_getRelatedChanges', - function() { - return Promise.resolve({changes: []}); - }); + sandbox.stub(element, '_getRelatedChanges', () => { + return Promise.resolve({changes: []}); + }); sandbox.stub(element, '_getSubmittedTogether', - function() { return Promise.resolve(); }); + () => { return Promise.resolve(); }); sandbox.stub(element, '_getCherryPicks', - function() { return Promise.resolve(); }); + () => { return Promise.resolve(); }); conflictsStub = sandbox.stub(element, '_getConflicts', - function() { return Promise.resolve(); }); + () => { return Promise.resolve(['test data']); }); }); - test('request conflicts if open and mergeable', function() { + test('request conflicts if open and mergeable', () => { element.patchNum = 7; element.change = { change_id: 123, @@ -262,7 +260,7 @@ assert.isTrue(conflictsStub.called); }); - test('does not request conflicts if closed and mergeable', function() { + test('does not request conflicts if closed and mergeable', () => { element.patchNum = 7; element.change = { change_id: 123, @@ -273,7 +271,7 @@ assert.isFalse(conflictsStub.called); }); - test('does not request conflicts if open and not mergeable', function() { + test('does not request conflicts if open and not mergeable', () => { element.patchNum = 7; element.change = { change_id: 123, @@ -284,8 +282,7 @@ assert.isFalse(conflictsStub.called); }); - test('does not request conflicts if closed and not mergeable', - function() { + test('doesnt request conflicts if closed and not mergeable', () => { element.patchNum = 7; element.change = { change_id: 123, @@ -297,9 +294,9 @@ }); }); - test('_calculateHasParent', function() { - var changeId = 123; - var relatedChanges = []; + test('_calculateHasParent', () => { + const changeId = 123; + const relatedChanges = []; assert.equal(element._calculateHasParent(changeId, relatedChanges), false); @@ -311,7 +308,27 @@ relatedChanges.push({change_id: 234}); assert.equal(element._calculateHasParent(changeId, relatedChanges), true); + }); + test('clear hides', () => { + element.loading = false; + element.hidden = false; + element.clear(); + assert.isTrue(element.loading); + assert.isTrue(element.hidden); + }); + + test('update fires', () => { + const updateHandler = sandbox.stub(); + element.addEventListener('update', updateHandler); + + element._resultsChanged([], [], [], [], []); + assert.isTrue(element.hidden); + assert.isFalse(updateHandler.called); + + element._resultsChanged([], [], [], [], ['test']); + assert.isFalse(element.hidden); + assert.isTrue(updateHandler.called); }); }); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html new file mode 100644 index 0000000..e76a9e5 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -0,0 +1,161 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2015 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-reply-dialog</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="../../plugins/gr-plugin-host/gr-plugin-host.html"> +<link rel="import" href="gr-reply-dialog.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-reply-dialog></gr-reply-dialog> + </template> +</test-fixture> + +<test-fixture id="plugin-host"> + <template> + <gr-plugin-host></gr-plugin-host> + </template> +</test-fixture> + +<script> + suite('gr-reply-dialog tests', () => { + let element; + let changeNum; + let patchNum; + + let sandbox; + + const setupElement = element => { + element.change = { + _number: changeNum, + labels: { + 'Verified': { + values: { + '-1': 'Fails', + ' 0': 'No score', + '+1': 'Verified', + }, + default_value: 0, + }, + 'Code-Review': { + values: { + '-2': 'Do not submit', + '-1': 'I would prefer that you didn\'t submit this', + ' 0': 'No score', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved', + }, + all: [{_account_id: 42, value: 0}], + default_value: 0, + }, + }, + }; + element.patchNum = patchNum; + element.permittedLabels = { + 'Code-Review': [ + '-1', + ' 0', + '+1', + ], + 'Verified': [ + '-1', + ' 0', + '+1', + ], + }; + element.serverConfig = {note_db_enabled: true}; + sandbox.stub(element, 'fetchIsLatestKnown', () => Promise.resolve(true)); + }; + + setup(() => { + sandbox = sinon.sandbox.create(); + + changeNum = 42; + patchNum = 1; + + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + getAccount() { return Promise.resolve({_account_id: 42}); }, + }); + + element = fixture('basic'); + setupElement(element); + + // Allow the elements created by dom-repeat to be stamped. + flushAsynchronousOperations(); + }); + + teardown(() => { + Gerrit._pluginsPending = -1; + Gerrit._allPluginsPromise = undefined; + sandbox.restore(); + }); + + test('send blocked when invalid email is supplied to ccs', () => { + const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve()); + // Stub the below function to avoid side effects from the send promise + // resolving. + sandbox.stub(element, '_purgeReviewersPendingRemove'); + + element.$$('#ccs').$.entry.setText('test'); + MockInteractions.tap(element.$$('gr-button.send')); + assert.isFalse(sendStub.called); + flushAsynchronousOperations(); + + element.$$('#ccs').$.entry.setText('test@test.test'); + MockInteractions.tap(element.$$('gr-button.send')); + assert.isTrue(sendStub.called); + }); + + test('lgtm plugin', done => { + const pluginHost = fixture('plugin-host'); + pluginHost.config = { + js_resource_paths: [], + html_resource_paths: [ + new URL('test/plugin.html', window.location.href).toString(), + ], + }; + element = fixture('basic'); + setupElement(element); + const importSpy = + sandbox.spy(element.$$('gr-endpoint-decorator'), '_import'); + Gerrit.awaitPluginsLoaded().then(() => { + Promise.all(importSpy.returnValues).then(() => { + flush(() => { + const textarea = element.$.textarea.getNativeTextarea(); + textarea.value = 'LGTM'; + textarea.dispatchEvent(new CustomEvent('input', {bubbles: true})); + const labelScoreRows = Polymer.dom(element.$.labelScores.root) + .querySelector('gr-label-score-row[name="Code-Review"]'); + const selectedBtn = Polymer.dom(labelScoreRows.root) + .querySelector('gr-button[value="+1"].iron-selected'); + assert.isOk(selectedBtn); + done(); + }); + }); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html index b1f95e6..6aea989 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -14,11 +14,15 @@ limitations under the License. --> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> +<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="../../../bower_components/iron-selector/iron-selector.html"> +<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html"> +<link rel="import" href="../../shared/gr-textarea/gr-textarea.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html"> <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> @@ -26,10 +30,12 @@ <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../../shared/gr-storage/gr-storage.html"> <link rel="import" href="../gr-account-list/gr-account-list.html"> +<link rel="import" href="../gr-label-scores/gr-label-scores.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-reply-dialog"> <template> - <style> + <style include="shared-styles"> :host { display: block; max-height: 90vh; @@ -51,8 +57,7 @@ width: 100%; } .peopleContainer, - .labelsContainer, - .actionsContainer { + .labelsContainer { flex-shrink: 0; } .peopleContainer { @@ -71,6 +76,8 @@ display: flex; flex-wrap: wrap; flex: 1; + max-height: 12em; + overflow-y: auto; } #reviewerConfirmationOverlay { padding: 1em; @@ -86,14 +93,14 @@ font-style: italic; } .textareaContainer { - display: flex; - flex: 1; min-height: 6em; position: relative; } - iron-autogrow-textarea { - padding: 0; - font-family: var(--monospace-font-family); + .textareaContainer, + #textarea, + gr-endpoint-decorator { + display: flex; + width: 100%; } .previewContainer gr-formatted-text { background: #f6f6f6; @@ -101,42 +108,6 @@ overflow-y: scroll; padding: 1em; } - .message { - border: none; - width: 100%; - } - .labelContainer:not(:first-of-type) { - margin-top: .5em; - } - .labelName { - display: inline-block; - margin-right: .5em; - min-width: 7em; - text-align: right; - white-space: nowrap; - width: 25%; - } - .labelMessage { - color: #666; - } - iron-selector { - display: inline-flex; - } - iron-selector > gr-button { - margin-right: .25em; - min-width: 3.5em; - } - iron-selector > gr-button:first-of-type { - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; - } - iron-selector > gr-button:last-of-type { - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - } - iron-selector > gr-button.iron-selected { - background-color: #ddd; - } .draftsContainer { flex: 1; overflow-y: auto; @@ -144,14 +115,24 @@ .draftsContainer h3 { margin-top: .25em; } - .actionsContainer { - display: flex; - justify-content: space-between; - } .action:link, .action:visited { color: #00e; } + #checkingStatusLabel, + #notLatestLabel { + margin-left: 1em; + } + #checkingStatusLabel { + color: #444; + font-style: italic; + } + #notLatestLabel { + color: red; + } + #cancelButton { + float:right; + } @media screen and (max-width: 50em) { :host { max-height: none; @@ -189,8 +170,9 @@ id="ccs" accounts="{{_ccs}}" change="[[change]]" - filter="[[filterReviewerSuggestion]]" + filter="[[filterCCSuggestion]]" pending-confirmation="{{_ccPendingConfirmation}}" + allow-any-input placeholder="Add CC..."> </gr-account-list> </div> @@ -219,17 +201,22 @@ </gr-overlay> </section> <section class="textareaContainer"> - <iron-autogrow-textarea - id="textarea" - class="message" - autocomplete="on" - placeholder="Say something nice..." - disabled="{{disabled}}" - rows="4" - max-rows="15" - bind-value="{{draft}}" - on-bind-value-changed="_handleHeightChanged"> - </iron-autogrow-textarea> + <gr-endpoint-decorator name="reply-text"> + <gr-textarea + id="textarea" + class="message" + autocomplete="on" + placeholder=[[_messagePlaceholder]] + fixed-position-dropdown + hide-border="true" + monospace="true" + disabled="{{disabled}}" + rows="4" + max-rows="15" + text="{{draft}}" + on-bind-value-changed="_handleHeightChanged"> + </gr-textarea> + </gr-endpoint-decorator> </section> <section class="previewContainer"> <label> @@ -242,36 +229,50 @@ config="[[projectConfig.commentlinks]]"></gr-formatted-text> </section> <section class="labelsContainer"> - <template is="dom-repeat" items="[[_labels]]" as="label"> - <div class="labelContainer"> - <span class="labelName">[[label.name]]</span> - <iron-selector data-label$="[[label.name]]" - selected="[[_computeIndexOfLabelValue(change.labels, permittedLabels, label)]]" - hidden$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]"> - <template is="dom-repeat" - items="[[_computePermittedLabelValues(permittedLabels, label.name)]]" - as="value"> - <gr-button has-tooltip data-value$="[[value]]" - title$="[[_computeLabelValueTitle(change.labels, label.name, value)]]">[[value]]</gr-button> - </template> - </iron-selector> - <span class="labelMessage" - hidden$="[[_computeAnyPermittedLabelValues(permittedLabels, label.name)]]"> - You don't have permission to edit this label. - </span> - </div> - </template> + <gr-label-scores + id="labelScores" + account="[[_account]]" + change="[[change]]" + permitted-labels=[[permittedLabels]]></gr-label-scores> </section> <section class="draftsContainer" hidden$="[[_computeHideDraftList(diffDrafts)]]"> - <h3>[[_computeDraftsTitle(diffDrafts)]]</h3> + <div class="includeComments"> + <input type="checkbox" id="includeComments" + checked="{{_includeComments::change}}"> + <label for="includeComments">Publish [[_computeDraftsTitle(diffDrafts)]]</label> + </div> <gr-comment-list + id="commentList" comments="[[diffDrafts]]" change-num="[[change._number]]" project-config="[[projectConfig]]" - patch-num="[[patchNum]]"></gr-comment-list> + patch-num="[[patchNum]]" + hidden$="[[!_includeComments]]"></gr-comment-list> </section> - <section class="actionsContainer"> - <gr-button primary class="action send" on-tap="_sendTapHandler">Send</gr-button> + <section> + <gr-button + primary + disabled="[[_isState(knownLatestState, 'not-latest')]]" + class="action send" + on-tap="_sendTapHandler">[[_sendButtonLabel]]</gr-button> + </gr-button> + <template is="dom-if" if="[[canBeStarted]]"> + <gr-button + disabled="[[_isState(knownLatestState, 'not-latest')]]" + class="action save" + on-tap="_saveTapHandler">Save</gr-button> + </template> + <span + id="checkingStatusLabel" + hidden$="[[!_isState(knownLatestState, 'checking')]]"> + Checking whether patch [[patchNum]] is latest... + </span> + <span + id="notLatestLabel" + hidden$="[[!_isState(knownLatestState, 'not-latest')]]"> + Patch [[patchNum]] is not latest. + <gr-button link on-tap="_reload">Reload</gr-button> + </span> <gr-button id="cancelButton" class="action cancel"
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js index 4c35f38..276e729 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -14,20 +14,26 @@ (function() { 'use strict'; - var STORAGE_DEBOUNCE_INTERVAL_MS = 400; + const STORAGE_DEBOUNCE_INTERVAL_MS = 400; - var FocusTarget = { + const FocusTarget = { ANY: 'any', BODY: 'body', CCS: 'cc', REVIEWERS: 'reviewers', }; - var ReviewerTypes = { + const ReviewerTypes = { REVIEWER: 'REVIEWER', CC: 'CC', }; + const LatestPatchState = { + LATEST: 'latest', + CHECKING: 'checking', + NOT_LATEST: 'not-latest', + }; + Polymer({ is: 'gr-reply-dialog', @@ -50,9 +56,19 @@ * @event autogrow */ + /** + * Fires to show an alert when a send is attempted on the non-latest patch. + * + * @event show-alert + */ + properties: { change: Object, patchNum: String, + canBeStarted: { + type: Boolean, + value: false, + }, disabled: { type: Boolean, value: false, @@ -70,13 +86,24 @@ diffDrafts: Object, filterReviewerSuggestion: { type: Function, - value: function() { - return this._filterReviewerSuggestion.bind(this); + value() { + return this._filterReviewerSuggestionGenerator(false); + }, + }, + filterCCSuggestion: { + type: Function, + value() { + return this._filterReviewerSuggestionGenerator(true); }, }, permittedLabels: Object, serverConfig: Object, projectConfig: Object, + knownLatestState: String, + underReview: { + type: Boolean, + value: true, + }, _account: Object, _ccs: Array, @@ -84,12 +111,16 @@ type: Object, observer: '_reviewerPendingConfirmationUpdated', }, - _labels: { - type: Array, - computed: '_computeLabels(change.labels.*, _account)', + _messagePlaceholder: { + type: String, + computed: '_computeMessagePlaceholder(canBeStarted)', }, _owner: Object, _pendingConfirmationDetails: Object, + _includeComments: { + type: Boolean, + value: true, + }, _reviewers: Array, _reviewerPendingConfirmation: { type: Object, @@ -107,80 +138,130 @@ REVIEWER: [], }, }, + _sendButtonLabel: { + type: String, + computed: '_computeSendButtonLabel(canBeStarted)', + }, + _ccsEnabled: { + type: Boolean, + computed: '_computeCCsEnabled(serverConfig)', + }, }, - FocusTarget: FocusTarget, + FocusTarget, behaviors: [ + Gerrit.BaseUrlBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, Gerrit.RESTClientBehavior, ], + keyBindings: { + esc: '_handleEscKey', + }, + observers: [ '_changeUpdated(change.reviewers.*, change.owner, serverConfig)', '_ccsChanged(_ccs.splices)', '_reviewersChanged(_reviewers.splices)', ], - attached: function() { - this._getAccount().then(function(account) { + attached() { + this._getAccount().then(account => { this._account = account || {}; - }.bind(this)); + }); }, - ready: function() { + ready() { this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this); }, - open: function(opt_focusTarget) { + open(opt_focusTarget) { + this.knownLatestState = LatestPatchState.CHECKING; + this.fetchIsLatestKnown(this.change, this.$.restAPI) + .then(isUpToDate => { + this.knownLatestState = isUpToDate ? + LatestPatchState.LATEST : LatestPatchState.NOT_LATEST; + }); + this._focusOn(opt_focusTarget); if (!this.draft || !this.draft.length) { this.draft = this._loadStoredDraft(); } }, - focus: function() { + focus() { this._focusOn(FocusTarget.ANY); }, - getFocusStops: function() { + getFocusStops() { return { start: this.$.reviewers.focusStart, end: this.$.cancelButton, }; }, - setLabelValue: function(label, value) { - var selectorEl = this.$$('iron-selector[data-label="' + label + '"]'); - // The selector may not be present if it’s not at the latest patch set. + setLabelValue(label, value) { + const selectorEl = + this.$.labelScores.$$(`gr-label-score-row[name="${label}"]`); if (!selectorEl) { return; } - var item = selectorEl.$$('gr-button[data-value="' + value + '"]'); - if (!item) { return; } - selectorEl.selectIndex(selectorEl.indexOf(item)); + selectorEl.setSelectedValue(value); }, - _ccsChanged: function(splices) { + getLabelValue(label) { + const selectorEl = + this.$.labelScores.$$(`gr-label-score-row[name="${label}"]`); + if (!selectorEl) { return null; } + + return selectorEl.selectedValue; + }, + + _handleEscKey(e) { + this.cancel(); + }, + + _ccsChanged(splices) { if (splices && splices.indexSplices) { this._processReviewerChange(splices.indexSplices, ReviewerTypes.CC); } }, - _reviewersChanged: function(splices) { + _reviewersChanged(splices) { if (splices && splices.indexSplices) { this._processReviewerChange(splices.indexSplices, ReviewerTypes.REVIEWER); + let key; + let index; + let account; + // Remove any accounts that already exist as a CC. + for (const splice of splices.indexSplices) { + for (const addedKey of splice.addedKeys) { + account = this.get(`_reviewers.${addedKey}`); + key = this._accountOrGroupKey(account); + index = this._ccs.findIndex( + account => this._accountOrGroupKey(account) === key); + if (index >= 0) { + this.splice('_ccs', index, 1); + const message = (account.name || account.email || key) + + ' moved from CC to reviewer.'; + this.fire('show-alert', {message}); + } + } + } } }, - _processReviewerChange: function(indexSplices, type) { - indexSplices.forEach(function(splice) { - splice.removed.forEach(function(account) { + _processReviewerChange(indexSplices, type) { + for (const splice of indexSplices) { + for (const account of splice.removed) { if (!this._reviewersPendingRemove[type]) { console.err('Invalid type ' + type + ' for reviewer.'); return; } this._reviewersPendingRemove[type].push(account); - }.bind(this)); - }.bind(this)); + } + } }, /** @@ -191,15 +272,14 @@ * @param {Object} opt_accountIdsTransferred map of account IDs that must * not be removed, because they have been readded in another state. */ - _purgeReviewersPendingRemove: function(isCancel, - opt_accountIdsTransferred) { - var reviewerArr; - var keep = opt_accountIdsTransferred || {}; - for (var type in this._reviewersPendingRemove) { + _purgeReviewersPendingRemove(isCancel, opt_accountIdsTransferred) { + let reviewerArr; + const keep = opt_accountIdsTransferred || {}; + for (const type in this._reviewersPendingRemove) { if (this._reviewersPendingRemove.hasOwnProperty(type)) { if (!isCancel) { reviewerArr = this._reviewersPendingRemove[type]; - for (var i = 0; i < reviewerArr.length; i++) { + for (let i = 0; i < reviewerArr.length; i++) { if (!keep[reviewerArr[i]._account_id]) { this._removeAccount(reviewerArr[i], type); } @@ -217,118 +297,108 @@ * @param {Object} account * @param {ReviewerTypes} type */ - _removeAccount: function(account, type) { + _removeAccount(account, type) { if (account._pendingAdd) { return; } return this.$.restAPI.removeChangeReviewer(this.change._number, - account._account_id).then(function(response) { - if (!response.ok) { return response; } + account._account_id).then(response => { + if (!response.ok) { return response; } - var reviewers = this.change.reviewers[type] || []; - for (var i = 0; i < reviewers.length; i++) { - if (reviewers[i]._account_id == account._account_id) { - this.splice(['change', 'reviewers', type], i, 1); - break; - } - } - }.bind(this)); + const reviewers = this.change.reviewers[type] || []; + for (let i = 0; i < reviewers.length; i++) { + if (reviewers[i]._account_id == account._account_id) { + this.splice(['change', 'reviewers', type], i, 1); + break; + } + } + }); }, - _mapReviewer: function(reviewer) { - var reviewerId; - var confirmed; + _mapReviewer(reviewer) { + let reviewerId; + let confirmed; if (reviewer.account) { - reviewerId = reviewer.account._account_id; + reviewerId = reviewer.account._account_id || reviewer.account.email; } else if (reviewer.group) { reviewerId = reviewer.group.id; confirmed = reviewer.group.confirmed; } - return {reviewer: reviewerId, confirmed: confirmed}; + return {reviewer: reviewerId, confirmed}; }, - send: function() { - var obj = { - drafts: 'PUBLISH_ALL_REVISIONS', - labels: {}, + send(includeComments) { + if (this.knownLatestState === 'not-latest') { + this.fire('show-alert', + {message: 'Cannot reply to non-latest patch.'}); + return; + } + + const labels = this.$.labelScores.getLabelValues(); + + const obj = { + drafts: includeComments ? 'PUBLISH_ALL_REVISIONS' : 'KEEP', + labels, }; - for (var label in this.permittedLabels) { - if (!this.permittedLabels.hasOwnProperty(label)) { continue; } - - var selectorEl = this.$$('iron-selector[data-label="' + label + '"]'); - - // The user may have not voted on this label. - if (!selectorEl || !selectorEl.selectedItem) { continue; } - - var selectedVal = selectorEl.selectedItem.getAttribute('data-value'); - selectedVal = parseInt(selectedVal, 10); - - // Only send the selection if the user changed it. - var prevVal = this._getVoteForAccount(this.change.labels, label, - this._account); - if (prevVal !== null) { - prevVal = parseInt(prevVal, 10); - } - if (selectedVal !== prevVal) { - obj.labels[label] = selectedVal; - } - } if (this.draft != null) { obj.message = this.draft; } - var accountAdditions = {}; - obj.reviewers = this.$.reviewers.additions().map(function(reviewer) { + const accountAdditions = {}; + obj.reviewers = this.$.reviewers.additions().map(reviewer => { if (reviewer.account) { accountAdditions[reviewer.account._account_id] = true; } return this._mapReviewer(reviewer); - }.bind(this)); - if (this.serverConfig.note_db_enabled) { - this.$$('#ccs').additions().forEach(function(reviewer) { + }); + const ccsEl = this.$$('#ccs'); + if (ccsEl) { + for (let reviewer of ccsEl.additions()) { if (reviewer.account) { accountAdditions[reviewer.account._account_id] = true; } reviewer = this._mapReviewer(reviewer); reviewer.state = 'CC'; obj.reviewers.push(reviewer); - }.bind(this)); + } } this.disabled = true; - var errFn = this._handle400Error.bind(this); - return this._saveReview(obj, errFn).then(function(response) { + const errFn = this._handle400Error.bind(this); + return this._saveReview(obj, errFn).then(response => { if (!response || !response.ok) { return response; } this.disabled = false; this.draft = ''; + this._includeComments = true; this.fire('send', null, {bubbles: false}); return accountAdditions; - }.bind(this)).catch(function(err) { + }).catch(err => { this.disabled = false; throw err; - }.bind(this)); + }); }, - _focusOn: function(section) { + _focusOn(section) { if (section === FocusTarget.ANY) { section = this._chooseFocusTarget(); } if (section === FocusTarget.BODY) { - var textarea = this.$.textarea; - textarea.async(textarea.textarea.focus.bind(textarea.textarea)); + const textarea = this.$.textarea; + textarea.async(textarea.getNativeTextarea() + .focus.bind(textarea.getNativeTextarea())); } else if (section === FocusTarget.REVIEWERS) { - var reviewerEntry = this.$.reviewers.focusStart; + const reviewerEntry = this.$.reviewers.focusStart; reviewerEntry.async(reviewerEntry.focus); } else if (section === FocusTarget.CCS) { - var ccEntry = this.$$('#ccs').focusStart; + const ccEntry = this.$$('#ccs').focusStart; ccEntry.async(ccEntry.focus); } }, - _chooseFocusTarget: function() { + _chooseFocusTarget() { // If we are the owner and the reviewers field is empty, focus on that. if (this._account && this.change && this.change.owner && this._account._account_id === this.change.owner._account_id && @@ -340,7 +410,7 @@ return FocusTarget.BODY; }, - _handle400Error: function(response) { + _handle400Error(response) { // A call to _saveReview could fail with a server error if erroneous // reviewers were requested. This is signalled with a 400 Bad Request // status. The default gr-rest-api-interface error handling would @@ -356,126 +426,87 @@ if (response.status !== 400) { // This is all restAPI does when there is no custom error handling. - this.fire('server-error', {response: response}); + this.fire('server-error', {response}); return response; } // Process the response body, format a better error message, and fire // an event for gr-event-manager to display. - var jsonPromise = this.$.restAPI.getResponseObject(response); - return jsonPromise.then(function(result) { - var errors = []; - ['reviewers', 'ccs'].forEach(function(state) { - for (var input in result[state]) { - var reviewer = result[state][input]; - if (!!reviewer.error) { + const jsonPromise = this.$.restAPI.getResponseObject(response); + return jsonPromise.then(result => { + const errors = []; + for (const state of ['reviewers', 'ccs']) { + if (!result.hasOwnProperty(state)) { continue; } + for (const reviewer of Object.values(result[state])) { + if (reviewer.error) { errors.push(reviewer.error); } } - }); + } response = { ok: false, status: response.status, - text: function() { return Promise.resolve(errors.join(', ')); }, + text() { return Promise.resolve(errors.join(', ')); }, }; - this.fire('server-error', {response: response}); - }.bind(this)); + this.fire('server-error', {response}); + }); }, - _computeHideDraftList: function(drafts) { + _computeHideDraftList(drafts) { return Object.keys(drafts || {}).length == 0; }, - _computeDraftsTitle: function(drafts) { - var total = 0; - for (var file in drafts) { - total += drafts[file].length; + _computeDraftsTitle(drafts) { + let total = 0; + for (const file in drafts) { + if (drafts.hasOwnProperty(file)) { + total += drafts[file].length; + } } if (total == 0) { return ''; } if (total == 1) { return '1 Draft'; } if (total > 1) { return total + ' Drafts'; } }, - _computeLabelValueTitle: function(labels, label, value) { - return labels[label] && labels[label].values[value]; + _computeMessagePlaceholder(canBeStarted) { + return canBeStarted ? + 'Add a note for your reviewers...' : + 'Say something nice...'; }, - _computeLabels: function(labelRecord) { - var labelsObj = labelRecord.base; - if (!labelsObj) { return []; } - return Object.keys(labelsObj).sort().map(function(key) { - return { - name: key, - value: this._getVoteForAccount(labelsObj, key, this._account), - }; - }.bind(this)); - }, - - _getVoteForAccount: function(labels, labelName, account) { - var votes = labels[labelName]; - if (votes.all && votes.all.length > 0) { - for (var i = 0; i < votes.all.length; i++) { - if (votes.all[i]._account_id == account._account_id) { - return votes.all[i].value; - } - } - } - return null; - }, - - _computeIndexOfLabelValue: function(labels, permittedLabels, label) { - if (!labels[label.name]) { return null; } - var labelValue = label.value; - var len = permittedLabels[label.name] != null ? - permittedLabels[label.name].length : 0; - for (var i = 0; i < len; i++) { - var val = parseInt(permittedLabels[label.name][i], 10); - if (val == labelValue) { - return i; - } - } - return null; - }, - - _computePermittedLabelValues: function(permittedLabels, label) { - return permittedLabels[label]; - }, - - _computeAnyPermittedLabelValues: function(permittedLabels, label) { - return permittedLabels.hasOwnProperty(label); - }, - - _changeUpdated: function(changeRecord, owner, serverConfig) { + _changeUpdated(changeRecord, owner, serverConfig) { this._rebuildReviewerArrays(changeRecord.base, owner, serverConfig); }, - _rebuildReviewerArrays: function(change, owner, serverConfig) { + _rebuildReviewerArrays(change, owner, serverConfig) { this._owner = owner; - var reviewers = []; - var ccs = []; + let reviewers = []; + const ccs = []; - for (var key in change) { - if (key !== 'REVIEWER' && key !== 'CC') { - console.warn('unexpected reviewer state:', key); - continue; + for (const key in change) { + if (change.hasOwnProperty(key)) { + if (key !== 'REVIEWER' && key !== 'CC') { + console.warn('unexpected reviewer state:', key); + continue; + } + for (const entry of change[key]) { + if (entry._account_id === owner._account_id) { + continue; + } + switch (key) { + case 'REVIEWER': + reviewers.push(entry); + break; + case 'CC': + ccs.push(entry); + break; + } + } } - change[key].forEach(function(entry) { - if (entry._account_id === owner._account_id) { - return; - } - switch (key) { - case 'REVIEWER': - reviewers.push(entry); - break; - case 'CC': - ccs.push(entry); - break; - } - }); } - if (serverConfig.note_db_enabled) { + if (this._ccsEnabled) { this._ccs = ccs; } else { this._ccs = []; @@ -484,58 +515,104 @@ this._reviewers = reviewers; }, - _accountOrGroupKey: function(entry) { + _accountOrGroupKey(entry) { return entry.id || entry._account_id; }, - _filterReviewerSuggestion: function(suggestion) { - var entry; - if (suggestion.account) { - entry = suggestion.account; - } else if (suggestion.group) { - entry = suggestion.group; - } else { - console.warn('received suggestion that was neither account nor group:', - suggestion); - } - if (entry._account_id === this._owner._account_id) { - return false; - } + /** + * Generates a function to filter out reviewer/CC entries. When isCCs is + * truthy, the function filters out entries that already exist in this._ccs. + * When falsy, the function filters entries that exist in this._reviewers. + * @param {Boolean} isCCs + * @return {Function} + */ + _filterReviewerSuggestionGenerator(isCCs) { + return suggestion => { + let entry; + if (suggestion.account) { + entry = suggestion.account; + } else if (suggestion.group) { + entry = suggestion.group; + } else { + console.warn( + 'received suggestion that was neither account nor group:', + suggestion); + } + if (entry._account_id === this._owner._account_id) { + return false; + } - var key = this._accountOrGroupKey(entry); - var finder = function(entry) { - return this._accountOrGroupKey(entry) === key; - }.bind(this); - - return this._reviewers.find(finder) === undefined && - this._ccs.find(finder) === undefined; + const key = this._accountOrGroupKey(entry); + const finder = entry => this._accountOrGroupKey(entry) === key; + if (isCCs) { + return this._ccs.find(finder) === undefined; + } + return this._reviewers.find(finder) === undefined; + }; }, - _getAccount: function() { + _getAccount() { return this.$.restAPI.getAccount(); }, - _cancelTapHandler: function(e) { + _cancelTapHandler(e) { e.preventDefault(); + this.cancel(); + }, + + cancel() { this.fire('cancel', null, {bubbles: false}); + this.$.textarea.closeDropdown(); this._purgeReviewersPendingRemove(true); this._rebuildReviewerArrays(this.change.reviewers, this._owner, this.serverConfig); }, - _sendTapHandler: function(e) { + _saveTapHandler(e) { e.preventDefault(); - this.send().then(function(keep) { - this._purgeReviewersPendingRemove(false, keep); - }.bind(this)); + if (this._ccsEnabled && !this.$$('#ccs').submitEntryText()) { + // Do not proceed with the save if there is an invalid email entry in + // the text field of the CC entry. + return; + } + this.send(this._includeComments).then(keepReviewers => { + this._purgeReviewersPendingRemove(false, keepReviewers); + }); }, - _saveReview: function(review, opt_errFn) { + _sendTapHandler(e) { + e.preventDefault(); + if (this._ccsEnabled && !this.$$('#ccs').submitEntryText()) { + // Do not proceed with the send if there is an invalid email entry in + // the text field of the CC entry. + return; + } + if (this.canBeStarted) { + this._startReview().then(() => { + return this.send(this._includeComments); + }).then(keepReviewers => { + this._purgeReviewersPendingRemove(false, keepReviewers); + }); + return; + } + this.send(this._includeComments).then(keepReviewers => { + this._purgeReviewersPendingRemove(false, keepReviewers); + }); + }, + + _saveReview(review, opt_errFn) { return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum, review, opt_errFn); }, - _reviewerPendingConfirmationUpdated: function(reviewer) { + _startReview() { + if (!this.canBeStarted) { + return Promise.resolve(); + } + return this.$.restAPI.startReview(this.change._number); + }, + + _reviewerPendingConfirmationUpdated(reviewer) { if (reviewer === null) { this.$.reviewerConfirmationOverlay.close(); } else { @@ -545,7 +622,7 @@ } }, - _confirmPendingReviewer: function() { + _confirmPendingReviewer() { if (this._ccPendingConfirmation) { this.$$('#ccs').confirmGroup(this._ccPendingConfirmation.group); this._focusOn(FocusTarget.CCS); @@ -555,32 +632,32 @@ } }, - _cancelPendingReviewer: function() { + _cancelPendingReviewer() { this._ccPendingConfirmation = null; this._reviewerPendingConfirmation = null; - var target = + const target = this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS; this._focusOn(target); }, - _getStorageLocation: function() { + _getStorageLocation() { // Tests trigger this method without setting change. if (!this.change) { return {}; } return { changeNum: this.change._number, - patchNum: this.patchNum, + patchNum: '@change', path: '@change', }; }, - _loadStoredDraft: function() { - var draft = this.$.storage.getDraftComment(this._getStorageLocation()); + _loadStoredDraft() { + const draft = this.$.storage.getDraftComment(this._getStorageLocation()); return draft ? draft.message : ''; }, - _draftChanged: function(newDraft, oldDraft) { - this.debounce('store', function() { + _draftChanged(newDraft, oldDraft) { + this.debounce('store', () => { if (!newDraft.length && oldDraft) { // If the draft has been modified to be empty, then erase the storage // entry. @@ -592,11 +669,28 @@ }, STORAGE_DEBOUNCE_INTERVAL_MS); }, - _handleHeightChanged: function(e) { + _handleHeightChanged(e) { // If the textarea resizes, we need to re-fit the overlay. - this.debounce('autogrow', function() { + this.debounce('autogrow', () => { this.fire('autogrow'); }); }, + + _isState(knownLatestState, value) { + return knownLatestState === value; + }, + + _reload() { + // Load the current change without any patch range. + location.href = this.getBaseUrl() + '/c/' + this.change._number; + }, + + _computeSendButtonLabel(canBeStarted) { + return canBeStarted ? 'Start review' : 'Send'; + }, + + _computeCCsEnabled(serverConfig) { + return serverConfig && serverConfig.note_db_enabled; + }, }); })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html index 5aa5848..2b1b42c 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-reply-dialog.html"> <script>void(0);</script> @@ -33,36 +32,36 @@ </test-fixture> <script> - suite('gr-reply-dialog tests', function() { - var element; - var changeNum; - var patchNum; + suite('gr-reply-dialog tests', () => { + let element; + let changeNum; + let patchNum; - var sandbox; - var getDraftCommentStub; - var setDraftCommentStub; - var eraseDraftCommentStub; + let sandbox; + let getDraftCommentStub; + let setDraftCommentStub; + let eraseDraftCommentStub; - var lastId = 0; - var makeAccount = function() { return {_account_id: lastId++}; }; - var makeGroup = function() { return {id: lastId++}; }; + let lastId = 0; + const makeAccount = function() { return {_account_id: lastId++}; }; + const makeGroup = function() { return {id: lastId++}; }; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); changeNum = 42; patchNum = 1; stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, - getAccount: function() { return Promise.resolve({}); }, + getConfig() { return Promise.resolve({}); }, + getAccount() { return Promise.resolve({}); }, }); element = fixture('basic'); element.change = { _number: changeNum, labels: { - Verified: { + 'Verified': { values: { '-1': 'Fails', ' 0': 'No score', @@ -89,7 +88,7 @@ ' 0', '+1', ], - Verified: [ + 'Verified': [ '-1', ' 0', '+1', @@ -102,85 +101,144 @@ eraseDraftCommentStub = sandbox.stub(element.$.storage, 'eraseDraftComment'); + sandbox.stub(element, 'fetchIsLatestKnown', + () => { return Promise.resolve(true); }); + // Allow the elements created by dom-repeat to be stamped. flushAsynchronousOperations(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('changes in label score are reflected in the DOM', function() { - element._account = {_account_id: 1}; - element.set(['change', 'labels', 'Verified', 'all'], - [{_account_id: 1, value: -1}]); - flushAsynchronousOperations(); - var selector = element.$$('iron-selector[data-label="Verified"]'); - assert.equal(selector.selected, 0); // Index 0, value -1 - element.set(['change', 'labels', 'Verified', 'all'], - [{_account_id: 1, value: 1}]); - flushAsynchronousOperations(); - assert.equal(selector.selected, 2); // Index 2, value 1 - }); - - test('cancel event', function(done) { - element.addEventListener('cancel', function() { done(); }); - MockInteractions.tap(element.$$('.cancel')); - }); - - test('label picker', function(done) { - element.revisions = {}; - element.patchNum = ''; - + test('default to publishing drafts with reply', done => { // Async tick is needed because iron-selector content is distributed and // distributed content requires an observer to be set up. // Note: Double flush seems to be needed in Safari. {@see Issue 4963}. - flush(function() { - flush(function() { - for (var label in element.permittedLabels) { - assert.ok(element.$$('iron-selector[data-label="' + label + '"]'), - label); - } + flush(() => { + flush(() => { element.draft = 'I wholeheartedly disapprove'; - MockInteractions.tap(element.$$( - 'iron-selector[data-label="Code-Review"] > ' + - 'gr-button[data-value="-1"]')); - MockInteractions.tap(element.$$( - 'iron-selector[data-label="Verified"] > ' + - 'gr-button[data-value="-1"]')); - var saveReviewStub = sinon.stub(element, '_saveReview', - function(review) { + sandbox.stub(element, '_saveReview', review => { assert.deepEqual(review, { drafts: 'PUBLISH_ALL_REVISIONS', - labels: { - 'Code-Review': -1, - 'Verified': -1, - }, + labels: {}, message: 'I wholeheartedly disapprove', reviewers: [], }); - return Promise.resolve({ok: true}); - }); - - element.addEventListener('send', function() { - assert.isFalse(element.disabled, - 'Element should be enabled when done sending reply.'); - assert.equal(element.draft.length, 0); - saveReviewStub.restore(); + assert.isFalse(element.$.commentList.hidden); done(); + return Promise.resolve({ok: true}); }); // This is needed on non-Blink engines most likely due to the ways in // which the dom-repeat elements are stamped. - flush(function() { + flush(() => { MockInteractions.tap(element.$$('.send')); - assert.isTrue(element.disabled); }); }); }); }); + test('keep drafts with reply', done => { + MockInteractions.tap(element.$$('#includeComments')); + assert.equal(element._includeComments, false); + + // Async tick is needed because iron-selector content is distributed and + // distributed content requires an observer to be set up. + // Note: Double flush seems to be needed in Safari. {@see Issue 4963}. + flush(() => { + flush(() => { + element.draft = 'I wholeheartedly disapprove'; + + sandbox.stub(element, '_saveReview', review => { + assert.deepEqual(review, { + drafts: 'KEEP', + labels: {}, + message: 'I wholeheartedly disapprove', + reviewers: [], + }); + assert.isTrue(element.$.commentList.hidden); + done(); + return Promise.resolve({ok: true}); + }); + + // This is needed on non-Blink engines most likely due to the ways in + // which the dom-repeat elements are stamped. + flush(() => { + MockInteractions.tap(element.$$('.send')); + }); + }); + }); + }); + + test('label picker', done => { + element.draft = 'I wholeheartedly disapprove'; + sandbox.stub(element, '_saveReview', review => { + assert.deepEqual(review, { + drafts: 'PUBLISH_ALL_REVISIONS', + labels: { + 'Code-Review': -1, + 'Verified': -1, + }, + message: 'I wholeheartedly disapprove', + reviewers: [], + }); + return Promise.resolve({ok: true}); + }); + + sandbox.stub(element.$.labelScores, 'getLabelValues', () => { + return { + 'Code-Review': -1, + 'Verified': -1, + }; + }); + + element.addEventListener('send', () => { + assert.isFalse(element.disabled, + 'Element should be enabled when done sending reply.'); + assert.equal(element.draft.length, 0); + done(); + }); + + // This is needed on non-Blink engines most likely due to the ways in + // which the dom-repeat elements are stamped. + flush(() => { + MockInteractions.tap(element.$$('.send')); + assert.isTrue(element.disabled); + }); + }); + + test('getlabelValue returns value', done => { + flush(() => { + element.$$('gr-label-scores').$$(`gr-label-score-row[name="Verified"]`) + .setSelectedValue(-1); + assert.equal('-1', element.getLabelValue('Verified')); + done(); + }); + }); + + test('getlabelValue when no score is selected', done => { + flush(() => { + element.$$('gr-label-scores').$$(`gr-label-score-row[name="Code-Review"]`) + .setSelectedValue(-1); + assert.isNull(element.getLabelValue('Verified')); + done(); + }); + }); + + test('setlabelValue', () => { + element._account = {_account_id: 1}; + flushAsynchronousOperations(); + const label = 'Verified'; + const value = '+1'; + element.setLabelValue(label, value); + flushAsynchronousOperations(); + const labels = element.$.labelScores.getLabelValues(); + assert.deepEqual(labels, {Verified: 1}); + }); + function getActiveElement() { return Polymer.IronOverlayManager.deepActiveElement; } @@ -191,7 +249,7 @@ } function overlayObserver(mode) { - return new Promise(function(resolve) { + return new Promise(resolve => { function listener() { element.removeEventListener('iron-overlay-' + mode, listener); resolve(); @@ -201,9 +259,9 @@ } function testConfirmationDialog(done, cc) { - var yesButton = + const yesButton = element.$$('.reviewerConfirmationButtons gr-button:first-child'); - var noButton = + const noButton = element.$$('.reviewerConfirmationButtons gr-button:last-child'); element.serverConfig = {note_db_enabled: true}; @@ -213,19 +271,19 @@ assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay)); // Cause the confirmation dialog to display. - var observer = overlayObserver('opened'); - var group = { + let observer = overlayObserver('opened'); + const group = { id: 'id', name: 'name', }; if (cc) { element._ccPendingConfirmation = { - group: group, + group, count: 10, }; } else { element._reviewerPendingConfirmation = { - group: group, + group, count: 10, }; } @@ -241,16 +299,16 @@ element._pendingConfirmationDetails); } - observer.then(function() { + observer.then(() => { assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay)); observer = overlayObserver('closed'); - var expected = 'Group name has 10 members'; + const expected = 'Group name has 10 members'; assert.notEqual( element.$.reviewerConfirmationOverlay.innerText.indexOf(expected), -1); MockInteractions.tap(noButton); // close the overlay return observer; - }).then(function() { + }).then(() => { assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay)); // We should be focused on account entry input. @@ -264,24 +322,24 @@ observer = overlayObserver('opened'); if (cc) { element._ccPendingConfirmation = { - group: group, + group, count: 10, }; } else { element._reviewerPendingConfirmation = { - group: group, + group, count: 10, }; } return observer; - }).then(function() { + }).then(() => { assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay)); observer = overlayObserver('closed'); MockInteractions.tap(yesButton); // Confirm the group. return observer; - }).then(function() { + }).then(() => { assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay)); - var additions = cc ? + const additions = cc ? element.$$('#ccs').additions() : element.$.reviewers.additions(); assert.deepEqual( @@ -301,41 +359,41 @@ // We should be focused on account entry input. assert.equal(getActiveElement().id, 'input'); }).then(done); - }; + } - test('cc confirmation', function(done) { + test('cc confirmation', done => { testConfirmationDialog(done, true); }); - test('reviewer confirmation', function(done) { + test('reviewer confirmation', done => { testConfirmationDialog(done, false); }); - test('_getStorageLocation', function() { - var actual = element._getStorageLocation(); + test('_getStorageLocation', () => { + const actual = element._getStorageLocation(); assert.equal(actual.changeNum, changeNum); - assert.equal(actual.patchNum, patchNum); + assert.equal(actual.patchNum, '@change'); assert.equal(actual.path, '@change'); }); - test('gets draft from storage on open', function() { - var storedDraft = 'hello world'; + test('gets draft from storage on open', () => { + const storedDraft = 'hello world'; getDraftCommentStub.returns({message: storedDraft}); element.open(); assert.isTrue(getDraftCommentStub.called); assert.equal(element.draft, storedDraft); }); - test('blank if no stored draft', function() { + test('blank if no stored draft', () => { getDraftCommentStub.returns(null); element.open(); assert.isTrue(getDraftCommentStub.called); assert.equal(element.draft, ''); }); - test('updates stored draft on edits', function() { - var firstEdit = 'hello'; - var location = element._getStorageLocation(); + test('updates stored draft on edits', () => { + const firstEdit = 'hello'; + const location = element._getStorageLocation(); element.draft = firstEdit; element.flushDebouncer('store'); @@ -348,32 +406,33 @@ assert.isTrue(eraseDraftCommentStub.calledWith(location)); }); - test('400 converts to human-readable server-error', function(done) { - sandbox.stub(window, 'fetch', function() { - var text = '....{"reviewers":{"id1":{"error":"first error"}},' + + test('400 converts to human-readable server-error', done => { + sandbox.stub(window, 'fetch', () => { + const text = '....{"reviewers":{"id1":{"error":"first error"}},' + '"ccs":{"id2":{"error":"second error"}}}'; return Promise.resolve({ ok: false, status: 400, - text: function() { return Promise.resolve(text); }, + text() { return Promise.resolve(text); }, }); }); - element.addEventListener('server-error', function(event) { + element.addEventListener('server-error', event => { if (event.target !== element) { return; } - event.detail.response.text().then(function(body) { + event.detail.response.text().then(body => { assert.equal(body, 'first error, second error'); + done(); }); }); // Async tick is needed because iron-selector content is distributed and // distributed content requires an observer to be set up. - flush(function() { element.send().then(done); }); + flush(() => { element.send(); }); }); - test('ccs are displayed if NoteDb is enabled', function() { + test('ccs are displayed if NoteDb is enabled', () => { function hasCc() { flushAsynchronousOperations(); return !!element.$$('#ccs'); @@ -386,34 +445,36 @@ assert.isTrue(hasCc()); }); - test('filterReviewerSuggestion', function() { - var owner = makeAccount(); - var reviewer1 = makeAccount(); - var reviewer2 = makeGroup(); - var cc1 = makeAccount(); - var cc2 = makeGroup(); + test('filterReviewerSuggestion', () => { + const owner = makeAccount(); + const reviewer1 = makeAccount(); + const reviewer2 = makeGroup(); + const cc1 = makeAccount(); + const cc2 = makeGroup(); + let filter = element._filterReviewerSuggestionGenerator(false); element._owner = owner; element._reviewers = [reviewer1, reviewer2]; element._ccs = [cc1, cc2]; - assert.isTrue( - element._filterReviewerSuggestion({account: makeAccount()})); - assert.isTrue(element._filterReviewerSuggestion({group: makeGroup()})); + assert.isTrue(filter({account: makeAccount()})); + assert.isTrue(filter({group: makeGroup()})); // Owner should be excluded. - assert.isFalse(element._filterReviewerSuggestion({account: owner})); + assert.isFalse(filter({account: owner})); - // Existing and pending reviewers should be excluded. - assert.isFalse(element._filterReviewerSuggestion({account: reviewer1})); - assert.isFalse(element._filterReviewerSuggestion({group: reviewer2})); + // Existing and pending reviewers should be excluded when isCC = false. + assert.isFalse(filter({account: reviewer1})); + assert.isFalse(filter({group: reviewer2})); - // Existing and pending CCs should be excluded. - assert.isFalse(element._filterReviewerSuggestion({account: cc1})); - assert.isFalse(element._filterReviewerSuggestion({group: cc2})); + filter = element._filterReviewerSuggestionGenerator(true); + + // Existing and pending CCs should be excluded when isCC = true;. + assert.isFalse(filter({account: cc1})); + assert.isFalse(filter({group: cc2})); }); - test('_chooseFocusTarget', function() { + test('_chooseFocusTarget', () => { element._account = null; assert.strictEqual( element._chooseFocusTarget(), element.FocusTarget.BODY); @@ -440,77 +501,29 @@ element._chooseFocusTarget(), element.FocusTarget.BODY); }); - test('only send labels that have changed', function(done) { - flush(function() { - var saveReviewStub = sinon.stub(element, '_saveReview', - function(review) { + test('only send labels that have changed', done => { + flush(() => { + sandbox.stub(element, '_saveReview', review => { assert.deepEqual(review.labels, {Verified: -1}); return Promise.resolve({ok: true}); }); - element.addEventListener('send', function() { - saveReviewStub.restore(); + element.addEventListener('send', () => { done(); }); // Without wrapping this test in flush(), the below two calls to // MockInteractions.tap() cause a race in some situations in shadow DOM. // The send button can be tapped before the others, causing the test to // fail. - MockInteractions.tap(element.$$( - 'iron-selector[data-label="Verified"] > ' + - 'gr-button[data-value="-1"]')); + + element.$$('gr-label-scores').$$( + 'gr-label-score-row[name="Verified"]').setSelectedValue(-1); MockInteractions.tap(element.$$('.send')); }); }); - test('do not display tooltips on touch devices', function() { - element._account = {_account_id: 1}; - element.set(['change', 'labels', 'Verified', 'all'], - [{_account_id: 1, value: -1}]); - element.labels = { - Verified: { - values: { - '-1': 'Fails', - ' 0': 'No score', - '+1': 'Verified' - }, - default_value: 0, - }, - 'Code-Review': { - values: { - '-2': 'Do not submit', - '-1': 'I would prefer that you didn\'t submit this', - ' 0': 'No score', - '+1': 'Looks good to me, but someone else must approve', - '+2': 'Looks good to me, approved' - }, - default_value: 0, - }, - }; - - flushAsynchronousOperations(); - - var verifiedBtn = element.$$( - 'iron-selector[data-label="Verified"] > ' + - 'gr-button[data-value="-1"]'); - - // On touch devices, tooltips should not be shown. - verifiedBtn._isTouchDevice = true; - verifiedBtn._handleShowTooltip(); - assert.isNotOk(verifiedBtn._tooltip); - verifiedBtn._handleHideTooltip(); - assert.isNotOk(verifiedBtn._tooltip); - - // On other devices, tooltips should be shown. - verifiedBtn._isTouchDevice = false; - verifiedBtn._handleShowTooltip(); - assert.isOk(verifiedBtn._tooltip); - verifiedBtn._handleHideTooltip(); - assert.isNotOk(verifiedBtn._tooltip); - }); - - test('_processReviewerChange', function() { - var mockIndexSplices = function(toRemove) { + test('_processReviewerChange', () => { + const mockIndexSplices = function(toRemove) { return [{ removed: [toRemove], }]; @@ -521,16 +534,16 @@ assert.equal(element._reviewersPendingRemove.REVIEWER.length, 1); }); - test('_purgeReviewersPendingRemove', function() { - var removeStub = sandbox.stub(element, '_removeAccount'); - var mock = function() { + test('_purgeReviewersPendingRemove', () => { + const removeStub = sandbox.stub(element, '_removeAccount'); + const mock = function() { element._reviewersPendingRemove = { test: [makeAccount()], test2: [makeAccount(), makeAccount()], }; }; - var checkObjEmpty = function(obj) { - for (var prop in obj) { + const checkObjEmpty = function(obj) { + for (const prop in obj) { if (obj.hasOwnProperty(prop) && obj[prop].length) { return false; } } return true; @@ -546,48 +559,81 @@ assert.isTrue(checkObjEmpty(element._reviewersPendingRemove)); }); - test('_removeAccount', function(done) { + test('_removeAccount', done => { sandbox.stub(element.$.restAPI, 'removeChangeReviewer') .returns(Promise.resolve({ok: true})); - var arr = [makeAccount(), makeAccount()]; + const arr = [makeAccount(), makeAccount()]; element.change.reviewers = { REVIEWER: arr.slice(), }; - element._removeAccount(arr[1], 'REVIEWER').then(function() { + element._removeAccount(arr[1], 'REVIEWER').then(() => { assert.equal(element.change.reviewers.REVIEWER.length, 1); assert.deepEqual(element.change.reviewers.REVIEWER, arr.slice(0, 1)); done(); }); }); - test('migrate reviewers between states', function(done) { + test('moving from cc to reviewer', () => { element.serverConfig = {note_db_enabled: true}; element._reviewersPendingRemove = { CC: [], REVIEWER: [], }; flushAsynchronousOperations(); - var reviewers = element.$.reviewers; - var ccs = element.$$('#ccs'); - var reviewer1 = makeAccount(); - var reviewer2 = makeAccount(); - var cc1 = makeAccount(); - var cc2 = makeAccount(); + + const reviewer1 = makeAccount(); + const reviewer2 = makeAccount(); + const reviewer3 = makeAccount(); + const cc1 = makeAccount(); + const cc2 = makeAccount(); + const cc3 = makeAccount(); + const cc4 = makeAccount(); + element._reviewers = [reviewer1, reviewer2, reviewer3]; + element._ccs = [cc1, cc2, cc3, cc4]; + element.push('_reviewers', cc1); + flushAsynchronousOperations(); + + assert.deepEqual(element._reviewers, + [reviewer1, reviewer2, reviewer3, cc1]); + assert.deepEqual(element._ccs, [cc2, cc3, cc4]); + assert.deepEqual(element._reviewersPendingRemove.CC, [cc1]); + + element.push('_reviewers', cc4, cc3); + flushAsynchronousOperations(); + + assert.deepEqual(element._reviewers, + [reviewer1, reviewer2, reviewer3, cc1, cc4, cc3]); + assert.deepEqual(element._ccs, [cc2]); + assert.deepEqual(element._reviewersPendingRemove.CC, [cc1, cc4, cc3]); + }); + + test('migrate reviewers between states', done => { + element.serverConfig = {note_db_enabled: true}; + element._reviewersPendingRemove = { + CC: [], + REVIEWER: [], + }; + flushAsynchronousOperations(); + const reviewers = element.$.reviewers; + const ccs = element.$$('#ccs'); + const reviewer1 = makeAccount(); + const reviewer2 = makeAccount(); + const cc1 = makeAccount(); + const cc2 = makeAccount(); + const cc3 = makeAccount(); element._reviewers = [reviewer1, reviewer2]; - element._ccs = [cc1, cc2]; + element._ccs = [cc1, cc2, cc3]; - var mutations = []; + const mutations = []; - var saveReviewStub = sandbox.stub(element, '_saveReview', - function(review) { - mutations.push.apply(mutations, review.reviewers); + sandbox.stub(element, '_saveReview', review => { + mutations.push(...review.reviewers); return Promise.resolve({ok: true}); }); - var removeAccountStub = sandbox.stub(element, '_removeAccount', - function(account, type) { - mutations.push({state: 'REMOVED', account: account}); + sandbox.stub(element, '_removeAccount', (account, type) => { + mutations.push({state: 'REMOVED', account}); return Promise.resolve(); }); @@ -595,33 +641,123 @@ reviewers.fire('remove', {account: reviewer1}); ccs.$.entry.fire('add', {value: {account: reviewer1}}); ccs.fire('remove', {account: cc1}); + ccs.fire('remove', {account: cc3}); reviewers.$.entry.fire('add', {value: {account: cc1}}); // Add to other field without removing from former field. // (Currently not possible in UI, but this is a good consistency check). reviewers.$.entry.fire('add', {value: {account: cc2}}); ccs.$.entry.fire('add', {value: {account: reviewer2}}); - var mapReviewer = function(reviewer, opt_state) { - var result = {reviewer: reviewer._account_id, confirmed: undefined}; + const mapReviewer = function(reviewer, opt_state) { + const result = {reviewer: reviewer._account_id, confirmed: undefined}; if (opt_state) { result.state = opt_state; } return result; }; - // Send and purge and verify moves without deletions. + // Send and purge and verify moves, delete cc3. element.send() - .then(element._purgeReviewersPendingRemove.bind(element)) - .then(function() { - assert.deepEqual( - mutations, [ - mapReviewer(cc1), - mapReviewer(cc2), - mapReviewer(reviewer1, 'CC'), - mapReviewer(reviewer2, 'CC'), - ]); - done(); + .then(keepReviewers => + element._purgeReviewersPendingRemove(false, keepReviewers)) + .then(() => { + assert.deepEqual( + mutations, [ + mapReviewer(cc1), + mapReviewer(cc2), + mapReviewer(reviewer1, 'CC'), + mapReviewer(reviewer2, 'CC'), + {account: cc3, state: 'REMOVED'}, + ]); + done(); + }); + }); + + test('emits cancel on esc key', () => { + const cancelHandler = sandbox.spy(); + element.addEventListener('cancel', cancelHandler); + MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc'); + flushAsynchronousOperations(); + + assert.isTrue(cancelHandler.called); + }); + + test('_computeMessagePlaceholder', () => { + assert.equal( + element._computeMessagePlaceholder(false), + 'Say something nice...'); + assert.equal( + element._computeMessagePlaceholder(true), + 'Add a note for your reviewers...'); + }); + + test('_computeSendButtonLabel', () => { + assert.equal( + element._computeSendButtonLabel(false), + 'Send'); + assert.equal( + element._computeSendButtonLabel(true), + 'Start review'); + }); + + test('_handle400Error reviewrs and CCs', done => { + const error1 = 'error 1'; + const error2 = 'error 2'; + const error3 = 'error 3'; + const response = { + status: 400, + text() { + return Promise.resolve(')]}\'' + JSON.stringify({ + reviewers: { + username1: { + input: 'user 1', + error: error1, + }, + username2: { + input: 'user 2', + error: error2, + }, + }, + ccs: { + username3: { + input: 'user 3', + error: error3, + }, + }, + })); + }, + }; + element.addEventListener('server-error', e => { + e.detail.response.text().then(text => { + assert.equal(text, [error1, error2, error3].join(', ')); + done(); + }); }); + element._handle400Error(response); + }); + + test('_handle400Error CCs only', done => { + const error1 = 'error 1'; + const response = { + status: 400, + text() { + return Promise.resolve(')]}\'' + JSON.stringify({ + ccs: { + username1: { + input: 'user 1', + error: error1, + }, + }, + })); + }, + }; + element.addEventListener('server-error', e => { + e.detail.response.text().then(text => { + assert.equal(text, error1); + done(); + }); + }); + element._handle400Error(response); }); }); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/test/plugin.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/test/plugin.html new file mode 100644 index 0000000..40092ae --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/test/plugin.html
@@ -0,0 +1,32 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<dom-module id="my-plugin"> + <script> + Gerrit.install(plugin => { + const replyApi = plugin.changeReply(); + replyApi.addReplyTextChangedCallback(text => { + const label = 'Code-Review'; + const labelValue = replyApi.getLabelValue(label); + if (labelValue && + labelValue === ' 0' && + text.indexOf('LGTM') === 0) { + replyApi.setLabelValue(label, '+1'); + } + }); + }); + </script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html index 435b7de..9a5b5ed 100644 --- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html +++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -19,10 +19,11 @@ <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-reviewer-list"> <template> - <style> + <style include="shared-styles"> :host { display: block; } @@ -33,6 +34,9 @@ .autocompleteContainer { position: relative; } + .hiddenReviewers { + margin-top: .3em; + } .inputContainer { display: flex; margin-top: .25em; @@ -56,13 +60,17 @@ } } </style> - <template is="dom-repeat" items="[[_reviewers]]" as="reviewer"> + <template is="dom-repeat" items="[[_displayedReviewers]]" as="reviewer"> <gr-account-chip class="reviewer" account="[[reviewer]]" on-remove="_handleRemove" - data-account-id$="[[reviewer._account_id]]" removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]"> </gr-account-chip> </template> + <gr-button + class="hiddenReviewers" + link + hidden$="[[!_hiddenReviewerCount]]" + on-tap="_handleViewAll">and [[_hiddenReviewerCount]] more</gr-button> <div class="controlsContainer" hidden$="[[!mutable]]"> <gr-button link
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js index 72a7c9b..010a01f 100644 --- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js +++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -14,6 +14,8 @@ (function() { 'use strict'; + const MAX_REVIEWERS_DISPLAYED = 5; + Polymer({ is: 'gr-reviewer-list', @@ -43,9 +45,13 @@ value: false, }, + _displayedReviewers: { + type: Array, + value() { return []; }, + }, _reviewers: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _showInput: { type: Boolean, @@ -55,6 +61,11 @@ type: String, computed: '_computeAddLabel(ccsOnly)', }, + _hiddenReviewerCount: { + type: Number, + computed: '_computeHiddenCount(_reviewers, _displayedReviewers)', + }, + // Used for testing. _lastAutocompleteRequest: Object, @@ -65,10 +76,10 @@ '_reviewersChanged(change.reviewers.*, change.owner)', ], - _reviewersChanged: function(changeRecord, owner) { - var result = []; - var reviewers = changeRecord.base; - for (var key in reviewers) { + _reviewersChanged(changeRecord, owner) { + let result = []; + const reviewers = changeRecord.base; + for (const key in reviewers) { if (this.reviewersOnly && key !== 'REVIEWER') { continue; } @@ -79,66 +90,86 @@ result = result.concat(reviewers[key]); } } - this._reviewers = result.filter(function(reviewer) { + this._reviewers = result.filter(reviewer => { return reviewer._account_id != owner._account_id; }); + // If there is one more than the max reviewers, don't show the 'show more' + // button, because it takes up just as much space. + if (this._reviewers.length <= MAX_REVIEWERS_DISPLAYED + 1) { + this._displayedReviewers = this._reviewers; + } else { + this._displayedReviewers = + this._reviewers.slice(0, MAX_REVIEWERS_DISPLAYED); + } }, - _computeCanRemoveReviewer: function(reviewer, mutable) { + _computeHiddenCount(reviewers, displayedReviewers) { + return reviewers.length - displayedReviewers.length; + }, + + _computeCanRemoveReviewer(reviewer, mutable) { if (!mutable) { return false; } - for (var i = 0; i < this.change.removable_reviewers.length; i++) { - if (this.change.removable_reviewers[i]._account_id == - reviewer._account_id) { + let current; + for (let i = 0; i < this.change.removable_reviewers.length; i++) { + current = this.change.removable_reviewers[i]; + if (current._account_id === reviewer._account_id || + (!reviewer._account_id && current.email === reviewer.email)) { return true; } } return false; }, - _handleRemove: function(e) { + _handleRemove(e) { e.preventDefault(); - var target = Polymer.dom(e).rootTarget; - var accountID = parseInt(target.getAttribute('data-account-id'), 10); + const target = Polymer.dom(e).rootTarget; + if (!target.account) { return; } + const accountID = target.account._account_id || target.account.email; this.disabled = true; - this._xhrPromise = - this._removeReviewer(accountID).then(function(response) { + this._xhrPromise = this._removeReviewer(accountID).then(response => { this.disabled = false; if (!response.ok) { return response; } - var reviewers = this.change.reviewers; - ['REVIEWER', 'CC'].forEach(function(type) { + const reviewers = this.change.reviewers; + + for (const type of ['REVIEWER', 'CC']) { reviewers[type] = reviewers[type] || []; - for (var i = 0; i < reviewers[type].length; i++) { - if (reviewers[type][i]._account_id == accountID) { + for (let i = 0; i < reviewers[type].length; i++) { + if (reviewers[type][i]._account_id == accountID || + reviewers[type][i].email == accountID) { this.splice('change.reviewers.' + type, i, 1); break; } } - }, this); - }.bind(this)).catch(function(err) { + } + }).catch(err => { this.disabled = false; throw err; - }.bind(this)); + }); }, - _handleAddTap: function(e) { + _handleAddTap(e) { e.preventDefault(); - var value = {}; + const value = {}; if (this.reviewersOnly) { value.reviewersOnly = true; } if (this.ccsOnly) { value.ccsOnly = true; } - this.fire('show-reply-dialog', {value: value}); + this.fire('show-reply-dialog', {value}); }, - _removeReviewer: function(id) { + _handleViewAll(e) { + this._displayedReviewers = this._reviewers; + }, + + _removeReviewer(id) { return this.$.restAPI.removeChangeReviewer(this.change._number, id); }, - _computeAddLabel: function(ccsOnly) { + _computeAddLabel(ccsOnly) { return ccsOnly ? 'Add CC' : 'Add reviewer'; }, });
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html index 4542df8..3c1773d 100644 --- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-reviewer-list.html"> <script>void(0);</script> @@ -33,47 +32,47 @@ </test-fixture> <script> - suite('gr-reviewer-list tests', function() { - var element; - var sandbox; + suite('gr-reviewer-list tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { element = fixture('basic'); sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, - removeChangeReviewer: function() { + getConfig() { return Promise.resolve({}); }, + removeChangeReviewer() { return Promise.resolve({ok: true}); }, }); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('controls hidden on immutable element', function() { + test('controls hidden on immutable element', () => { element.mutable = false; assert.isTrue(element.$$('.controlsContainer').hasAttribute('hidden')); element.mutable = true; assert.isFalse(element.$$('.controlsContainer').hasAttribute('hidden')); }); - test('add reviewer button opens reply dialog', function(done) { - element.addEventListener('show-reply-dialog', function() { + test('add reviewer button opens reply dialog', done => { + element.addEventListener('show-reply-dialog', () => { done(); }); MockInteractions.tap(element.$$('.addReviewer')); }); - test('only show remove for removable reviewers', function() { + test('only show remove for removable reviewers', () => { element.mutable = true; element.change = { owner: { _account_id: 1, }, reviewers: { - 'REVIEWER': [ + REVIEWER: [ { _account_id: 2, name: 'Bojack Horseman', @@ -84,13 +83,16 @@ name: 'Pinky Penguin', }, ], - 'CC': [ + CC: [ { _account_id: 4, name: 'Diane Nguyen', email: 'macarthurfellow2B@juno.com', }, - ] + { + email: 'test@e.mail', + }, + ], }, removable_reviewers: [ { @@ -102,36 +104,40 @@ name: 'Diane Nguyen', email: 'macarthurfellow2B@juno.com', }, - ] + { + email: 'test@e.mail', + }, + ], }; flushAsynchronousOperations(); - var chips = + const chips = Polymer.dom(element.root).querySelectorAll('gr-account-chip'); - assert.equal(chips.length, 3); - Array.from(chips).forEach(function(el) { - var accountID = parseInt(el.getAttribute('data-account-id'), 10); + assert.equal(chips.length, 4); + + for (const el of Array.from(chips)) { + const accountID = el.account._account_id || el.account.email; assert.ok(accountID); - var buttonEl = el.$$('gr-button'); + const buttonEl = el.$$('gr-button'); assert.isNotNull(buttonEl); if (accountID == 2) { assert.isTrue(buttonEl.hasAttribute('hidden')); } else { assert.isFalse(buttonEl.hasAttribute('hidden')); } - }); + } }); - test('tracking reviewers and ccs', function() { - var counter = 0; + test('tracking reviewers and ccs', () => { + let counter = 0; function makeAccount() { return {_account_id: counter++}; } - var owner = makeAccount(); - var reviewer = makeAccount(); - var cc = makeAccount(); - var reviewers = { + const owner = makeAccount(); + const reviewer = makeAccount(); + const cc = makeAccount(); + const reviewers = { REMOVED: [makeAccount()], REVIEWER: [owner, reviewer], CC: [owner, cc], @@ -140,30 +146,30 @@ element.ccsOnly = false; element.reviewersOnly = false; element.change = { - owner: owner, - reviewers: reviewers, + owner, + reviewers, }; assert.deepEqual(element._reviewers, [reviewer, cc]); element.reviewersOnly = true; element.change = { - owner: owner, - reviewers: reviewers, + owner, + reviewers, }; assert.deepEqual(element._reviewers, [reviewer]); element.ccsOnly = true; element.reviewersOnly = false; element.change = { - owner: owner, - reviewers: reviewers, + owner, + reviewers, }; assert.deepEqual(element._reviewers, [cc]); }); - test('_handleAddTap passes mode with event', function() { - var fireStub = sandbox.stub(element, 'fire'); - var e = {preventDefault: function() {}}; + test('_handleAddTap passes mode with event', () => { + const fireStub = sandbox.stub(element, 'fire'); + const e = {preventDefault() {}}; element.ccsOnly = false; element.reviewersOnly = false; @@ -181,5 +187,81 @@ assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog', {value: {ccsOnly: true}})); }); + + test('no show all reviewers button with 6 reviewers', () => { + const reviewers = []; + for (let i = 0; i < 6; i++) { + reviewers.push( + {email: i+'reviewer@google.com', name: 'reviewer-' + i}); + } + element.ccsOnly = true; + + element.change = { + owner: { + _account_id: 1, + }, + reviewers: { + CC: reviewers, + }, + }; + flushAsynchronousOperations(); + assert.equal(element._hiddenReviewerCount, 0); + assert.equal(element._displayedReviewers.length, 6); + assert.equal(element._reviewers.length, 6); + assert.isTrue(element.$$('.hiddenReviewers').hidden); + }); + + test('how all reviewers button with 7 reviewers', () => { + const reviewers = []; + for (let i = 0; i < 7; i++) { + reviewers.push( + {email: i+'reviewer@google.com', name: 'reviewer-' + i}); + } + element.ccsOnly = true; + + element.change = { + owner: { + _account_id: 1, + }, + reviewers: { + CC: reviewers, + }, + }; + flushAsynchronousOperations(); + assert.equal(element._hiddenReviewerCount, 2); + assert.equal(element._displayedReviewers.length, 5); + assert.equal(element._reviewers.length, 7); + assert.isFalse(element.$$('.hiddenReviewers').hidden); + }); + + test('show all reviewers button', () => { + const reviewers = []; + for (let i = 0; i < 100; i++) { + reviewers.push( + {email: i+'reviewer@google.com', name: 'reviewer-' + i}); + } + element.ccsOnly = true; + + element.change = { + owner: { + _account_id: 1, + }, + reviewers: { + CC: reviewers, + }, + }; + flushAsynchronousOperations(); + assert.equal(element._hiddenReviewerCount, 95); + assert.equal(element._displayedReviewers.length, 5); + assert.equal(element._reviewers.length, 100); + assert.isFalse(element.$$('.hiddenReviewers').hidden); + + MockInteractions.tap(element.$$('.hiddenReviewers')); + flushAsynchronousOperations(); + assert.equal(element._hiddenReviewerCount, 0); + assert.equal(element._displayedReviewers.length, 100); + assert.equal(element._reviewers.length, 100); + assert.isTrue(element.$$('.hiddenReviewers').hidden); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html index 7e358fd..4fc12b5 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.html
@@ -18,10 +18,11 @@ <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-account-dropdown"> <template> - <style> + <style include="shared-styles"> button { background: none; border: none;
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js index d1da829..318794f 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -12,53 +12,92 @@ // See the License for the specific language governing permissions and // limitations under the License. (function() { - 'use strict' + 'use strict'; - var ANONYMOUS_NAME = 'Anonymous'; + const INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g; + + const ANONYMOUS_NAME = 'Anonymous'; Polymer({ is: 'gr-account-dropdown', properties: { account: Object, - _hasAvatars: Boolean, _anonymousName: { type: String, value: ANONYMOUS_NAME, }, links: { type: Array, - value: [ - {name: 'Settings', url: '/settings'}, - {name: 'Switch account', url: '/switch-account'}, - {name: 'Sign out', url: '/logout'}, - ], + computed: '_getLinks(_switchAccountUrl, _path)', }, topContent: { type: Array, computed: '_getTopContent(account, _anonymousName)', }, + _path: { + type: String, + value: '/', + }, + _hasAvatars: Boolean, + _switchAccountUrl: String, }, - attached: function() { - this.$.restAPI.getConfig().then(function(cfg) { + attached() { + this._handleLocationChange(); + this.listen(window, 'location-change', '_handleLocationChange'); + this.$.restAPI.getConfig().then(cfg => { + if (cfg && cfg.auth && cfg.auth.switch_account_url) { + this._switchAccountUrl = cfg.auth.switch_account_url; + } else { + this._switchAccountUrl = null; + } this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); + if (cfg && cfg.user && cfg.user.anonymous_coward_name && cfg.user.anonymous_coward_name !== 'Anonymous Coward') { this._anonymousName = cfg.user.anonymous_coward_name; } - }.bind(this)); + }); }, - _getTopContent: function(account, _anonymousName) { + detached() { + this.unlisten(window, 'location-change', '_handleLocationChange'); + }, + + _getLinks(switchAccountUrl, path) { + const links = [{name: 'Settings', url: '/settings'}]; + if (switchAccountUrl) { + const replacements = {path}; + const url = this._interpolateUrl(switchAccountUrl, replacements); + links.push({name: 'Switch account', url}); + } + links.push({name: 'Sign out', url: '/logout'}); + return links; + }, + + _getTopContent(account, _anonymousName) { return [ {text: this._accountName(account, _anonymousName), bold: true}, {text: account.email ? account.email : ''}, ]; }, - _accountName: function(account, _anonymousName) { + _handleLocationChange() { + this._path = + window.location.pathname + + window.location.search + + window.location.hash; + }, + + _interpolateUrl(url, replacements) { + return url.replace(INTERPOLATE_URL_PATTERN, (match, p1) => { + return replacements[p1] || ''; + }); + }, + + _accountName(account, _anonymousName) { if (account && account.name) { return account.name; } else if (account && account.email) {
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html index d7f09b8..6bda66f 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-account-dropdown.html"> <script>void(0);</script> @@ -33,40 +32,73 @@ </test-fixture> <script> - suite('gr-account-dropdown tests', function() { - var element; + suite('gr-account-dropdown tests', () => { + let element; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, + getConfig() { return Promise.resolve({}); }, }); element = fixture('basic'); }); - test('account information', function() { + test('account information', () => { element.account = {name: 'John Doe', email: 'john@doe.com'}; assert.deepEqual(element.topContent, [{text: 'John Doe', bold: true}, {text: 'john@doe.com'}]); }); - test('test for account without a name', function() { + test('test for account without a name', () => { element.account = {id: '0001'}; assert.deepEqual(element.topContent, [{text: 'Anonymous', bold: true}, {text: ''}]); }); - test('test for account without a name but using config', function() { + test('test for account without a name but using config', () => { element._anonymousName = 'WikiGerrit'; element.account = {id: '0001'}; assert.deepEqual(element.topContent, [{text: 'WikiGerrit', bold: true}, {text: ''}]); }); - test('test for account name as an email', function() { + test('test for account name as an email', () => { element._anonymousName = 'WikiGerrit'; element.account = {email: 'john@doe.com'}; assert.deepEqual(element.topContent, [{text: 'john@doe.com', bold: true}, {text: 'john@doe.com'}]); }); + + test('switch account', () => { + // No switch account link. + assert.equal(element._getLinks(null).length, 2); + + // Unparameterized switch account link. + let links = element._getLinks('/switch-account'); + assert.equal(links.length, 3); + assert.deepEqual(links[1], + {name: 'Switch account', url: '/switch-account'}); + + // Parameterized switch account link. + links = element._getLinks('/switch-account${path}', '/c/123'); + assert.equal(links.length, 3); + assert.deepEqual(links[1], + {name: 'Switch account', url: '/switch-account/c/123'}); + }); + + test('_interpolateUrl', () => { + const replacements = { + foo: 'bar', + test: 'TEST', + }; + const interpolate = function(url) { + return element._interpolateUrl(url, replacements); + }; + + assert.equal(interpolate('test'), 'test'); + assert.equal(interpolate('${test}'), 'TEST'); + assert.equal( + interpolate('${}, ${test}, ${TEST}, ${foo}'), + '${}, TEST, , bar'); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html index e3f2bbc..5765411 100644 --- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html +++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.html
@@ -25,4 +25,3 @@ </template> <script src="gr-error-manager.js"></script> </dom-module> -
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js index d48d870..5cddb07 100644 --- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js +++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -14,12 +14,13 @@ (function() { 'use strict'; - var HIDE_ALERT_TIMEOUT_MS = 5000; - var CHECK_SIGN_IN_INTERVAL_MS = 60*1000; - var STALE_CREDENTIAL_THRESHOLD_MS = 10*60*1000; - var SIGN_IN_WIDTH_PX = 690; - var SIGN_IN_HEIGHT_PX = 500; - var TOO_MANY_FILES = 'too many files to find conflicts'; + const HIDE_ALERT_TIMEOUT_MS = 5000; + const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000; + const STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000; + const SIGN_IN_WIDTH_PX = 690; + const SIGN_IN_HEIGHT_PX = 500; + const TOO_MANY_FILES = 'too many files to find conflicts'; + const AUTHENTICATION_REQUIRED = 'Authentication required\n'; Polymer({ is: 'gr-error-manager', @@ -47,91 +48,109 @@ */ _lastCredentialCheck: { type: Number, - value: function() { return Date.now(); }, - } + value() { return Date.now(); }, + }, }, - attached: function() { + attached() { this.listen(document, 'server-error', '_handleServerError'); this.listen(document, 'network-error', '_handleNetworkError'); this.listen(document, 'show-alert', '_handleShowAlert'); this.listen(document, 'visibilitychange', '_handleVisibilityChange'); + this.listen(document, 'show-auth-required', '_handleAuthRequired'); }, - detached: function() { + detached() { this._clearHideAlertHandle(); this.unlisten(document, 'server-error', '_handleServerError'); this.unlisten(document, 'network-error', '_handleNetworkError'); + this.unlisten(document, 'show-auth-required', '_handleAuthRequired'); this.unlisten(document, 'visibilitychange', '_handleVisibilityChange'); }, - _shouldSuppressError: function(msg) { - return msg.indexOf(TOO_MANY_FILES) > -1; + _shouldSuppressError(msg) { + return msg.includes(TOO_MANY_FILES); }, - _handleServerError: function(e) { - if (e.detail.response.status === 403) { - this._getLoggedIn().then(function(loggedIn) { - if (loggedIn) { - // The app was logged at one point and is now getting auth errors. - // This indicates the auth token is no longer valid. - this._showAuthErrorAlert(); - } - }.bind(this)); - } else { - e.detail.response.text().then(function(text) { - if (!this._shouldSuppressError(text)) { - this._showAlert('Server error: ' + text); - } - }.bind(this)); - } + _handleAuthRequired() { + this._showAuthErrorAlert( + 'Log in is required to perform that action.', 'Log in.'); }, - _handleShowAlert: function(e) { - this._showAlert(e.detail.message); + _handleServerError(e) { + Promise.all([ + e.detail.response.text(), this._getLoggedIn(), + ]).then(values => { + const text = values[0]; + const loggedIn = values[1]; + if (e.detail.response.status === 403 && + loggedIn && + text === AUTHENTICATION_REQUIRED) { + // The app was logged at one point and is now getting auth errors. + // This indicates the auth token is no longer valid. + this._showAuthErrorAlert('Auth error', 'Refresh credentials.'); + } else if (!this._shouldSuppressError(text)) { + this._showAlert('Server error: ' + text); + } + console.error(text); + }); }, - _handleNetworkError: function(e) { + _handleShowAlert(e) { + this._showAlert(e.detail.message, e.detail.action, e.detail.callback, + e.detail.dismissOnNavigation); + }, + + _handleNetworkError(e) { this._showAlert('Server unavailable'); console.error(e.detail.error.message); }, - _getLoggedIn: function() { + _getLoggedIn() { return this.$.restAPI.getLoggedIn(); }, - _showAlert: function(text) { + _showAlert(text, opt_actionText, opt_actionCallback, + dismissOnNavigation) { if (this._alertElement) { return; } this._clearHideAlertHandle(); - this._hideAlertHandle = - this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS); - var el = this._createToastAlert(); - el.show(text); + if (dismissOnNavigation) { + // Persist alert until navigation. + this.listen(document, 'location-change', '_hideAlert'); + } else { + this._hideAlertHandle = + this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS); + } + const el = this._createToastAlert(); + el.show(text, opt_actionText, opt_actionCallback); this._alertElement = el; }, - _hideAlert: function() { + _hideAlert() { if (!this._alertElement) { return; } this._alertElement.hide(); this._alertElement = null; + + // Remove listener for page navigation, if it exists. + this.unlisten(document, 'location-change', '_hideAlert'); }, - _clearHideAlertHandle: function() { + _clearHideAlertHandle() { if (this._hideAlertHandle != null) { this.cancelAsync(this._hideAlertHandle); this._hideAlertHandle = null; } }, - _showAuthErrorAlert: function() { + _showAuthErrorAlert(errorText, actionText) { // TODO(viktard): close alert if it's not for auth error. if (this._alertElement) { return; } this._alertElement = this._createToastAlert(); - this._alertElement.show('Auth error', 'Refresh credentials.'); - this.listen(this._alertElement, 'action', '_createLoginPopup'); + this._alertElement.show(errorText, actionText, + this._createLoginPopup.bind(this)); this._refreshingCredentials = true; this._requestCheckLoggedIn(); @@ -140,46 +159,40 @@ } }, - _createToastAlert: function() { - var el = document.createElement('gr-alert'); + _createToastAlert() { + const el = document.createElement('gr-alert'); el.toast = true; return el; }, - _handleVisibilityChange: function() { + _handleVisibilityChange() { // Ignore when the page is transitioning to hidden (or hidden is // undefined). if (document.hidden !== false) { return; } - // If we're currently in a credential refresh, flush the debouncer so that - // it can be checked immediately. - if (this._refreshingCredentials) { - this.flushDebouncer('checkLoggedIn'); - return; - } - - // If the credentials are old, request them to confirm their validity or - // (display an auth toast if it fails). - var timeSinceLastCheck = Date.now() - this._lastCredentialCheck; - if (this.knownAccountId !== undefined && + // If not currently refreshing credentials and the credentials are old, + // request them to confirm their validity or (display an auth toast if it + // fails). + const timeSinceLastCheck = Date.now() - this._lastCredentialCheck; + if (!this._refreshingCredentials && + this.knownAccountId !== undefined && timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) { this._lastCredentialCheck = Date.now(); this.$.restAPI.checkCredentials(); } }, - _requestCheckLoggedIn: function() { + _requestCheckLoggedIn() { this.debounce( - 'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS); + 'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS); }, - _checkSignedIn: function() { - this.$.restAPI.checkCredentials().then(function(account) { - var isLoggedIn = !!account; + _checkSignedIn() { + this.$.restAPI.checkCredentials().then(account => { + const isLoggedIn = !!account; this._lastCredentialCheck = Date.now(); if (this._refreshingCredentials) { if (isLoggedIn) { - // If the credentials were refreshed but the account is different // then reload the page completely. if (account._account_id !== this.knownAccountId) { @@ -192,17 +205,19 @@ this._requestCheckLoggedIn(); } } - }.bind(this)); + }); }, - _reloadPage: function() { + _reloadPage() { window.location.reload(); }, - _createLoginPopup: function() { - var left = window.screenLeft + (window.outerWidth - SIGN_IN_WIDTH_PX) / 2; - var top = window.screenTop + (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2; - var options = [ + _createLoginPopup() { + const left = window.screenLeft + + (window.outerWidth - SIGN_IN_WIDTH_PX) / 2; + const top = window.screenTop + + (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2; + const options = [ 'width=' + SIGN_IN_WIDTH_PX, 'height=' + SIGN_IN_HEIGHT_PX, 'left=' + left, @@ -210,13 +225,18 @@ ]; window.open(this.getBaseUrl() + '/login/%3FcloseAfterLogin', '_blank', options.join(',')); + this.listen(window, 'focus', '_handleWindowFocus'); }, - _handleCredentialRefreshed: function() { + _handleCredentialRefreshed() { + this.unlisten(window, 'focus', '_handleWindowFocus'); this._refreshingCredentials = false; - this.unlisten(this._alertElement, 'action', '_createLoginPopup'); this._hideAlert(); this._showAlert('Credentials refreshed.'); }, + + _handleWindowFocus() { + this.flushDebouncer('checkLoggedIn'); + }, }); })();
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html index 2013686..5d1c461 100644 --- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html +++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-error-manager.html"> <script>void(0);</script> @@ -33,38 +32,69 @@ </test-fixture> <script> - suite('gr-error-manager tests', function() { - var element; - var sandbox; + suite('gr-error-manager tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(true); }, + getLoggedIn() { return Promise.resolve(true); }, }); element = fixture('basic'); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('show auth error', function(done) { - var showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert'); - element.fire('server-error', {response: {status: 403}}); - element.$.restAPI.getLoggedIn.lastCall.returnValue.then(function() { + test('does not show auth error on 403 by default', done => { + const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert'); + const responseText = Promise.resolve('server says no.'); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + Promise.all([ + element.$.restAPI.getLoggedIn.lastCall.returnValue, + responseText, + ]).then(() => { + assert.isFalse(showAuthErrorStub.calledOnce); + done(); + }); + }); + + test('shows auth error on 403 and Authentication required', done => { + const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert'); + const responseText = Promise.resolve('Authentication required\n'); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + Promise.all([ + element.$.restAPI.getLoggedIn.lastCall.returnValue, + responseText, + ]).then(() => { assert.isTrue(showAuthErrorStub.calledOnce); done(); }); }); - test('show normal server error', function(done) { - var showAlertStub = sandbox.stub(element, '_showAlert'); - var textSpy = sandbox.spy(function() { return Promise.resolve('ZOMG'); }); + test('show logged in error', () => { + sandbox.stub(element, '_showAuthErrorAlert'); + element.fire('show-auth-required'); + assert.isTrue(element._showAuthErrorAlert.calledWithExactly( + 'Log in is required to perform that action.', 'Log in.')); + }); + + test('show normal server error', done => { + const showAlertStub = sandbox.stub(element, '_showAlert'); + const textSpy = sandbox.spy(() => { return Promise.resolve('ZOMG'); }); element.fire('server-error', {response: {status: 500, text: textSpy}}); assert.isTrue(textSpy.called); - textSpy.lastCall.returnValue.then(function() { + Promise.all([ + element.$.restAPI.getLoggedIn.lastCall.returnValue, + textSpy.lastCall.returnValue, + ]).then(() => { assert.isTrue(showAlertStub.calledOnce); assert.isTrue(showAlertStub.lastCall.calledWithExactly( 'Server error: ZOMG')); @@ -72,25 +102,28 @@ }); }); - test('suppress TOO_MANY_FILES error', function(done) { - var showAlertStub = sandbox.stub(element, '_showAlert'); - var textSpy = sandbox.spy(function() { + test('suppress TOO_MANY_FILES error', done => { + const showAlertStub = sandbox.stub(element, '_showAlert'); + const textSpy = sandbox.spy(() => { return Promise.resolve('too many files to find conflicts'); }); element.fire('server-error', {response: {status: 500, text: textSpy}}); assert.isTrue(textSpy.called); - textSpy.lastCall.returnValue.then(function() { + Promise.all([ + element.$.restAPI.getLoggedIn.lastCall.returnValue, + textSpy.lastCall.returnValue, + ]).then(() => { assert.isFalse(showAlertStub.called); done(); }); }); - test('show network error', function(done) { - var consoleErrorStub = sandbox.stub(console, 'error'); - var showAlertStub = sandbox.stub(element, '_showAlert'); + test('show network error', done => { + const consoleErrorStub = sandbox.stub(console, 'error'); + const showAlertStub = sandbox.stub(element, '_showAlert'); element.fire('network-error', {error: new Error('ZOMG')}); - flush(function() { + flush(() => { assert.isTrue(showAlertStub.calledOnce); assert.isTrue(showAlertStub.lastCall.calledWithExactly( 'Server unavailable')); @@ -100,15 +133,21 @@ }); }); - test('show auth refresh toast', function(done) { - var refreshStub = sandbox.stub(element.$.restAPI, 'checkCredentials', - function() { return Promise.resolve(true); }); - var toastSpy = sandbox.spy(element, '_createToastAlert'); - var windowOpen = sandbox.stub(window, 'open'); - element.fire('server-error', {response: {status: 403}}); - element.$.restAPI.getLoggedIn.lastCall.returnValue.then(function() { + test('show auth refresh toast', done => { + const refreshStub = sandbox.stub(element.$.restAPI, 'checkCredentials', + () => { return Promise.resolve(true); }); + const toastSpy = sandbox.spy(element, '_createToastAlert'); + const windowOpen = sandbox.stub(window, 'open'); + const responseText = Promise.resolve('Authentication required\n'); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + Promise.all([ + element.$.restAPI.getLoggedIn.lastCall.returnValue, + responseText, + ]).then(() => { assert.isTrue(toastSpy.called); - var toast = toastSpy.lastCall.returnValue; + let toast = toastSpy.lastCall.returnValue; assert.isOk(toast); assert.include( Polymer.dom(toast.root).textContent, 'Auth error'); @@ -116,18 +155,19 @@ Polymer.dom(toast.root).textContent, 'Refresh credentials.'); assert.isFalse(windowOpen.called); - toast.fire('action'); + MockInteractions.tap(toast.$$('gr-button.action')); assert.isTrue(windowOpen.called); // @see Issue 5822: noopener breaks closeAfterLogin assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'), -1); - var hideToastSpy = sandbox.spy(toast, 'hide'); + const hideToastSpy = sandbox.spy(toast, 'hide'); + element._handleWindowFocus(); assert.isTrue(refreshStub.called); element.flushDebouncer('checkLoggedIn'); - flush(function() { + flush(() => { assert.isTrue(refreshStub.called); assert.isTrue(hideToastSpy.called); @@ -141,15 +181,18 @@ }); }); - test('show alert', function() { + test('show alert', () => { + const alertObj = {message: 'foo'}; sandbox.stub(element, '_showAlert'); - element.fire('show-alert', {message: 'foo'}); + element.fire('show-alert', alertObj); assert.isTrue(element._showAlert.calledOnce); - assert.isTrue(element._showAlert.lastCall.calledWithExactly('foo')); + assert.equal(element._showAlert.lastCall.args[0], 'foo'); + assert.isNotOk(element._showAlert.lastCall.args[1]); + assert.isNotOk(element._showAlert.lastCall.args[2]); }); - test('checks stale credentials on visibility change', function() { - var refreshStub = sandbox.stub(element.$.restAPI, + test('checks stale credentials on visibility change', () => { + const refreshStub = sandbox.stub(element.$.restAPI, 'checkCredentials'); sandbox.stub(Date, 'now').returns(999999); element._lastCredentialCheck = 0; @@ -167,19 +210,19 @@ assert.equal(element._lastCredentialCheck, 999999); }); - test('refresh loop continues on credential fail', function(done) { - var accountPromise = Promise.resolve(null); + test('refresh loop continues on credential fail', done => { + const accountPromise = Promise.resolve(null); sandbox.stub(element.$.restAPI, 'checkCredentials') .returns(accountPromise); - var requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn'); - var handleRefreshStub = sandbox.stub(element, + const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn'); + const handleRefreshStub = sandbox.stub(element, '_handleCredentialRefreshed'); - var reloadStub = sandbox.stub(element, '_reloadPage'); + const reloadStub = sandbox.stub(element, '_reloadPage'); element._refreshingCredentials = true; element._checkSignedIn(); - accountPromise.then(function() { + accountPromise.then(() => { assert.isTrue(requestCheckStub.called); assert.isFalse(handleRefreshStub.called); assert.isFalse(reloadStub.called); @@ -187,20 +230,20 @@ }); }); - test('refreshes with same credentials', function(done) { - var accountPromise = Promise.resolve({_account_id: 1234}); + test('refreshes with same credentials', done => { + const accountPromise = Promise.resolve({_account_id: 1234}); sandbox.stub(element.$.restAPI, 'checkCredentials') .returns(accountPromise); - var requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn'); - var handleRefreshStub = sandbox.stub(element, + const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn'); + const handleRefreshStub = sandbox.stub(element, '_handleCredentialRefreshed'); - var reloadStub = sandbox.stub(element, '_reloadPage'); + const reloadStub = sandbox.stub(element, '_reloadPage'); element.knownAccountId = 1234; element._refreshingCredentials = true; element._checkSignedIn(); - accountPromise.then(function() { + accountPromise.then(() => { assert.isFalse(requestCheckStub.called); assert.isTrue(handleRefreshStub.called); assert.isFalse(reloadStub.called); @@ -208,25 +251,41 @@ }); }); - test('reloads when refreshed credentials differ', function(done) { - var accountPromise = Promise.resolve({_account_id: 1234}); + test('reloads when refreshed credentials differ', done => { + const accountPromise = Promise.resolve({_account_id: 1234}); sandbox.stub(element.$.restAPI, 'checkCredentials') .returns(accountPromise); - var requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn'); - var handleRefreshStub = sandbox.stub(element, + const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn'); + const handleRefreshStub = sandbox.stub(element, '_handleCredentialRefreshed'); - var reloadStub = sandbox.stub(element, '_reloadPage'); + const reloadStub = sandbox.stub(element, '_reloadPage'); element.knownAccountId = 4321; // Different from 1234 element._refreshingCredentials = true; element._checkSignedIn(); - accountPromise.then(function() { + accountPromise.then(() => { assert.isFalse(requestCheckStub.called); assert.isFalse(handleRefreshStub.called); assert.isTrue(reloadStub.called); done(); }); }); + + test('dismissOnNavigation respected', () => { + const asyncStub = sandbox.stub(element, 'async'); + const hideSpy = sandbox.spy(element, '_hideAlert'); + // No async call when dismissOnNavigation supplied. + element._showAlert('test', null, null, true); + assert.isFalse(asyncStub.called); + + // When page nav happens, clear alert. + document.dispatchEvent(new CustomEvent('location-change')); + assert.isTrue(hideSpy.called); + + // When timeout is not supplied, use HIDE_ALERT_TIMEOUT_MS. + element._showAlert('test'); + assert.isTrue(asyncStub.called); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html index 9a3a267..fef0f87 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -16,10 +16,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-keyboard-shortcuts-dialog"> <template> - <style> + <style include="shared-styles"> :host { display: block; } @@ -162,6 +163,13 @@ </td> <td>Show selected change</td> </tr> + <tr> + <td> + <span class="key modifier">Shift</span> + <span class="key">r</span> + </td> + <td>Refresh list of changes</td> + </tr> </tbody> <!-- Dashboard --> <tbody hidden$="[[!_computeInView(view, 'gr-dashboard-view')]]" hidden> @@ -183,6 +191,13 @@ </td> <td>Show selected change</td> </tr> + <tr> + <td> + <span class="key modifier">Shift</span> + <span class="key">r</span> + </td> + <td>Refresh list of changes</td> + </tr> </tbody> <!-- Change View --> <tbody hidden$="[[!_computeInView(view, 'gr-change-view')]]" hidden> @@ -216,7 +231,7 @@ <td>Select previous file</td> </tr> <tr> - <td><span class="key">Enter</span> or <span class="key">o</span></td> + <td><span class="key">o</span></td> <td>Show selected file</td> </tr> <tr>
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js index 1a286c7..b7900c0 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -31,11 +31,11 @@ role: 'dialog', }, - _computeInView: function(currentView, view) { + _computeInView(currentView, view) { return view === currentView; }, - _handleCloseTap: function(e) { + _handleCloseTap(e) { e.preventDefault(); this.fire('close', null, {bubbles: false}); },
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html index 1e6596b..258c901 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -13,18 +13,17 @@ See the License for the specific language governing permissions and limitations under the License. --> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> +<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html"> -<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> <link rel="import" href="../gr-search-bar/gr-search-bar.html"> <dom-module id="gr-main-header"> <template> - <style> + <style include="shared-styles"> :host { display: block; } @@ -40,21 +39,36 @@ .bigTitle:hover { text-decoration: underline; } + .bigTitle::before { + background-image: var(--header-icon); + background-size: var(--header-icon-size) var(--header-icon-size); + content: ""; + display: inline-block; + height: var(--header-icon-size); + margin: 0 .25em 0 0; + vertical-align: text-bottom; + width: var(--header-icon-size); + } + .bigTitle::after { + content: var(--header-title-content); + } ul { list-style: none; } .links > li { cursor: default; display: inline-block; - margin-left: 1em; padding: 0; position: relative; } .linksTitle { - color: black; + color: var(--primary-text-color); display: inline-block; position: relative; } + .linksTitle:hover { + opacity: .75; + } .rightItems { align-items: center; display: flex; @@ -66,6 +80,13 @@ margin-left: .5em; max-width: 500px; } + gr-dropdown { + padding: 0.5em; + } + .more { + padding: 1em; + text-decoration: none; + } .accountContainer:not(.loggedIn):not(.loggedOut) .loginButton, .accountContainer:not(.loggedIn):not(.loggedOut) gr-account-dropdown, .accountContainer.loggedIn .loginButton, @@ -75,11 +96,14 @@ .accountContainer { align-items: center; display: flex; - margin-left: var(--default-horizontal-margin); + margin: 0 -0.5em 0 0.5em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .loginButton { + padding: 1em; + } .dropdown-trigger { text-decoration: none; } @@ -92,19 +116,24 @@ font-size: 14px; font-weight: bold; } - gr-search-bar { + gr-search-bar, + .more, + li.hideOnMobile { display: none; } .accountContainer { margin-left: .5em !important; } + gr-dropdown { + padding: .5em 0 .5em .5em; + } } </style> <nav> - <a href$="[[_computeRelativeURL('/')]]" class="bigTitle">PolyGerrit</a> + <a href$="[[_computeRelativeURL('/')]]" class="bigTitle"></a> <ul class="links"> <template is="dom-repeat" items="[[_links]]" as="linkGroup"> - <li> + <li class$="[[linkGroup.class]]"> <gr-dropdown link down-arrow @@ -116,11 +145,16 @@ </gr-dropdown> </li> </template> + <li> + <a class="more linksTitle" href$="[[_computeRelativeURL('/admin/projects')]]"> + More</a> + </li> </ul> <div class="rightItems"> <gr-search-bar value="{{searchQuery}}" role="search"></gr-search-bar> <div class="accountContainer" id="accountContainer"> - <a class="loginButton" href$="[[_loginURL]]" on-tap="_loginTapHandler">Sign in</a> + <a class="loginButton" href$="[[_loginURL]]" + on-tap="_loginTapHandler">Sign in</a> <gr-account-dropdown account="[[_account]]"></gr-account-dropdown> </div> </div>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js index affa8e5..620f733 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -14,33 +14,7 @@ (function() { 'use strict'; - var ADMIN_LINKS = [ - { - url: '/admin/groups', - name: 'Groups', - }, - { - url: '/admin/create-group', - name: 'Create Group', - capability: 'createGroup' - }, - { - url: '/admin/projects', - name: 'Projects', - }, - { - url: '/admin/create-project', - name: 'Create Project', - capability: 'createProject', - }, - { - url: '/admin/plugins', - name: 'Plugins', - capability: 'viewPlugins', - }, - ]; - - var DEFAULT_LINKS = [{ + const DEFAULT_LINKS = [{ title: 'Changes', links: [ { @@ -58,31 +32,31 @@ ], }]; - var DOCUMENTATION_LINKS = [ + const DOCUMENTATION_LINKS = [ { - url : '/index.html', - name : 'Table of Contents', + url: '/index.html', + name: 'Table of Contents', }, { - url : '/user-search.html', - name : 'Searching', + url: '/user-search.html', + name: 'Searching', }, { - url : '/user-upload.html', - name : 'Uploading', + url: '/user-upload.html', + name: 'Uploading', }, { - url : '/access-control.html', - name : 'Access Control', + url: '/access-control.html', + name: 'Access Control', }, { - url : '/rest-api.html', - name : 'REST API', + url: '/rest-api.html', + name: 'REST API', }, { - url : '/intro-project-owner.html', - name : 'Project Owner Guide', - } + url: '/intro-project-owner.html', + name: 'Project Owner Guide', + }, ]; Polymer({ @@ -101,21 +75,21 @@ _account: Object, _adminLinks: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _defaultLinks: { type: Array, - value: function() { + value() { return DEFAULT_LINKS; }, }, _docBaseUrl: { type: String, + value: null, }, _links: { type: Array, - computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' + - '_docBaseUrl)', + computed: '_computeLinks(_defaultLinks, _userLinks, _docBaseUrl)', }, _loginURL: { type: String, @@ -123,7 +97,7 @@ }, _userLinks: { type: Array, - value: function() { return []; }, + value() { return []; }, }, }, @@ -135,21 +109,21 @@ '_accountLoaded(_account)', ], - attached: function() { + attached() { this._loadAccount(); this._loadConfig(); this.listen(window, 'location-change', '_handleLocationChange'); }, - detached: function() { + detached() { this.unlisten(window, 'location-change', '_handleLocationChange'); }, - reload: function() { + reload() { this._loadAccount(); }, - _handleLocationChange: function(e) { + _handleLocationChange(e) { if (this.getBaseUrl()) { // Strip the canonical path from the path since needing canonical in // the path is uneeded and breaks the url. @@ -165,40 +139,35 @@ } }, - _computeRelativeURL: function(path) { + _computeRelativeURL(path) { return '//' + window.location.host + this.getBaseUrl() + path; }, - _computeLinks: function(defaultLinks, userLinks, adminLinks, docBaseUrl) { - var links = defaultLinks.slice(); + _computeLinks(defaultLinks, userLinks, docBaseUrl) { + const links = defaultLinks.slice(); if (userLinks && userLinks.length > 0) { links.push({ title: 'Your', links: userLinks, }); } - if (adminLinks && adminLinks.length > 0) { - links.push({ - title: 'Admin', - links: adminLinks, - }); - } - var docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS); + const docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS); if (docLinks.length) { links.push({ title: 'Documentation', links: docLinks, + class: 'hideOnMobile', }); } return links; }, - _getDocLinks: function(docBaseUrl, docLinks) { + _getDocLinks(docBaseUrl, docLinks) { if (!docBaseUrl || !docLinks) { return []; } - return docLinks.map(function(link) { - var url = docBaseUrl; + return docLinks.map(link => { + let url = docBaseUrl; if (url && url[url.length - 1] === '/') { url = url.substring(0, url.length - 1); } @@ -210,66 +179,64 @@ }); }, - _loadAccount: function() { - this.$.restAPI.getAccount().then(function(account) { + _loadAccount() { + return this.$.restAPI.getAccount().then(account => { this._account = account; this.$.accountContainer.classList.toggle('loggedIn', account != null); this.$.accountContainer.classList.toggle('loggedOut', account == null); - }.bind(this)); + }); }, - _loadConfig: function() { - this.$.restAPI.getConfig().then(function(config) { + _loadConfig() { + this.$.restAPI.getConfig().then(config => { if (config && config.gerrit && config.gerrit.doc_url) { this._docBaseUrl = config.gerrit.doc_url; } if (!this._docBaseUrl) { return this._probeDocLink('/Documentation/index.html'); } - }.bind(this)); + }); }, - _probeDocLink: function(path) { - return this.$.restAPI.probePath(this.getBaseUrl() + path).then(function(ok) { + _probeDocLink(path) { + return this.$.restAPI.probePath(this.getBaseUrl() + path).then(ok => { if (ok) { this._docBaseUrl = this.getBaseUrl() + '/Documentation'; } else { this._docBaseUrl = null; } - }.bind(this)); + }); }, - _accountLoaded: function(account) { + _accountLoaded(account) { if (!account) { return; } - this.$.restAPI.getPreferences().then(function(prefs) { + this.$.restAPI.getPreferences().then(prefs => { this._userLinks = - prefs.my.map(this._stripHashPrefix).filter(this._isSupportedLink); - }.bind(this)); - this._loadAccountCapabilities(); + prefs.my.map(this._fixMyMenuItem).filter(this._isSupportedLink); + }); }, - _loadAccountCapabilities: function() { - var params = ['createProject', 'createGroup', 'viewPlugins']; - return this.$.restAPI.getAccountCapabilities(params) - .then(function(capabilities) { - this._adminLinks = ADMIN_LINKS.filter(function(link) { - return !link.capability || - capabilities.hasOwnProperty(link.capability); - }); - }.bind(this)); - }, - - _stripHashPrefix: function(linkObj) { - if (linkObj.url.indexOf('#') === 0) { + _fixMyMenuItem(linkObj) { + // Normalize all urls to PolyGerrit style. + if (linkObj.url.startsWith('#')) { linkObj.url = linkObj.url.slice(1); } + + // Delete target property due to complications of + // https://bugs.chromium.org/p/gerrit/issues/detail?id=5888 + // + // The server tries to guess whether URL is a view within the UI. + // If not, it sets target='_blank' on the menu item. The server + // makes assumptions that work for the GWT UI, but not PolyGerrit, + // so we'll just disable it altogether for now. + delete linkObj.target; return linkObj; }, - _isSupportedLink: function(linkObj) { + _isSupportedLink(linkObj) { // Groups are not yet supported. - return linkObj.url.indexOf('/groups') !== 0; + return !linkObj.url.startsWith('/groups'); }, }); })();
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html index 4582b4f..e4cc7bd 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-main-header.html"> <script>void(0);</script> @@ -33,107 +32,77 @@ </test-fixture> <script> - suite('gr-main-header tests', function() { - var element; - var sandbox; + suite('gr-main-header tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, - probePath: function(path) { return Promise.resolve(false); }, + getConfig() { return Promise.resolve({}); }, + probePath(path) { return Promise.resolve(false); }, }); stub('gr-main-header', { - _loadAccount: function() {}, + _loadAccount() {}, }); element = fixture('basic'); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('strip hash prefix', function() { + test('fix my menu item', () => { assert.deepEqual([ {url: '#/q/owner:self+is:draft'}, {url: 'https://awesometown.com/#hashyhash'}, - ].map(element._stripHashPrefix), - [ + {url: 'url', target: '_blank'}, + ].map(element._fixMyMenuItem), [ {url: '/q/owner:self+is:draft'}, {url: 'https://awesometown.com/#hashyhash'}, + {url: 'url'}, ]); }); - test('filter unsupported urls', function() { + test('filter unsupported urls', () => { assert.deepEqual([ {url: '/q/owner:self+is:draft'}, {url: '/c/331788/'}, {url: '/groups/self'}, {url: 'https://awesometown.com/#hashyhash'}, - ].filter(element._isSupportedLink), - [ + ].filter(element._isSupportedLink), [ {url: '/q/owner:self+is:draft'}, {url: '/c/331788/'}, {url: 'https://awesometown.com/#hashyhash'}, ]); }); - test('_loadAccountCapabilities admin', function(done) { - sandbox.stub(element.$.restAPI, 'getAccountCapabilities', function() { - return Promise.resolve({ - createGroup: true, - createProject: true, - viewPlugins: true, - }); - }); - element._loadAccountCapabilities().then(function() { - assert.equal(element._adminLinks.length, 5); - done(); - }); - }); - test('_loadAccountCapabilities non admin', function(done) { - sandbox.stub(element.$.restAPI, 'getAccountCapabilities', function() { - return Promise.resolve({}); - }); - element._loadAccountCapabilities().then(function() { - assert.equal(element._adminLinks.length, 2); - done(); - }); - }); - - test('user links', function() { - var defaultLinks = [{ + test('user links', () => { + const defaultLinks = [{ title: 'Faves', links: [{ name: 'Pinterest', url: 'https://pinterest.com', }], }]; - var userLinks = [{ + const userLinks = [{ name: 'Facebook', url: 'https://facebook.com', }]; - var adminLinks = [{ - url: '/admin/groups', - name: 'Groups', - }]; + // When no admin links are passed, it should use the default. + assert.deepEqual(element._computeLinks(defaultLinks, []), defaultLinks); assert.deepEqual( - element._computeLinks(defaultLinks, [], []), defaultLinks); - assert.deepEqual( - element._computeLinks(defaultLinks, userLinks, adminLinks), + element._computeLinks(defaultLinks, userLinks), defaultLinks.concat({ title: 'Your', links: userLinks, - }, { - title: 'Admin', - links: adminLinks, })); }); - test('documentation links', function() { - var docLinks = [ + test('documentation links', () => { + const docLinks = [ { name: 'Table of Contents', url: '/index.html',
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html new file mode 100644 index 0000000..47655cf --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -0,0 +1,205 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<script> + (function(window) { + 'use strict'; + + // Navigation parameters object format: + // + // Each object has a `view` property with a value from Gerrit.Nav.View. The + // remaining properties depend on the value used for view. + // + // - Gerrit.Nav.View.CHANGE: + // - `id`, required, String: the numeric ID of the change. + // + // - Gerrit.Nav.View.SEARCH: + // - `owner`, optional, String: the owner name. + // - `project`, optional, String: the project name. + // - `branch`, optional, String: the branch name. + // - `topic`, optional, String: the topic name. + // - `statuses`, optional, Array<String>: the list of change statuses to + // search for. If more than one is provided, the search will OR them + // together. + // + // - Gerrit.Nav.View.DIFF: + // - `changeId`, required, String: the numeric ID of the change. + // - `path`, required, String: the filepath of the diff. + // - `patchNum`, required, Number, the patch for the right-hand-side of + // the diff. + // - `basePatchNum`, optional, Number, the patch for the left-hand-side + // of the diff. If `basePatchNum` is provided, then `patchNum` must + // also be provided. + + window.Gerrit = window.Gerrit || {}; + + // Prevent redefinition. + if (window.Gerrit.hasOwnProperty('Nav')) { return; } + + const uninitialized = () => { + console.warn('Use of uninitialized routing'); + }; + + window.Gerrit.Nav = { + + View: { + CHANGE: 'change', + SEARCH: 'search', + DIFF: 'diff', + }, + + /** @type {Function} */ + _navigate: uninitialized, + + /** @type {Function} */ + _generateUrl: uninitialized, + + _checkPatchRange(patchNum, basePatchNum) { + if (basePatchNum && !patchNum) { + throw new Error('Cannot use base patch number without patch number.'); + } + }, + + /** + * Setup router implementation. + * @param {Function} handleNavigate + * @param {Function} generateUrl + */ + setup(navigate, generateUrl) { + this._navigate = navigate; + this._generateUrl = generateUrl; + }, + + destroy() { + this._navigate = uninitialized; + this._generateUrl = uninitialized; + }, + + /** + * Generate a URL for the given route parameters. + * @param {Object} params + * @return {String} + */ + _getUrlFor(params) { + return this._generateUrl(params); + }, + + /** + * @param {String} project The name of the project. + * @return {String} + */ + getUrlForProject(project) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + project, + }); + }, + + /** + * @param {String} branch The name of the branch. + * @param {String} project The name of the project. + * @param {String} status The status to search. + * @return {String} + */ + getUrlForBranch(branch, project, status) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + branch, + project, + statuses: [status], + }); + }, + + /** + * @param {String} topic The name of the topic. + * @return {String} + */ + getUrlForTopic(topic) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + topic, + statuses: ['open', 'merged'], + }); + }, + + /** + * @param {!Object} change The change object. + * @param {Number} opt_patchNum + * @param {Number} opt_basePatchNum + * @return {String} + */ + getUrlForChange(change, opt_patchNum, opt_basePatchNum) { + this._checkPatchRange(opt_patchNum, opt_basePatchNum); + return this._getUrlFor({ + view: Gerrit.Nav.View.CHANGE, + id: change._number, + patchNum: opt_patchNum, + basePatchNum: opt_basePatchNum, + }); + }, + + /** + * @param {!Object} change The change object. + * @param {Number} opt_patchNum + * @param {Number} opt_basePatchNum + * @return {String} + */ + navigateToChange(change, opt_patchNum, opt_basePatchNum) { + this._navigate(this.getUrlForChange(change, opt_patchNum, + opt_basePatchNum)); + }, + + /** + * @param {!Object} change The change object. + * @param {!String} path The file path. + * @param {Number} opt_patchNum + * @param {Number} opt_basePatchNum + * @return {String} + */ + getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum) { + this._checkPatchRange(opt_patchNum, opt_basePatchNum); + return this._getUrlFor({ + view: Gerrit.Nav.View.DIFF, + changeId: change._number, + path, + patchNum: opt_patchNum, + basePatchNum: opt_basePatchNum, + }); + }, + + /** + * @param {!Object} change The change object. + * @param {!String} path The file path. + * @param {Number} opt_patchNum + * @param {Number} opt_basePatchNum + */ + navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) { + this._navigate(this.getUrlForDiff(change, path, opt_patchNum, + opt_basePatchNum)); + }, + + /** + * @param {String} owner The name of the owner. + * @return {String} + */ + getUrlForOwner(owner) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + owner, + }); + }, + }; + })(window); +</script>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html new file mode 100644 index 0000000..b829e83 --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
@@ -0,0 +1,32 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-navigation</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> + +<script> + suite('gr-navigation tests', () => { + test('invalid patch ranges throw exceptions', () => { + assert.throw(() => Gerrit.Nav.getUrlForChange('123', undefined, 12)); + assert.throw(() => Gerrit.Nav.getUrlForDiff('123', 'x.c', undefined, 12)); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js index 1f96014..2296567 100644 --- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -15,7 +15,7 @@ 'use strict'; // Latency reporting constants. - var TIMING = { + const TIMING = { TYPE: 'timing-report', CATEGORY: 'UI Latency', // Reported events - alphabetize below. @@ -24,25 +24,25 @@ }; // Navigation reporting constants. - var NAVIGATION = { + const NAVIGATION = { TYPE: 'nav-report', CATEGORY: 'Location Changed', PAGE: 'Page', }; - var ERROR = { + const ERROR = { TYPE: 'error', CATEGORY: 'exception', }; - var INTERACTION_TYPE = 'interaction'; + const INTERACTION_TYPE = 'interaction'; - var CHANGE_VIEW_REGEX = /^\/c\/\d+\/?\d*$/; - var DIFF_VIEW_REGEX = /^\/c\/\d+\/\d+\/.+$/; + const CHANGE_VIEW_REGEX = /^\/c\/\d+\/?\d*$/; + const DIFF_VIEW_REGEX = /^\/c\/\d+\/\d+\/.+$/; - var pending = []; + const pending = []; - var onError = function(oldOnError, msg, url, line, column, error) { + const onError = function(oldOnError, msg, url, line, column, error) { if (oldOnError) { oldOnError(msg, url, line, column, error); } @@ -51,23 +51,23 @@ column = column || error.columnNumber; msg = msg || error.toString(); } - var payload = { - url: url, - line: line, - column: column, - error: error, + const payload = { + url, + line, + column, + error, }; GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload); return true; }; - var catchErrors = function(opt_context) { - var context = opt_context || window; + const catchErrors = function(opt_context) { + const context = opt_context || window; context.onerror = onError.bind(null, context.onerror); }; catchErrors(); - var GrReporting = Polymer({ + const GrReporting = Polymer({ is: 'gr-reporting', properties: { @@ -75,7 +75,7 @@ _baselines: { type: Array, - value: function() { return {}; }, + value() { return {}; }, }, }, @@ -87,24 +87,24 @@ return window.performance.timing; }, - now: function() { + now() { return Math.round(10 * window.performance.now()) / 10; }, - reporter: function() { - var report = (Gerrit._arePluginsLoaded() && !pending.length) ? + reporter(...args) { + const report = (Gerrit._arePluginsLoaded() && !pending.length) ? this.defaultReporter : this.cachingReporter; - report.apply(this, arguments); + report.apply(this, args); }, - defaultReporter: function(type, category, eventName, eventValue) { - var detail = { - type: type, - category: category, + defaultReporter(type, category, eventName, eventValue) { + const detail = { + type, + category, name: eventName, value: eventValue, }; - document.dispatchEvent(new CustomEvent(type, {detail: detail})); + document.dispatchEvent(new CustomEvent(type, {detail})); if (type === ERROR.TYPE) { console.error(eventValue.error || eventName); } else { @@ -113,15 +113,15 @@ } }, - cachingReporter: function(type, category, eventName, eventValue) { + cachingReporter(type, category, eventName, eventValue) { if (type === ERROR.TYPE) { console.error(eventValue.error || eventName); } if (Gerrit._arePluginsLoaded()) { if (pending.length) { - pending.splice(0).forEach(function(args) { - this.reporter.apply(this, args); - }, this); + for (const args of pending.splice(0)) { + this.reporter(...args); + } } this.reporter(type, category, eventName, eventValue); } else { @@ -132,8 +132,8 @@ /** * User-perceived app start time, should be reported when the app is ready. */ - appStarted: function() { - var startTime = + appStarted() { + const startTime = new Date().getTime() - this.performanceTiming.navigationStart; this.reporter( TIMING.TYPE, TIMING.CATEGORY, TIMING.APP_STARTED, startTime); @@ -142,22 +142,22 @@ /** * Page load time, should be reported at any time after navigation. */ - pageLoaded: function() { + pageLoaded() { if (this.performanceTiming.loadEventEnd === 0) { console.error('pageLoaded should be called after window.onload'); this.async(this.pageLoaded, 100); } else { - var loadTime = this.performanceTiming.loadEventEnd - + const loadTime = this.performanceTiming.loadEventEnd - this.performanceTiming.navigationStart; this.reporter( - TIMING.TYPE, TIMING.CATEGORY, TIMING.PAGE_LOADED, loadTime); + TIMING.TYPE, TIMING.CATEGORY, TIMING.PAGE_LOADED, loadTime); } }, - locationChanged: function() { - var page = ''; - var pathname = this._getPathname(); - if (pathname.indexOf('/q/') === 0) { + locationChanged() { + let page = ''; + const pathname = this._getPathname(); + if (pathname.startsWith('/q/')) { page = this.getBaseUrl() + '/q/'; } else if (pathname.match(CHANGE_VIEW_REGEX)) { // change view page = this.getBaseUrl() + '/c/'; @@ -171,32 +171,32 @@ NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page); }, - pluginsLoaded: function() { + pluginsLoaded() { this.timeEnd('PluginsLoaded'); }, - _getPathname: function() { + _getPathname() { return '/' + window.location.pathname.substring(this.getBaseUrl().length); }, /** * Reset named timer. */ - time: function(name) { + time(name) { this._baselines[name] = this.now(); }, /** * Finish named timer and report it to server. */ - timeEnd: function(name) { - var baseTime = this._baselines[name] || 0; - var time = this.now() - baseTime; + timeEnd(name) { + const baseTime = this._baselines[name] || 0; + const time = this.now() - baseTime; this.reporter(TIMING.TYPE, TIMING.CATEGORY, name, time); delete this._baselines[name]; }, - reportInteraction: function(eventName, opt_msg) { + reportInteraction(eventName, opt_msg) { this.reporter(INTERACTION_TYPE, this.category, eventName, opt_msg); }, }); @@ -204,5 +204,4 @@ window.GrReporting = GrReporting; // Expose onerror installation so it would be accessible from tests. window.GrReporting._catchErrors = catchErrors; - })();
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html index 2720ebd..08f72b7 100644 --- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -20,7 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-reporting.html"> <script>void(0);</script> @@ -32,15 +32,15 @@ </test-fixture> <script> - suite('gr-reporting tests', function() { - var element; - var sandbox; - var clock; - var fakePerformance; + suite('gr-reporting tests', () => { + let element; + let sandbox; + let clock; + let fakePerformance; - var NOW_TIME = 100; + const NOW_TIME = 100; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); clock = sinon.useFakeTimers(NOW_TIME); element = fixture('basic'); @@ -49,15 +49,15 @@ loadEventEnd: 2, }; sinon.stub(element, 'performanceTiming', - {get: function() {return fakePerformance;}}); + {get() {return fakePerformance;}}); sandbox.stub(element, 'reporter'); }); - teardown(function() { + teardown(() => { sandbox.restore(); clock.restore(); }); - test('appStarted', function() { + test('appStarted', () => { element.appStarted(); assert.isTrue( element.reporter.calledWithExactly( @@ -66,7 +66,7 @@ )); }); - test('pageLoaded', function() { + test('pageLoaded', () => { element.pageLoaded(); assert.isTrue( element.reporter.calledWithExactly( @@ -75,8 +75,8 @@ ); }); - test('time and timeEnd', function() { - var nowStub = sandbox.stub(element, 'now').returns(0); + test('time and timeEnd', () => { + const nowStub = sandbox.stub(element, 'now').returns(0); element.time('foo'); nowStub.returns(1); element.time('bar'); @@ -92,14 +92,14 @@ )); }); - suite('plugins', function() { - setup(function() { + suite('plugins', () => { + setup(() => { element.reporter.restore(); sandbox.stub(element, 'defaultReporter'); sandbox.stub(Gerrit, '_arePluginsLoaded'); }); - test('pluginsLoaded reports time', function() { + test('pluginsLoaded reports time', () => { Gerrit._arePluginsLoaded.returns(true); sandbox.stub(element, 'now').returns(42); element.pluginsLoaded(); @@ -108,19 +108,19 @@ )); }); - test('caches reports if plugins are not loaded', function() { + test('caches reports if plugins are not loaded', () => { Gerrit._arePluginsLoaded.returns(false); element.timeEnd('foo'); assert.isFalse(element.defaultReporter.called); }); - test('reports if plugins are loaded', function() { + test('reports if plugins are loaded', () => { Gerrit._arePluginsLoaded.returns(true); element.timeEnd('foo'); assert.isTrue(element.defaultReporter.called); }); - test('reports cached events preserving order', function() { + test('reports cached events preserving order', () => { Gerrit._arePluginsLoaded.returns(false); element.timeEnd('foo'); Gerrit._arePluginsLoaded.returns(true); @@ -134,38 +134,38 @@ }); }); - suite('location changed', function() { - var pathnameStub; - setup(function() { + suite('location changed', () => { + let pathnameStub; + setup(() => { pathnameStub = sinon.stub(element, '_getPathname'); }); - teardown(function() { + teardown(() => { pathnameStub.restore(); }); - test('search', function() { + test('search', () => { pathnameStub.returns('/q/foo'); element.locationChanged(); assert.isTrue(element.reporter.calledWithExactly( 'nav-report', 'Location Changed', 'Page', '/q/')); }); - test('change view', function() { + test('change view', () => { pathnameStub.returns('/c/42/'); element.locationChanged(); assert.isTrue(element.reporter.calledWithExactly( 'nav-report', 'Location Changed', 'Page', '/c/')); }); - test('change view', function() { + test('change view', () => { pathnameStub.returns('/c/41/2'); element.locationChanged(); assert.isTrue(element.reporter.calledWithExactly( 'nav-report', 'Location Changed', 'Page', '/c/')); }); - test('diff view', function() { + test('diff view', () => { pathnameStub.returns('/c/41/2/file.txt'); element.locationChanged(); assert.isTrue(element.reporter.calledWithExactly( @@ -173,36 +173,35 @@ }); }); - suite('exception logging', function() { - var fakeWindow; - var reporter; + suite('exception logging', () => { + let fakeWindow; + let reporter; - var emulateThrow = function(msg, url, line, column, error) { + const emulateThrow = function(msg, url, line, column, error) { return fakeWindow.onerror(msg, url, line, column, error); }; - setup(function() { + setup(() => { reporter = sandbox.stub(GrReporting.prototype, 'reporter'); fakeWindow = {}; sandbox.stub(console, 'error'); window.GrReporting._catchErrors(fakeWindow); }); - test('is reported', function() { - var error = new Error('bar'); + test('is reported', () => { + const error = new Error('bar'); emulateThrow('bar', 'http://url', 4, 2, error); - assert.isTrue( - reporter.calledWith('error', 'exception', 'bar')); - var payload = reporter.lastCall.args[3]; + assert.isTrue(reporter.calledWith('error', 'exception', 'bar')); + const payload = reporter.lastCall.args[3]; assert.deepEqual(payload, { url: 'http://url', line: 4, column: 2, - error: error, + error, }); }); - test('prevent default event handler', function() { + test('prevent default event handler', () => { assert.isTrue(emulateThrow()); }); });
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.html b/polygerrit-ui/app/elements/core/gr-router/gr-router.html index 5a494b1..d311a6b 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router.html +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
@@ -14,7 +14,9 @@ limitations under the License. --> <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> +<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../gr-reporting/gr-reporting.html">
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js index cc8d478..2e3d30c 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -16,12 +16,12 @@ // Polymer makes `app` intrinsically defined on the window by virtue of the // custom element having the id "app", but it is made explicit here. - var app = document.querySelector('#app'); + const app = document.querySelector('#app'); if (!app) { console.log('No gr-app found (running tests)'); } - var _reporting; + let _reporting; function getReporting() { if (!_reporting) { _reporting = document.createElement('gr-reporting'); @@ -33,26 +33,30 @@ getReporting().pageLoaded(); }; - window.addEventListener('WebComponentsReady', function() { + window.addEventListener('WebComponentsReady', () => { getReporting().timeEnd('WebComponentsReady'); }); - function startRouter() { - var base = window.Gerrit.BaseUrlBehavior.getBaseUrl(); + const encode = window.Gerrit.URLEncodingBehavior.encodeURL; + + function startRouter(generateUrl) { + const base = window.Gerrit.BaseUrlBehavior.getBaseUrl(); if (base) { page.base(base); } - var restAPI = document.createElement('gr-rest-api-interface'); - var reporting = getReporting(); + const restAPI = document.createElement('gr-rest-api-interface'); + const reporting = getReporting(); + + Gerrit.Nav.setup(url => { page.show(url); }, generateUrl); // Middleware - page(function(ctx, next) { + page((ctx, next) => { document.body.scrollTop = 0; // Fire asynchronously so that the URL is changed by the time the event // is processed. - app.async(function() { + app.async(() => { app.fire('location-change', { hash: window.location.hash, pathname: window.location.pathname, @@ -63,13 +67,13 @@ }); function loadUser(ctx, next) { - restAPI.getLoggedIn().then(function() { + restAPI.getLoggedIn().then(() => { next(); }); } // Routes. - page('/', loadUser, function(data) { + page('/', loadUser, data => { if (data.querystring.match(/^closeAfterLogin/)) { // Close child window on redirect after login. window.close(); @@ -81,14 +85,14 @@ if (data.hash[0] !== '/') { data.hash = '/' + data.hash; } - var newUrl = data.hash; - if (newUrl.indexOf('/VE/') === 0) { + let newUrl = data.hash; + if (newUrl.startsWith('/VE/')) { newUrl = '/settings' + data.hash; } page.redirect(newUrl); return; } - restAPI.getLoggedIn().then(function(loggedIn) { + restAPI.getLoggedIn().then(loggedIn => { if (loggedIn) { page.redirect('/dashboard/self'); } else { @@ -97,8 +101,8 @@ }); }); - page('/dashboard/(.*)', loadUser, function(data) { - restAPI.getLoggedIn().then(function(loggedIn) { + page('/dashboard/(.*)', loadUser, data => { + restAPI.getLoggedIn().then(loggedIn => { if (loggedIn) { data.params.view = 'gr-dashboard-view'; app.params = data.params; @@ -108,10 +112,192 @@ }); }); - page('/admin/(.*)', loadUser, function(data) { - restAPI.getLoggedIn().then(function(loggedIn) { + // Matches /admin/groups[,<offset>][/]. + page(/^\/admin\/groups(,(\d+))?(\/)?$/, loadUser, data => { + restAPI.getLoggedIn().then(loggedIn => { + if (loggedIn) { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-admin-group-list', + offset: data.params[1] || 0, + filter: null, + }; + } else { + page.redirect('/login/' + encodeURIComponent(data.canonicalPath)); + } + }); + }); + + page('/admin/groups/q/filter::filter,:offset', loadUser, data => { + restAPI.getLoggedIn().then(loggedIn => { + if (loggedIn) { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-admin-group-list', + offset: data.params.offset, + filter: data.params.filter, + }; + } else { + page.redirect('/login/' + encodeURIComponent(data.canonicalPath)); + } + }); + }); + + page('/admin/groups/q/filter::filter', loadUser, data => { + restAPI.getLoggedIn().then(loggedIn => { + if (loggedIn) { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-admin-group-list', + filter: data.params.filter || null, + }; + } else { + page.redirect('/login/' + encodeURIComponent(data.canonicalPath)); + } + }); + }); + + // Matches /admin/create-project. + page('/admin/create-project', loadUser, data => { + restAPI.getLoggedIn().then(loggedIn => { + restAPI.getAccountCapabilities(false).then(permission => { + if (loggedIn && + (permission.administrateServer || permission.createProject)) { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-admin-create-project', + }; + } else { + page.redirect('/login/' + encodeURIComponent(data.canonicalPath)); + } + }); + }); + }); + + // Matches /admin/projects/<project>,branches[,<offset>]. + page(/^\/admin\/projects\/(.+),branches(,(.+))?$/, loadUser, data => { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-project-detail-list', + detailType: 'branches', + project: data.params[0], + offset: data.params[2] || 0, + filter: null, + }; + }); + + page('/admin/projects/:project,branches/q/filter::filter,:offset', + loadUser, data => { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-project-detail-list', + detailType: 'branches', + project: data.params.project, + offset: data.params.offset, + filter: data.params.filter, + }; + }); + + page('/admin/projects/:project,branches/q/filter::filter', + loadUser, data => { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-project-detail-list', + detailType: 'branches', + project: data.params.project, + filter: data.params.filter || null, + }; + }); + + // Matches /admin/projects/<project>,tags[,<offset>]. + page(/^\/admin\/projects\/(.+),tags(,(.+))?$/, loadUser, data => { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-project-detail-list', + detailType: 'tags', + project: data.params[0], + offset: data.params[2] || 0, + filter: null, + }; + }); + + page('/admin/projects/:project,tags/q/filter::filter,:offset', + loadUser, data => { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-project-detail-list', + detailType: 'tags', + project: data.params.project, + offset: data.params.offset, + filter: data.params.filter, + }; + }); + + page('/admin/projects/:project,tags/q/filter::filter', + loadUser, data => { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-project-detail-list', + detailType: 'tags', + project: data.params.project, + filter: data.params.filter || null, + }; + }); + + // Matches /admin/projects[,<offset>][/]. + page(/^\/admin\/projects(,(\d+))?(\/)?$/, loadUser, data => { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-admin-project-list', + offset: data.params[1] || 0, + filter: null, + }; + }); + + page('/admin/projects/q/filter::filter,:offset', loadUser, data => { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-admin-project-list', + offset: data.params.offset, + filter: data.params.filter, + }; + }); + + page('/admin/projects/q/filter::filter', loadUser, data => { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-admin-project-list', + filter: data.params.filter || null, + }; + }); + + // Matches /admin/projects/<project> + page(/^\/admin\/projects\/(.+)$/, loadUser, data => { + app.params = { + view: 'gr-admin-view', + project: data.params[0], + adminView: 'gr-admin-project', + }; + }); + + page(/^\/admin\/plugins(\/)?$/, loadUser, data => { + restAPI.getLoggedIn().then(loggedIn => { + if (loggedIn) { + app.params = { + view: 'gr-admin-view', + adminView: 'gr-admin-plugin-list', + }; + } else { + page.redirect('/login/' + encodeURIComponent(data.canonicalPath)); + } + }); + }); + + page('/admin/(.*)', loadUser, data => { + restAPI.getLoggedIn().then(loggedIn => { if (loggedIn) { data.params.view = 'gr-admin-view'; + data.params.placeholder = true; app.params = data.params; } else { page.redirect('/login/' + encodeURIComponent(data.canonicalPath)); @@ -127,7 +313,7 @@ page('/q/:query,:offset', queryHandler); page('/q/:query', queryHandler); - page(/^\/(\d+)\/?/, function(ctx) { + page(/^\/(\d+)\/?/, ctx => { page.redirect('/c/' + encodeURIComponent(ctx.params[0])); }); @@ -139,9 +325,9 @@ } // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>]. - page(/^\/c\/(\d+)\/?(((\d+)(\.\.(\d+))?))?$/, function(ctx) { + page(/^\/c\/(\d+)\/?(((\d+)(\.\.(\d+))?))?$/, ctx => { // Parameter order is based on the regex group number matched. - var params = { + const params = { changeNum: ctx.params[0], basePatchNum: ctx.params[3], patchNum: ctx.params[5], @@ -163,9 +349,9 @@ }); // Matches /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>. - page(/^\/c\/(\d+)\/((\d+)(\.\.(\d+))?)\/(.+)/, function(ctx) { + page(/^\/c\/(\d+)\/((\d+)(\.\.(\d+))?)\/(.+)/, ctx => { // Parameter order is based on the regex group number matched. - var params = { + const params = { changeNum: ctx.params[0], basePatchNum: ctx.params[2], patchNum: ctx.params[4], @@ -177,7 +363,7 @@ // TODO(kaspern): Utilize gr-url-encoding-behavior.html when the router // is replaced with a Polymer counterpart. // @see Issue 4255 regarding double-encoding. - var path = encodeURIComponent(encodeURIComponent(params.path)); + let path = encodeURIComponent(encodeURIComponent(params.path)); // @see Issue 4577 regarding more readable URLs. path = path.replace(/%252F/g, '/'); path = path.replace(/%2520/g, '+'); @@ -194,7 +380,7 @@ // Check if path has an '@' which indicates it was using GWT style line // numbers. Even if the filename had an '@' in it, it would have already // been URI encoded. Redirect to hash version of path. - if (ctx.path.indexOf('@') !== -1) { + if (ctx.path.includes('@')) { page.redirect(ctx.path.replace('@', '#')); return; } @@ -203,8 +389,8 @@ app.params = params; }); - page(/^\/settings\/(agreements|new-agreement)/, loadUser, function(data) { - restAPI.getLoggedIn().then(function(loggedIn) { + page(/^\/settings\/(agreements|new-agreement)/, loadUser, data => { + restAPI.getLoggedIn().then(loggedIn => { if (loggedIn) { data.params.view = 'gr-cla-view'; app.params = data.params; @@ -214,8 +400,8 @@ }); }); - page(/^\/settings\/VE\/(\S+)/, function(data) { - restAPI.getLoggedIn().then(function(loggedIn) { + page(/^\/settings\/VE\/(\S+)/, data => { + restAPI.getLoggedIn().then(loggedIn => { if (loggedIn) { app.params = { view: 'gr-settings-view', @@ -227,8 +413,8 @@ }); }); - page(/^\/settings\/?/, function(data) { - restAPI.getLoggedIn().then(function(loggedIn) { + page(/^\/settings\/?/, data => { + restAPI.getLoggedIn().then(loggedIn => { if (loggedIn) { app.params = {view: 'gr-settings-view'}; } else { @@ -237,9 +423,10 @@ }); }); - page(/^\/register(\/.*)?/, function(ctx) { + page(/^\/register(\/.*)?/, ctx => { app.params = {justRegistered: true}; - var path = ctx.params[0] || '/'; + const path = ctx.params[0] || '/'; + if (path[0] !== '/') { return; } page.show(path); }); @@ -248,9 +435,62 @@ Polymer({ is: 'gr-router', - start: function() { + start() { if (!app) { return; } - startRouter(); + startRouter(this._generateUrl.bind(this)); + }, + + _generateUrl(params) { + const base = window.Gerrit.BaseUrlBehavior.getBaseUrl(); + let url = ''; + + if (params.view === Gerrit.Nav.View.SEARCH) { + const operators = []; + if (params.owner) { + operators.push('owner:' + encode(params.owner)); + } + if (params.project) { + operators.push('project:' + encode(params.project)); + } + if (params.branch) { + operators.push('branch:' + encode(params.branch)); + } + if (params.topic) { + operators.push('topic:"' + encode(params.topic) + '"'); + } + if (params.statuses) { + if (params.statuses.length === 1) { + operators.push('status:' + encode(params.statuses[0])); + } else if (params.statuses.length > 1) { + operators.push( + '(' + + params.statuses.map(s => `status:${encode(s)}`).join(' OR ') + + ')'); + } + } + url = '/q/' + operators.join('+'); + } else if (params.view === Gerrit.Nav.View.CHANGE) { + let range = this._getPatchRangeExpression(params); + if (range.length) { range = '/' + range; } + + url = `/c/${params.id}${range}`; + } else if (params.view === Gerrit.Nav.View.DIFF) { + let range = this._getPatchRangeExpression(params); + if (range.length) { range = '/' + range; } + + url = `/c/${params.changeId}${range}/${encode(params.path, true)}`; + } else { + throw new Error('Can\'t generate'); + } + + return base + url; + }, + + _getPatchRangeExpression(params) { + let range = ''; + if (params.patchNum) { range = '' + params.patchNum; } + if (params.basePatchNum) { range = params.basePatchNum + '..' + range; } + return range; }, }); })();
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html new file mode 100644 index 0000000..143be9c --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -0,0 +1,99 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-router</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-router.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-router></gr-router> + </template> +</test-fixture> + +<script> + suite('gr-router tests', () => { + suite('generateUrl', () => { + let element; + + setup(() => { + element = fixture('basic'); + }); + + test('search', () => { + let params = { + view: Gerrit.Nav.View.SEARCH, + owner: 'a%b', + project: 'c%d', + branch: 'e%f', + topic: 'g%h', + statuses: ['op%en'], + }; + assert.equal(element._generateUrl(params), + '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' + + 'topic:"g%2525h"+status:op%2525en'); + + params = { + view: Gerrit.Nav.View.SEARCH, + statuses: ['a', 'b', 'c'], + }; + assert.equal(element._generateUrl(params), + '/q/(status:a OR status:b OR status:c)'); + }); + + test('change', () => { + const params = { + view: Gerrit.Nav.View.CHANGE, + id: '1234', + }; + assert.equal(element._generateUrl(params), '/c/1234'); + + params.patchNum = 10; + assert.equal(element._generateUrl(params), '/c/1234/10'); + + params.basePatchNum = 5; + assert.equal(element._generateUrl(params), '/c/1234/5..10'); + }); + + test('diff', () => { + const params = { + view: Gerrit.Nav.View.DIFF, + changeId: '42', + path: 'x+y/path.cpp', + patchNum: 12, + }; + assert.equal(element._generateUrl(params), '/c/42/12/x%252By/path.cpp'); + + params.basePatchNum = 6; + assert.equal(element._generateUrl(params), + '/c/42/6..12/x%252By/path.cpp'); + + params.path = 'foo bar/my+file.txt%'; + params.patchNum = 2; + delete params.basePatchNum; + assert.equal(element._generateUrl(params), + '/c/42/2/foo+bar/my%252Bfile.txt%2525'); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html index 7a63810..12f9fea 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -20,11 +20,11 @@ <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-search-bar"> <template> - <style> + <style include="shared-styles"> :host { display: inline-block; } @@ -55,6 +55,7 @@ allowNonSuggestedValues multi borderless + threshold="[[_threshold]]" tab-complete-without-commit></gr-autocomplete> <gr-button id="searchButton">Search</gr-button> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js index d4c8380..69a247b 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -15,7 +15,7 @@ 'use strict'; // Possible static search options for auto complete. - var SEARCH_OPERATORS = [ + const SEARCH_OPERATORS = [ 'added', 'age', 'age:1week', // Give an example age @@ -44,6 +44,7 @@ 'is:abandoned', 'is:closed', 'is:draft', + 'is:ignored', 'is:mergeable', 'is:merged', 'is:open', @@ -80,11 +81,12 @@ 'tr', ]; - var SELF_EXPRESSION = 'self'; + const SELF_EXPRESSION = 'self'; + const ME_EXPRESSION = 'me'; - var MAX_AUTOCOMPLETE_RESULTS = 10; + const MAX_AUTOCOMPLETE_RESULTS = 10; - var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g; + const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g; Polymer({ is: 'gr-search-bar', @@ -111,22 +113,26 @@ }, keyEventTarget: { type: Object, - value: function() { return document.body; }, + value() { return document.body; }, }, query: { type: Function, - value: function() { + value() { return this._getSearchSuggestions.bind(this); }, }, _inputVal: String, + _threshold: { + type: Number, + value: 3, + }, }, - _valueChanged: function(value) { + _valueChanged(value) { this._inputVal = value; }, - _handleInputCommit: function(e) { + _handleInputCommit(e) { this._preventDefaultAndNavigateToInputVal(e); }, @@ -138,9 +144,9 @@ * * @param {!Event} e */ - _preventDefaultAndNavigateToInputVal: function(e) { + _preventDefaultAndNavigateToInputVal(e) { e.preventDefault(); - var target = Polymer.dom(e).rootTarget; + const target = Polymer.dom(e).rootTarget; // If the target is the #searchInput or has a sub-input component, that // is what holds the focus as opposed to the target from the DOM event. if (target.$.input) { @@ -162,22 +168,25 @@ * @return {!Promise} This returns a promise that resolves to an array of * strings. */ - _fetchAccounts: function(predicate, expression) { + _fetchAccounts(predicate, expression) { if (expression.length === 0) { return Promise.resolve([]); } return this.$.restAPI.getSuggestedAccounts( expression, MAX_AUTOCOMPLETE_RESULTS) - .then(function(accounts) { + .then(accounts => { if (!accounts) { return []; } - return accounts.map(function(acct) { - return predicate + ':"' + acct.name + ' <' + acct.email + '>"'; - }); - }).then(function(accounts) { + return accounts.map(acct => + predicate + ':"' + acct.name + ' <' + acct.email + '>"'); + }).then(accounts => { // When the expression supplied is a beginning substring of 'self', // add it as an autocomplete option. - return SELF_EXPRESSION.indexOf(expression) === 0 ? - accounts.concat([predicate + ':' + SELF_EXPRESSION]) : - accounts; + if (SELF_EXPRESSION.startsWith(expression)) { + return accounts.concat([predicate + ':' + SELF_EXPRESSION]); + } else if (ME_EXPRESSION.startsWith(expression)) { + return accounts.concat([predicate + ':' + ME_EXPRESSION]); + } else { + return accounts; + } }); }, @@ -190,15 +199,15 @@ * @return {!Promise} This returns a promise that resolves to an array of * strings. */ - _fetchGroups: function(predicate, expression) { + _fetchGroups(predicate, expression) { if (expression.length === 0) { return Promise.resolve([]); } return this.$.restAPI.getSuggestedGroups( expression, MAX_AUTOCOMPLETE_RESULTS) - .then(function(groups) { + .then(groups => { if (!groups) { return []; } - var keys = Object.keys(groups); - return keys.map(function(key) { return predicate + ':' + key; }); + const keys = Object.keys(groups); + return keys.map(key => predicate + ':' + key); }); }, @@ -211,14 +220,14 @@ * @return {!Promise} This returns a promise that resolves to an array of * strings. */ - _fetchProjects: function(predicate, expression) { + _fetchProjects(predicate, expression) { return this.$.restAPI.getSuggestedProjects( expression, MAX_AUTOCOMPLETE_RESULTS) - .then(function(projects) { + .then(projects => { if (!projects) { return []; } - var keys = Object.keys(projects); - return keys.map(function(key) { return predicate + ':' + key; }); + const keys = Object.keys(projects); + return keys.map(key => predicate + ':' + key); }); }, @@ -229,11 +238,11 @@ * @return {!Promise} This returns a promise that resolves to an array of * strings. */ - _fetchSuggestions: function(input) { + _fetchSuggestions(input) { // Split the input on colon to get a two part predicate/expression. - var splitInput = input.split(':'); - var predicate = splitInput[0]; - var expression = splitInput[1] || ''; + const splitInput = input.split(':'); + const predicate = splitInput[0]; + const expression = splitInput[1] || ''; // Switch on the predicate to determine what to autocomplete. switch (predicate) { case 'ownerin': @@ -259,9 +268,7 @@ default: return Promise.resolve(SEARCH_OPERATORS - .filter(function(operator) { - return operator.indexOf(input) !== -1; - })); + .filter(operator => operator.includes(input))); } }, @@ -271,19 +278,19 @@ * @return {!Promise} This returns a promise that resolves to an array of * strings. */ - _getSearchSuggestions: function(input) { + _getSearchSuggestions(input) { // Allow spaces within quoted terms. - var tokens = input.match(TOKENIZE_REGEX); - var trimmedInput = tokens[tokens.length - 1].toLowerCase(); + const tokens = input.match(TOKENIZE_REGEX); + const trimmedInput = tokens[tokens.length - 1].toLowerCase(); return this._fetchSuggestions(trimmedInput) - .then(function(operators) { + .then(operators => { if (!operators || !operators.length) { return []; } return operators // Prioritize results that start with the input. - .sort(function(a, b) { - var aContains = a.toLowerCase().indexOf(trimmedInput); - var bContains = b.toLowerCase().indexOf(trimmedInput); + .sort((a, b) => { + const aContains = a.toLowerCase().indexOf(trimmedInput); + const bContains = b.toLowerCase().indexOf(trimmedInput); if (aContains === bContains) { return a.localeCompare(b); } @@ -298,7 +305,7 @@ // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results. .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1) // Map to an object to play nice with gr-autocomplete. - .map(function(operator) { + .map(operator => { return { name: operator, value: operator, @@ -307,7 +314,7 @@ }); }, - _handleForwardSlashKey: function(e) { + _handleForwardSlashKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; }
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html index 3ddc96b..059ea7d 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../bower_components/page/page.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-search-bar.html"> <script src="../../../scripts/util.js"></script> @@ -35,26 +35,26 @@ </test-fixture> <script> - suite('gr-search-bar tests', function() { - var element; + suite('gr-search-bar tests', () => { + let element; - setup(function() { + setup(() => { element = fixture('basic'); }); - test('value is propagated to _inputVal', function() { + test('value is propagated to _inputVal', () => { element.value = 'foo'; assert.equal(element._inputVal, 'foo'); }); - function getActiveElement() { + getActiveElement = () => { return document.activeElement.shadowRoot ? document.activeElement.shadowRoot.activeElement : document.activeElement; - } + }; - test('tap on search button triggers nav', function(done) { - sinon.stub(page, 'show', function() { + test('tap on search button triggers nav', done => { + sinon.stub(page, 'show', () => { page.show.restore(); assert.notEqual(getActiveElement(), element.$.searchInput); assert.notEqual(getActiveElement(), element.$.searchButton); @@ -64,8 +64,8 @@ MockInteractions.tap(element.$.searchButton); }); - test('enter in search input triggers nav', function(done) { - sinon.stub(page, 'show', function() { + test('enter in search input triggers nav', done => { + sinon.stub(page, 'show', () => { page.show.restore(); assert.notEqual(getActiveElement(), element.$.searchInput); assert.notEqual(getActiveElement(), element.$.searchButton); @@ -76,8 +76,8 @@ null, 'enter'); }); - test('search query should be double-escaped', function() { - var showStub = sinon.stub(page, 'show'); + test('search query should be double-escaped', () => { + const showStub = sinon.stub(page, 'show'); element.$.searchInput.text = 'fate/stay'; MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, null, 'enter'); @@ -85,9 +85,9 @@ showStub.restore(); }); - test('input blurred after commit', function() { - var showStub = sinon.stub(page, 'show'); - var blurSpy = sinon.spy(element.$.searchInput.$.input, 'blur'); + test('input blurred after commit', () => { + const showStub = sinon.stub(page, 'show'); + const blurSpy = sinon.spy(element.$.searchInput.$.input, 'blur'); element.$.searchInput.text = 'fate/stay'; MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, null, 'enter'); @@ -96,86 +96,97 @@ blurSpy.restore(); }); - test('empty search query does not trigger nav', function() { - var showSpy = sinon.spy(page, 'show'); + test('empty search query does not trigger nav', () => { + const showSpy = sinon.spy(page, 'show'); element.value = ''; MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, null, 'enter'); assert.isFalse(showSpy.called); }); - test('keyboard shortcuts', function() { - var focusSpy = sinon.spy(element.$.searchInput, 'focus'); - var selectAllSpy = sinon.spy(element.$.searchInput, 'selectAll'); + test('keyboard shortcuts', () => { + const focusSpy = sinon.spy(element.$.searchInput, 'focus'); + const selectAllSpy = sinon.spy(element.$.searchInput, 'selectAll'); MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/'); assert.isTrue(focusSpy.called); assert.isTrue(selectAllSpy.called); }); - suite('_getSearchSuggestions', function() { - setup(function() { - sinon.stub(element.$.restAPI, 'getSuggestedAccounts', function() { - return Promise.resolve([ + suite('_getSearchSuggestions', () => { + setup(() => { + sinon.stub(element.$.restAPI, 'getSuggestedAccounts', () => + Promise.resolve([ { name: 'fred', email: 'fred@goog.co', }, - ]); - }); - sinon.stub(element.$.restAPI, 'getSuggestedGroups', function() { - return Promise.resolve({ + ]) + ); + sinon.stub(element.$.restAPI, 'getSuggestedGroups', () => + Promise.resolve({ Polygerrit: 0, gerrit: 0, gerrittest: 0, - }); - }); - sinon.stub(element.$.restAPI, 'getSuggestedProjects', function() { - return Promise.resolve({ + }) + ); + sinon.stub(element.$.restAPI, 'getSuggestedProjects', () => + Promise.resolve({ Polygerrit: 0, - }); - }); + }) + ); }); - teardown(function() { + teardown(() => { element.$.restAPI.getSuggestedAccounts.restore(); element.$.restAPI.getSuggestedGroups.restore(); element.$.restAPI.getSuggestedProjects.restore(); }); - test('Autocompletes accounts', function(done) { - element._getSearchSuggestions('owner:fr').then(function(s) { + test('Autocompletes accounts', done => { + element._getSearchSuggestions('owner:fr').then(s => { assert.equal(s[0].value, 'owner:"fred <fred@goog.co>"'); done(); }); }); - test('Inserts self as option when valid', function(done) { - element._getSearchSuggestions('owner:s').then(function(s) { + test('Inserts self as option when valid', done => { + element._getSearchSuggestions('owner:s').then(s => { assert.equal(s[0].value, 'owner:self'); - }).then(function() { - element._getSearchSuggestions('owner:selfs').then(function(s) { + }).then(() => { + element._getSearchSuggestions('owner:selfs').then(s => { assert.notEqual(s[0].value, 'owner:self'); done(); }); }); }); - test('Autocompletes groups', function(done) { - element._getSearchSuggestions('ownerin:pol').then(function(s) { + test('Inserts me as option when valid', done => { + element._getSearchSuggestions('owner:m').then(s => { + assert.equal(s[0].value, 'owner:me'); + }).then(() => { + element._getSearchSuggestions('owner:meme').then(s => { + assert.notEqual(s[0].value, 'owner:me'); + done(); + }); + }); + }); + + test('Autocompletes groups', done => { + element._getSearchSuggestions('ownerin:pol').then(s => { assert.equal(s[0].value, 'ownerin:Polygerrit'); done(); }); }); - test('Autocompletes projects', function(done) { - element._getSearchSuggestions('project:pol').then(function(s) { + test('Autocompletes projects', done => { + element._getSearchSuggestions('project:pol').then(s => { assert.equal(s[0].value, 'project:Polygerrit'); done(); }); }); - test('Autocompletes simple searches', function(done) { - element._getSearchSuggestions('is:o').then(function(s) { + test('Autocompletes simple searches', done => { + element._getSearchSuggestions('is:o').then(s => { assert.equal(s[0].name, 'is:open'); assert.equal(s[0].value, 'is:open'); assert.equal(s[1].name, 'is:owner'); @@ -184,26 +195,25 @@ }); }); - test('Does not autocomplete with no match', function(done) { - element._getSearchSuggestions('asdasdasdasd').then(function(s) { + test('Does not autocomplete with no match', done => { + element._getSearchSuggestions('asdasdasdasd').then(s => { assert.equal(s.length, 0); done(); }); }); - test('Autocomplete doesnt override exact matches to input', - function(done) { - element._getSearchSuggestions('ownerin:gerrit').then(function(s) { + test('Autocomplete doesnt override exact matches to input', done => { + element._getSearchSuggestions('ownerin:gerrit').then(s => { assert.equal(s[0].value, 'ownerin:gerrit'); done(); }); }); - test('Autocomplete respects spaces', function(done) { - element._getSearchSuggestions('is:ope').then(function(s) { + test('Autocomplete respects spaces', done => { + element._getSearchSuggestions('is:ope').then(s => { assert.equal(s[0].name, 'is:open'); assert.equal(s[0].value, 'is:open'); - element._getSearchSuggestions('is:ope ').then(function(s) { + element._getSearchSuggestions('is:ope ').then(s => { assert.equal(s.length, 0); done(); });
diff --git a/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html b/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html new file mode 100644 index 0000000..33e8fe6 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html
@@ -0,0 +1,70 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-confirm-delete-comment-dialog"> + <template> + <style include="shared-styles"> + :host { + display: block; + } + :host([disabled]) { + opacity: .5; + pointer-events: none; + } + .main { + display: flex; + flex-direction: column; + width: 100%; + } + label { + cursor: pointer; + display: block; + width: 100%; + } + iron-autogrow-textarea { + font-family: var(--monospace-font-family); + padding: 0; + width: 73ch; /* Add a char to account for the border. */ + + --iron-autogrow-textarea { + border: 1px solid #ddd; + font-family: var(--monospace-font-family); + } + } + </style> + <gr-confirm-dialog + confirm-label="Delete" + on-confirm="_handleConfirmTap" + on-cancel="_handleCancelTap"> + <div class="header">Delete Comment</div> + <div class="main"> + <label for="messageInput">Enter comment delete reason</label> + <iron-autogrow-textarea + id="messageInput" + class="message" + autocomplete="on" + placeholder="<Insert reasoning here>" + bind-value="{{message}}"></iron-autogrow-textarea> + </div> + </gr-confirm-dialog> + </template> + <script src="gr-confirm-delete-comment-dialog.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js b/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js new file mode 100644 index 0000000..e0eb078 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
@@ -0,0 +1,50 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-confirm-delete-comment-dialog', + + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ + + /** + * Fired when the cancel button is pressed. + * + * @event cancel + */ + + properties: { + message: String, + }, + + resetFocus() { + this.$.messageInput.textarea.focus(); + }, + + _handleConfirmTap(e) { + e.preventDefault(); + this.fire('confirm', {reason: this.message}, {bubbles: false}); + }, + + _handleCancelTap(e) { + e.preventDefault(); + this.fire('cancel', null, {bubbles: false}); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js index 6b1e59e..683f2fc 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
@@ -17,6 +17,8 @@ // Prevent redefinition. if (window.GrDiffBuilderImage) { return; } + const IMAGE_MIME_PATTERN = /^image\/(bmp|gif|jpeg|jpg|png|tiff|webp)$/; + function GrDiffBuilderImage(diff, comments, prefs, outputEl, baseImage, revisionImage) { GrDiffBuilderSideBySide.call(this, diff, comments, prefs, outputEl, []); @@ -29,7 +31,7 @@ GrDiffBuilderImage.prototype.constructor = GrDiffBuilderImage; GrDiffBuilderImage.prototype.renderDiffImages = function() { - var section = this._createElement('tbody', 'image-diff'); + const section = this._createElement('tbody', 'image-diff'); this._emitImagePair(section); this._emitImageLabels(section); @@ -38,25 +40,30 @@ }; GrDiffBuilderImage.prototype._emitImagePair = function(section) { - var tr = this._createElement('tr'); + const tr = this._createElement('tr'); tr.appendChild(this._createElement('td')); - tr.appendChild(this._createImageCell(this._baseImage, 'left')); + tr.appendChild(this._createImageCell(this._baseImage, 'left', section)); tr.appendChild(this._createElement('td')); - tr.appendChild(this._createImageCell(this._revisionImage, 'right')); + tr.appendChild(this._createImageCell( + this._revisionImage, 'right', section)); section.appendChild(tr); }; - GrDiffBuilderImage.prototype._createImageCell = function(image, className) { - var td = this._createElement('td', className); - if (image) { - var imageEl = this._createElement('img'); + GrDiffBuilderImage.prototype._createImageCell = function(image, className, + section) { + const td = this._createElement('td', className); + if (image && IMAGE_MIME_PATTERN.test(image.type)) { + const imageEl = this._createElement('img'); + imageEl.onload = function() { + image._height = imageEl.naturalHeight; + image._width = imageEl.naturalWidth; + this._updateImageLabel(section, className, image); + }.bind(this); imageEl.src = 'data:' + image.type + ';base64, ' + image.body; - image._height = imageEl.naturalHeight; - image._width = imageEl.naturalWidth; - imageEl.addEventListener('error', function(e) { + imageEl.addEventListener('error', () => { imageEl.remove(); td.textContent = '[Image failed to load]'; }); @@ -65,20 +72,61 @@ return td; }; + GrDiffBuilderImage.prototype._updateImageLabel = function(section, className, + image) { + const label = Polymer.dom(section) + .querySelector('.' + className + ' span.label'); + this._setLabelText(label, image); + }; + + GrDiffBuilderImage.prototype._setLabelText = function(label, image) { + label.textContent = this._getImageLabel(image); + }; + GrDiffBuilderImage.prototype._emitImageLabels = function(section) { - var tr = this._createElement('tr'); + const tr = this._createElement('tr'); + + let addNamesInLabel = false; + + if (this._baseImage && this._revisionImage && + this._baseImage._name !== this._revisionImage._name) { + addNamesInLabel = true; + } tr.appendChild(this._createElement('td')); - var td = this._createElement('td', 'left'); - var label = this._createElement('label'); - label.textContent = this._getImageLabel(this._baseImage); + let td = this._createElement('td', 'left'); + let label = this._createElement('label'); + let nameSpan; + let labelSpan = this._createElement('span', 'label'); + + if (addNamesInLabel) { + nameSpan = this._createElement('span', 'name'); + nameSpan.textContent = this._baseImage._name; + label.appendChild(nameSpan); + label.appendChild(this._createElement('br')); + } + + this._setLabelText(labelSpan, this._baseImage, addNamesInLabel); + + label.appendChild(labelSpan); td.appendChild(label); tr.appendChild(td); tr.appendChild(this._createElement('td')); td = this._createElement('td', 'right'); label = this._createElement('label'); - label.textContent = this._getImageLabel(this._revisionImage); + labelSpan = this._createElement('span', 'label'); + + if (addNamesInLabel) { + nameSpan = this._createElement('span', 'name'); + nameSpan.textContent = this._revisionImage._name; + label.appendChild(nameSpan); + label.appendChild(this._createElement('br')); + } + + this._setLabelText(labelSpan, this._revisionImage, addNamesInLabel); + + label.appendChild(labelSpan); td.appendChild(label); tr.appendChild(td); @@ -87,7 +135,7 @@ GrDiffBuilderImage.prototype._getImageLabel = function(image) { if (image) { - var type = image.type || image._expectedType; + const type = image.type || image._expectedType; if (image._width && image._height) { return image._width + '⨉' + image._height + ' ' + type; } else {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js index cbc00b8..7606b6c 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-side-by-side.js
@@ -24,13 +24,16 @@ GrDiffBuilderSideBySide.prototype.constructor = GrDiffBuilderSideBySide; GrDiffBuilderSideBySide.prototype.buildSectionElement = function(group) { - var sectionEl = this._createElement('tbody', 'section'); + const sectionEl = this._createElement('tbody', 'section'); sectionEl.classList.add(group.type); if (this._isTotal(group)) { sectionEl.classList.add('total'); } - var pairs = group.getSideBySidePairs(); - for (var i = 0; i < pairs.length; i++) { + if (group.dueToRebase) { + sectionEl.classList.add('dueToRebase'); + } + const pairs = group.getSideBySidePairs(); + for (let i = 0; i < pairs.length; i++) { sectionEl.appendChild(this._createRow(sectionEl, pairs[i].left, pairs[i].right)); } @@ -38,11 +41,11 @@ }; GrDiffBuilderSideBySide.prototype.addColumns = function(outputEl, fontSize) { - var width = fontSize * 4; - var colgroup = document.createElement('colgroup'); + const width = fontSize * 4; + const colgroup = document.createElement('colgroup'); // Add left-side line number. - var col = document.createElement('col'); + let col = document.createElement('col'); col.setAttribute('width', width); colgroup.appendChild(col); @@ -62,10 +65,11 @@ GrDiffBuilderSideBySide.prototype._createRow = function(section, leftLine, rightLine) { - var row = this._createElement('tr'); + const row = this._createElement('tr'); row.classList.add('diff-row', 'side-by-side'); row.setAttribute('left-type', leftLine.type); row.setAttribute('right-type', rightLine.type); + row.tabIndex = -1; this._appendPair(section, row, leftLine, leftLine.beforeNumber, GrDiffBuilder.Side.LEFT); @@ -76,15 +80,15 @@ GrDiffBuilderSideBySide.prototype._appendPair = function(section, row, line, lineNumber, side) { - var lineEl = this._createLineEl(line, lineNumber, line.type, side); + const lineEl = this._createLineEl(line, lineNumber, line.type, side); lineEl.classList.add(side); row.appendChild(lineEl); - var action = this._createContextControl(section, line); + const action = this._createContextControl(section, line); if (action) { row.appendChild(action); } else { - var textEl = this._createTextEl(line, side); - var threadGroupEl = this._commentThreadGroupForLine(line, side); + const textEl = this._createTextEl(line, side); + const threadGroupEl = this._commentThreadGroupForLine(line, side); if (threadGroupEl) { textEl.appendChild(threadGroupEl); } @@ -94,7 +98,7 @@ GrDiffBuilderSideBySide.prototype._getNextContentOnSide = function( content, side) { - var tr = content.parentElement.parentElement; + let tr = content.parentElement.parentElement; while (tr = tr.nextSibling) { content = tr.querySelector( 'td.content .contentText[data-side="' + side + '"]');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js index 55a6bea..e6943dd 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified.js
@@ -24,24 +24,27 @@ GrDiffBuilderUnified.prototype.constructor = GrDiffBuilderUnified; GrDiffBuilderUnified.prototype.buildSectionElement = function(group) { - var sectionEl = this._createElement('tbody', 'section'); + const sectionEl = this._createElement('tbody', 'section'); sectionEl.classList.add(group.type); if (this._isTotal(group)) { sectionEl.classList.add('total'); } + if (group.dueToRebase) { + sectionEl.classList.add('dueToRebase'); + } - for (var i = 0; i < group.lines.length; ++i) { + for (let i = 0; i < group.lines.length; ++i) { sectionEl.appendChild(this._createRow(sectionEl, group.lines[i])); } return sectionEl; }; GrDiffBuilderUnified.prototype.addColumns = function(outputEl, fontSize) { - var width = fontSize * 4; - var colgroup = document.createElement('colgroup'); + const width = fontSize * 4; + const colgroup = document.createElement('colgroup'); // Add left-side line number. - var col = document.createElement('col'); + let col = document.createElement('col'); col.setAttribute('width', width); colgroup.appendChild(col); @@ -57,8 +60,8 @@ }; GrDiffBuilderUnified.prototype._createRow = function(section, line) { - var row = this._createElement('tr', line.type); - var lineEl = this._createLineEl(line, line.beforeNumber, + const row = this._createElement('tr', line.type); + let lineEl = this._createLineEl(line, line.beforeNumber, GrDiffLine.Type.REMOVE); lineEl.classList.add('left'); row.appendChild(lineEl); @@ -67,13 +70,14 @@ lineEl.classList.add('right'); row.appendChild(lineEl); row.classList.add('diff-row', 'unified'); + row.tabIndex = -1; - var action = this._createContextControl(section, line); + const action = this._createContextControl(section, line); if (action) { row.appendChild(action); } else { - var textEl = this._createTextEl(line); - var threadGroupEl = this._commentThreadGroupForLine(line); + const textEl = this._createTextEl(line); + const threadGroupEl = this._commentThreadGroupForLine(line); if (threadGroupEl) { textEl.appendChild(threadGroupEl); } @@ -84,7 +88,7 @@ GrDiffBuilderUnified.prototype._getNextContentOnSide = function( content, side) { - var tr = content.parentElement.parentElement; + let tr = content.parentElement.parentElement; while (tr = tr.nextSibling) { if (tr.classList.contains('both') || ( (side === 'left' && tr.classList.contains('remove')) ||
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html index a936934..b98959b 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -48,12 +48,12 @@ (function() { 'use strict'; - var DiffViewMode = { + const DiffViewMode = { SIDE_BY_SIDE: 'SIDE_BY_SIDE', UNIFIED: 'UNIFIED_DIFF', }; - var TimingLabel = { + const TimingLabel = { TOTAL: 'Diff Total Render', CONTENT: 'Diff Content Render', SYNTAX: 'Diff Syntax Render', @@ -61,9 +61,9 @@ // If any line of the diff is more than the character limit, then disable // syntax highlighting for the entire file. - var SYNTAX_MAX_LINE_LENGTH = 500; + const SYNTAX_MAX_LINE_LENGTH = 500; - var TRAILING_WHITESPACE_PATTERN = /\s+$/; + const TRAILING_WHITESPACE_PATTERN = /\s+$/; Polymer({ is: 'gr-diff-builder', @@ -108,7 +108,7 @@ '_groupsChanged(_groups.splices)', ], - attached: function() { + attached() { // Setup annotation layers. this._layers = [ this._createTrailingWhitespaceLayer(), @@ -118,19 +118,18 @@ this.$.rangeLayer, ]; - this.async(function() { + this.async(() => { this._preRenderThread(); }); }, - render: function(comments, prefs) { + render(comments, prefs) { this.$.syntaxLayer.enabled = prefs.syntax_highlighting; this._showTabs = !!prefs.show_tabs; this._showTrailingWhitespace = !!prefs.show_whitespace_errors; - // Stop the processor (if it's running). - this.$.processor.cancel(); - this.$.syntaxLayer.cancel(); + // Stop the processor and syntax layer (if they're running). + this.cancel(); this._builder = this._getDiffBuilder(this.diff, comments, prefs); @@ -140,33 +139,35 @@ this._clearDiffContent(); this._builder.addColumns(this.diffElement, prefs.font_size); - var reporting = this.$.reporting; + const reporting = this.$.reporting; reporting.time(TimingLabel.TOTAL); reporting.time(TimingLabel.CONTENT); this.dispatchEvent(new CustomEvent('render-start', {bubbles: true})); - return this.$.processor.process(this.diff.content).then(function() { - if (this.isImageDiff) { - this._builder.renderDiffImages(); - } - this.dispatchEvent(new CustomEvent('render-content', - {bubbles: true})); + return this.$.processor.process(this.diff.content, this.isImageDiff) + .then(() => { + if (this.isImageDiff) { + this._builder.renderDiffImages(); + } + this.dispatchEvent(new CustomEvent('render-content', + {bubbles: true})); - if (this._anyLineTooLong()) { - this.$.syntaxLayer.enabled = false; - } + if (this._anyLineTooLong()) { + this.$.syntaxLayer.enabled = false; + } - reporting.timeEnd(TimingLabel.CONTENT); - reporting.time(TimingLabel.SYNTAX); - return this.$.syntaxLayer.process().then(function() { - reporting.timeEnd(TimingLabel.SYNTAX); - reporting.timeEnd(TimingLabel.TOTAL); - this.dispatchEvent(new CustomEvent('render', {bubbles: true})); - }.bind(this)); - }.bind(this)); + reporting.timeEnd(TimingLabel.CONTENT); + reporting.time(TimingLabel.SYNTAX); + return this.$.syntaxLayer.process().then(() => { + reporting.timeEnd(TimingLabel.SYNTAX); + reporting.timeEnd(TimingLabel.TOTAL); + this.dispatchEvent( + new CustomEvent('render', {bubbles: true})); + }); + }); }, - getLineElByChild: function(node) { + getLineElByChild(node) { while (node) { if (node instanceof Element) { if (node.classList.contains('lineNum')) { @@ -181,154 +182,159 @@ return null; }, - getLineNumberByChild: function(node) { - var lineEl = this.getLineElByChild(node); + getLineNumberByChild(node) { + const lineEl = this.getLineElByChild(node); return lineEl ? - parseInt(lineEl.getAttribute('data-value'), 10) : null; + parseInt(lineEl.getAttribute('data-value'), 10) : + null; }, - getContentByLine: function(lineNumber, opt_side, opt_root) { + getContentByLine(lineNumber, opt_side, opt_root) { return this._builder.getContentByLine(lineNumber, opt_side, opt_root); }, - getContentByLineEl: function(lineEl) { - var root = Polymer.dom(lineEl.parentElement); - var side = this.getSideByLineEl(lineEl); - var line = lineEl.getAttribute('data-value'); + getContentByLineEl(lineEl) { + const root = Polymer.dom(lineEl.parentElement); + const side = this.getSideByLineEl(lineEl); + const line = lineEl.getAttribute('data-value'); return this.getContentByLine(line, side, root); }, - getLineElByNumber: function(lineNumber, opt_side) { - var sideSelector = !!opt_side ? ('.' + opt_side) : ''; + getLineElByNumber(lineNumber, opt_side) { + const sideSelector = opt_side ? ('.' + opt_side) : ''; return this.diffElement.querySelector( '.lineNum[data-value="' + lineNumber + '"]' + sideSelector); }, - getContentsByLineRange: function(startLine, endLine, opt_side) { - var result = []; + getContentsByLineRange(startLine, endLine, opt_side) { + const result = []; this._builder.findLinesByRange(startLine, endLine, opt_side, null, result); return result; }, - getSideByLineEl: function(lineEl) { + getSideByLineEl(lineEl) { return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ? - GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT; + GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT; }, - createCommentThreadGroup: function(changeNum, patchNum, path, + createCommentThreadGroup(changeNum, patchNum, path, isOnParent, commentSide, projectConfig) { return this._builder.createCommentThreadGroup(changeNum, patchNum, path, isOnParent, commentSide, projectConfig); }, - emitGroup: function(group, sectionEl) { + emitGroup(group, sectionEl) { this._builder.emitGroup(group, sectionEl); }, - showContext: function(newGroups, sectionEl) { - var groups = this._builder.groups; - // TODO(viktard): Polyfill findIndex for IE10. - var contextIndex = groups.findIndex(function(group) { - return group.element == sectionEl; - }); - groups.splice.apply(groups, [contextIndex, 1].concat(newGroups)); + showContext(newGroups, sectionEl) { + const groups = this._builder.groups; - newGroups.forEach(function(newGroup) { + const contextIndex = groups.findIndex(group => + group.element === sectionEl + ); + groups.splice(...[contextIndex, 1].concat(newGroups)); + + for (const newGroup of newGroups) { this._builder.emitGroup(newGroup, sectionEl); - }, this); + } sectionEl.parentNode.removeChild(sectionEl); - this.async(function() { - this.fire('render'); - }, 1); + this.async(() => this.fire('render-content'), 1); }, - _getDiffBuilder: function(diff, comments, prefs) { + cancel() { + this.$.processor.cancel(); + this.$.syntaxLayer.cancel(); + }, + + _getDiffBuilder(diff, comments, prefs) { if (this.isImageDiff) { return new GrDiffBuilderImage(diff, comments, prefs, - this.diffElement, this.baseImage, this.revisionImage); + this.diffElement, this.baseImage, this.revisionImage); } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) { return new GrDiffBuilderSideBySide( - diff, comments, prefs, this.diffElement, this._layers); + diff, comments, prefs, this.diffElement, this._layers); } else if (this.viewMode === DiffViewMode.UNIFIED) { return new GrDiffBuilderUnified( - diff, comments, prefs, this.diffElement, this._layers); + diff, comments, prefs, this.diffElement, this._layers); } throw Error('Unsupported diff view mode: ' + this.viewMode); }, - _clearDiffContent: function() { + _clearDiffContent() { this.diffElement.innerHTML = null; }, - _getCommentLocations: function(comments) { - var result = { + _getCommentLocations(comments) { + const result = { left: {}, right: {}, }; - for (var side in comments) { + for (const side in comments) { if (side !== GrDiffBuilder.Side.LEFT && side !== GrDiffBuilder.Side.RIGHT) { continue; } - comments[side].forEach(function(c) { + for (const c of comments[side]) { result[side][c.line || GrDiffLine.FILE] = true; - }); + } } return result; }, - _groupsChanged: function(changeRecord) { + _groupsChanged(changeRecord) { if (!changeRecord) { return; } - changeRecord.indexSplices.forEach(function(splice) { - var group; - for (var i = 0; i < splice.addedCount; i++) { + for (const splice of changeRecord.indexSplices) { + let group; + for (let i = 0; i < splice.addedCount; i++) { group = splice.object[splice.index + i]; this._builder.groups.push(group); this._builder.emitGroup(group); } - }, this); + } }, - _createIntralineLayer: function() { + _createIntralineLayer() { return { // Take a DIV.contentText element and a line object with intraline // differences to highlight and apply them to the element as // annotations. - annotate: function(el, line) { - var HL_CLASS = 'style-scope gr-diff intraline'; - line.highlights.forEach(function(highlight) { + annotate(el, line) { + const HL_CLASS = 'style-scope gr-diff 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 // next one. Ignore it. - if (highlight.startIndex === highlight.endIndex) { return; } + if (highlight.startIndex === highlight.endIndex) { continue; } // If endIndex isn't present, continue to the end of the line. - var endIndex = highlight.endIndex === undefined ? - line.text.length : highlight.endIndex; + const endIndex = highlight.endIndex === undefined ? + line.text.length : + highlight.endIndex; GrAnnotation.annotateElement( el, highlight.startIndex, endIndex - highlight.startIndex, HL_CLASS); - }); + } }, }; }, - _createTabIndicatorLayer: function() { - var show = function() { return this._showTabs; }.bind(this); + _createTabIndicatorLayer() { + const show = () => this._showTabs; return { - annotate: function(el, line) { + annotate(el, line) { // If visible tabs are disabled, do nothing. if (!show()) { return; } // Find and annotate the locations of tabs. - var split = line.text.split('\t'); + const split = line.text.split('\t'); if (!split) { return; } - for (var i = 0, pos = 0; i < split.length - 1; i++) { + for (let i = 0, pos = 0; i < split.length - 1; i++) { // Skip forward by the length of the content pos += split[i].length; @@ -342,22 +348,22 @@ }; }, - _createTrailingWhitespaceLayer: function() { - var show = function() { + _createTrailingWhitespaceLayer() { + const show = function() { return this._showTrailingWhitespace; }.bind(this); return { - annotate: function(el, line) { + annotate(el, line) { if (!show()) { return; } - var match = line.text.match(TRAILING_WHITESPACE_PATTERN); + const match = line.text.match(TRAILING_WHITESPACE_PATTERN); if (match) { // Normalize string positions in case there is unicode before or // within the match. - var index = GrAnnotation.getStringLength( + const index = GrAnnotation.getStringLength( line.text.substr(0, match.index)); - var length = GrAnnotation.getStringLength(match[0]); + const length = GrAnnotation.getStringLength(match[0]); GrAnnotation.annotateElement(el, index, length, 'style-scope gr-diff trailing-whitespace'); } @@ -374,11 +380,11 @@ * already exist and the user's comment will be quick to load. * @see https://gerrit-review.googlesource.com/c/82213/ */ - _preRenderThread: function() { - var thread = document.createElement('gr-diff-comment-thread'); + _preRenderThread() { + const thread = document.createElement('gr-diff-comment-thread'); thread.setAttribute('hidden', true); thread.addDraft(); - var parent = Polymer.dom(this.root); + const parent = Polymer.dom(this.root); parent.appendChild(thread); Polymer.dom.flush(); parent.removeChild(thread); @@ -388,9 +394,9 @@ * @return {Boolean} whether any of the lines in _groups are longer * than SYNTAX_MAX_LINE_LENGTH. */ - _anyLineTooLong: function() { - return this._groups.reduce(function(acc, group) { - return acc || group.lines.reduce(function(acc, line) { + _anyLineTooLong() { + return this._groups.reduce((acc, group) => { + return acc || group.lines.reduce((acc, line) => { return acc || line.text.length >= SYNTAX_MAX_LINE_LENGTH; }, false); }, false);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js index 214454a..3cb1a39 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -14,8 +14,8 @@ (function(window, GrDiffGroup, GrDiffLine) { 'use strict'; - var HTML_ENTITY_PATTERN = /[&<>"'`\/]/g; - var HTML_ENTITY_MAP = { + const HTML_ENTITY_PATTERN = /[&<>"'`\/]/g; + const HTML_ENTITY_MAP = { '&': '&', '<': '<', '>': '>', @@ -28,7 +28,7 @@ // Prevent redefinition. if (window.GrDiffBuilder) { return; } - var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/; + const REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/; function GrDiffBuilder(diff, comments, prefs, outputEl, layers) { this._diff = diff; @@ -39,11 +39,11 @@ this.layers = layers || []; - this.layers.forEach(function(layer) { + for (const layer of this.layers) { if (layer.addListener) { layer.addListener(this._handleLayerUpdate.bind(this)); } - }.bind(this)); + } } GrDiffBuilder.LESS_THAN_CODE = '<'.charCodeAt(0); @@ -76,7 +76,7 @@ ALL: 'all', }; - var PARTIAL_CONTEXT_AMOUNT = 10; + const PARTIAL_CONTEXT_AMOUNT = 10; /** * Abstract method @@ -96,16 +96,16 @@ }; GrDiffBuilder.prototype.emitGroup = function(group, opt_beforeSection) { - var element = this.buildSectionElement(group); + const element = this.buildSectionElement(group); this._outputEl.insertBefore(element, opt_beforeSection); group.element = element; }; GrDiffBuilder.prototype.renderSection = function(element) { - for (var i = 0; i < this.groups.length; i++) { - var group = this.groups[i]; + for (let i = 0; i < this.groups.length; i++) { + const group = this.groups[i]; if (group.element === element) { - var newElement = this.buildSectionElement(group); + const newElement = this.buildSectionElement(group); group.element.parentElement.replaceChild(newElement, group.element); group.element = newElement; break; @@ -115,14 +115,14 @@ GrDiffBuilder.prototype.getGroupsByLineRange = function( startLine, endLine, opt_side) { - var groups = []; - for (var i = 0; i < this.groups.length; i++) { - var group = this.groups[i]; + const groups = []; + for (let i = 0; i < this.groups.length; i++) { + const group = this.groups[i]; if (group.lines.length === 0) { continue; } - var groupStartLine = 0; - var groupEndLine = 0; + let groupStartLine = 0; + let groupEndLine = 0; if (opt_side) { groupStartLine = group.lineRange[opt_side].start; groupEndLine = group.lineRange[opt_side].end; @@ -143,8 +143,8 @@ GrDiffBuilder.prototype.getContentByLine = function(lineNumber, opt_side, opt_root) { - var root = Polymer.dom(opt_root || this._outputEl); - var sideSelector = !!opt_side ? ('.' + opt_side) : ''; + const root = Polymer.dom(opt_root || this._outputEl); + const sideSelector = opt_side ? ('.' + opt_side) : ''; return root.querySelector('td.lineNum[data-value="' + lineNumber + '"]' + sideSelector + ' ~ td.content .contentText'); }; @@ -162,17 +162,17 @@ */ GrDiffBuilder.prototype.findLinesByRange = function(start, end, opt_side, out_lines, out_elements) { - var groups = this.getGroupsByLineRange(start, end, opt_side); - groups.forEach(function(group) { - var content = null; - group.lines.forEach(function(line) { + const groups = this.getGroupsByLineRange(start, end, opt_side); + for (const group of groups) { + let content = null; + for (const line of group.lines) { if ((opt_side === 'left' && line.type === GrDiffLine.Type.ADD) || (opt_side === 'right' && line.type === GrDiffLine.Type.REMOVE)) { - return; + continue; } - var lineNumber = opt_side === 'left' ? + const lineNumber = opt_side === 'left' ? line.beforeNumber : line.afterNumber; - if (lineNumber < start || lineNumber > end) { return; } + if (lineNumber < start || lineNumber > end) { continue; } if (out_lines) { out_lines.push(line); } if (out_elements) { @@ -184,8 +184,8 @@ } if (content) { out_elements.push(content); } } - }.bind(this)); - }.bind(this)); + } + } }; /** @@ -193,12 +193,12 @@ * diff content. */ GrDiffBuilder.prototype._renderContentByRange = function(start, end, side) { - var lines = []; - var elements = []; - var line; - var el; + const lines = []; + const elements = []; + let line; + let el; this.findLinesByRange(start, end, side, lines, elements); - for (var i = 0; i < lines.length; i++) { + for (let i = 0; i < lines.length; i++) { line = lines[i]; el = elements[i]; el.parentElement.replaceChild(this._createTextEl(line, side).firstChild, @@ -209,7 +209,7 @@ GrDiffBuilder.prototype.getSectionsByLineRange = function( startLine, endLine, opt_side) { return this.getGroupsByLineRange(startLine, endLine, opt_side).map( - function(group) { return group.element; }); + group => { return group.element; }); }; GrDiffBuilder.prototype._commentIsAtLineNum = function(side, lineNum) { @@ -219,15 +219,15 @@ // TODO(wyatta): Move this completely into the processor. GrDiffBuilder.prototype._insertContextGroups = function(groups, lines, hiddenRange) { - var linesBeforeCtx = lines.slice(0, hiddenRange[0]); - var hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]); - var linesAfterCtx = lines.slice(hiddenRange[1]); + const linesBeforeCtx = lines.slice(0, hiddenRange[0]); + const hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]); + const linesAfterCtx = lines.slice(hiddenRange[1]); if (linesBeforeCtx.length > 0) { groups.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx)); } - var ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL); + const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL); ctxLine.contextGroup = new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines); groups.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL, @@ -243,8 +243,8 @@ return null; } - var td = this._createElement('td'); - var showPartialLinks = + const td = this._createElement('td'); + const showPartialLinks = line.contextGroup.lines.length > PARTIAL_CONTEXT_AMOUNT; if (showPartialLinks) { @@ -266,14 +266,14 @@ }; GrDiffBuilder.prototype._createContextButton = function(type, section, line) { - var contextLines = line.contextGroup.lines; - var context = PARTIAL_CONTEXT_AMOUNT; + const contextLines = line.contextGroup.lines; + const context = PARTIAL_CONTEXT_AMOUNT; - var button = this._createElement('gr-button', 'showContext'); + const button = this._createElement('gr-button', 'showContext'); button.setAttribute('link', true); - var text; - var groups = []; // The groups that replace this one if tapped. + let text; + const groups = []; // The groups that replace this one if tapped. if (type === GrDiffBuilder.ContextButtonType.ALL) { text = 'Show ' + contextLines.length + ' common line'; @@ -291,10 +291,10 @@ button.textContent = text; - button.addEventListener('tap', function(e) { + button.addEventListener('tap', e => { e.detail = { - groups: groups, - section: section, + groups, + section, }; // Let it bubble up the DOM tree. }); @@ -310,15 +310,15 @@ (c.line === undefined && lineNum === GrDiffLine.FILE); }; } - var leftComments = + const leftComments = comments[GrDiffBuilder.Side.LEFT].filter(byLineNum(line.beforeNumber)); - var rightComments = + const rightComments = comments[GrDiffBuilder.Side.RIGHT].filter(byLineNum(line.afterNumber)); - leftComments.forEach(function(c) { c.__commentSide = 'left'; }); - rightComments.forEach(function(c) { c.__commentSide = 'right'; }); + leftComments.forEach(c => { c.__commentSide = 'left'; }); + rightComments.forEach(c => { c.__commentSide = 'right'; }); - var result; + let result; switch (opt_side) { case GrDiffBuilder.Side.LEFT: @@ -337,7 +337,7 @@ GrDiffBuilder.prototype.createCommentThreadGroup = function(changeNum, patchNum, path, isOnParent, projectConfig, range) { - var threadGroupEl = + const threadGroupEl = document.createElement('gr-diff-comment-thread-group'); threadGroupEl.changeNum = changeNum; threadGroupEl.patchForNewThreads = patchNum; @@ -348,24 +348,25 @@ return threadGroupEl; }; - GrDiffBuilder.prototype._commentThreadGroupForLine = - function(line, opt_side) { - var comments = this._getCommentsForLine(this._comments, line, opt_side); + GrDiffBuilder.prototype._commentThreadGroupForLine = function(line, + opt_side) { + const comments = + this._getCommentsForLine(this._comments, line, opt_side); if (!comments || comments.length === 0) { return null; } - var patchNum = this._comments.meta.patchRange.patchNum; - var isOnParent = comments[0].side === 'PARENT' || false; + let patchNum = this._comments.meta.patchRange.patchNum; + let isOnParent = comments[0].side === 'PARENT' || false; if (line.type === GrDiffLine.Type.REMOVE || - opt_side === GrDiffBuilder.Side.LEFT) { + opt_side === GrDiffBuilder.Side.LEFT) { if (this._comments.meta.patchRange.basePatchNum === 'PARENT') { isOnParent = true; } else { patchNum = this._comments.meta.patchRange.basePatchNum; } } - var threadGroupEl = this.createCommentThreadGroup( + const threadGroupEl = this.createCommentThreadGroup( this._comments.meta.changeNum, patchNum, this._comments.meta.path, @@ -380,10 +381,17 @@ GrDiffBuilder.prototype._createLineEl = function(line, number, type, opt_class) { - var td = this._createElement('td'); + const td = this._createElement('td'); if (opt_class) { td.classList.add(opt_class); } + + if (line.type === GrDiffLine.Type.REMOVE) { + td.setAttribute('aria-label', `${number} removed`); + } else if (line.type === GrDiffLine.Type.ADD) { + td.setAttribute('aria-label', `${number} added`); + } + if (line.type === GrDiffLine.Type.BLANK) { return td; } else if (line.type === GrDiffLine.Type.CONTEXT_CONTROL) { @@ -397,22 +405,21 @@ }; GrDiffBuilder.prototype._createTextEl = function(line, opt_side) { - var td = this._createElement('td'); - var text = line.text; + const td = this._createElement('td'); + const text = line.text; if (line.type !== GrDiffLine.Type.BLANK) { td.classList.add('content'); } td.classList.add(line.type); - var html = this._escapeHTML(text); + let html = this._escapeHTML(text); html = this._addTabWrappers(html, this._prefs.tab_size); - if (!this._prefs.line_wrapping && this._textLength(text, this._prefs.tab_size) > this._prefs.line_length) { html = this._addNewlines(text, html); } - var contentText = this._createElement('div', 'contentText'); + const contentText = this._createElement('div', 'contentText'); if (opt_side) { contentText.setAttribute('data-side', opt_side); } @@ -425,9 +432,9 @@ contentText.innerHTML = html; } - this.layers.forEach(function(layer) { + for (const layer of this.layers) { layer.annotate(contentText, line); - }); + } td.appendChild(contentText); @@ -440,8 +447,8 @@ */ GrDiffBuilder.prototype._textLength = function(text, tabSize) { text = text.replace(REGEX_ASTRAL_SYMBOL, '_'); - var numChars = 0; - for (var i = 0; i < text.length; i++) { + let numChars = 0; + for (let i = 0; i < text.length; i++) { if (text[i] === '\t') { numChars += tabSize - (numChars % tabSize); } else { @@ -489,11 +496,11 @@ }; GrDiffBuilder.prototype._addNewlines = function(text, html) { - var htmlIndex = 0; - var indices = []; - var numChars = 0; - var prevHtmlIndex = 0; - for (var i = 0; i < text.length; i++) { + let htmlIndex = 0; + const indices = []; + let numChars = 0; + let prevHtmlIndex = 0; + for (let i = 0; i < text.length; i++) { if (numChars > 0 && numChars % this._prefs.line_length === 0) { indices.push(htmlIndex); } @@ -513,11 +520,11 @@ } prevHtmlIndex = htmlIndex; } - var result = html; + let result = html; // Since the result string is being altered in place, start from the end // of the string so that the insertion indices are not affected as the // result string changes. - for (var i = indices.length - 1; i >= 0; i--) { + for (let i = indices.length - 1; i >= 0; i--) { result = result.slice(0, indices[i]) + GrDiffBuilder.LINE_FEED_HTML + result.slice(indices[i]); } @@ -536,12 +543,12 @@ GrDiffBuilder.prototype._addTabWrappers = function(line, tabSize) { if (!line.length) { return ''; } - var result = ''; - var offset = 0; - var split = line.split('\t'); - var width; + let result = ''; + let offset = 0; + const split = line.split('\t'); + let width; - for (var i = 0; i < split.length - 1; i++) { + for (let i = 0; i < split.length - 1; i++) { offset += split[i].length; width = tabSize - (offset % tabSize); result += split[i] + this._getTabWrapper(width); @@ -561,7 +568,7 @@ throw Error('Invalid tab size from preferences.'); } - var str = '<span class="style-scope gr-diff tab '; + let str = '<span class="style-scope gr-diff tab '; str += '" style="'; // TODO(andybons): CSS tab-size is not supported in IE. str += 'tab-size:' + tabSize + ';'; @@ -571,7 +578,7 @@ }; GrDiffBuilder.prototype._createElement = function(tagName, className) { - var el = document.createElement(tagName); + const el = document.createElement(tagName); // When Shady DOM is being used, these classes are added to account for // Polymer's polyfill behavior. In order to guarantee sufficient // specificity within the CSS rules, these are added to every element. @@ -579,7 +586,7 @@ // automatically) are not being used for performance reasons, this is // done manually. el.classList.add('style-scope', 'gr-diff'); - if (!!className) { + if (className) { el.classList.add(className); } return el; @@ -613,7 +620,7 @@ }; GrDiffBuilder.prototype._escapeHTML = function(str) { - return str.replace(HTML_ENTITY_PATTERN, function(s) { + return str.replace(HTML_ENTITY_PATTERN, s => { return HTML_ENTITY_MAP[s]; }); };
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html index 591fa9c..ee66b42 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder_test.html
@@ -20,6 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> <script src="../gr-diff/gr-diff-line.js"></script> <script src="../gr-diff/gr-diff-group.js"></script> @@ -54,15 +55,15 @@ </test-fixture> <script> - suite('gr-diff-builder tests', function() { - var element; - var builder; + suite('gr-diff-builder tests', () => { + let element; + let builder; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(false); }, + getLoggedIn() { return Promise.resolve(false); }, }); - var prefs = { + const prefs = { line_length: 10, show_tabs: true, tab_size: 4, @@ -70,18 +71,18 @@ builder = new GrDiffBuilder({content: []}, {left: [], right: []}, prefs); }); - test('context control buttons', function() { - var section = {}; - var line = {contextGroup: {lines: []}}; + test('context control buttons', () => { + const section = {}; + const line = {contextGroup: {lines: []}}; // Create 10 lines. - for (var i = 0; i < 10; i++) { + for (let i = 0; i < 10; i++) { line.contextGroup.lines.push('lorem upsum'); } // Does not include +10 buttons when there are fewer than 11 lines. - var td = builder._createContextControl(section, line); - var buttons = td.querySelectorAll('gr-button.showContext'); + let td = builder._createContextControl(section, line); + let buttons = td.querySelectorAll('gr-button.showContext'); assert.equal(buttons.length, 1); assert.equal(buttons[0].textContent, 'Show 10 common lines'); @@ -99,8 +100,8 @@ assert.equal(buttons[2].textContent, '+10↓'); }); - test('newlines 1', function() { - var text = 'abcdef'; + test('newlines 1', () => { + let text = 'abcdef'; assert.equal(builder._addNewlines(text, text), text); text = 'a'.repeat(20); assert.equal(builder._addNewlines(text, text), @@ -109,9 +110,10 @@ 'a'.repeat(10)); }); - test('newlines 2', function() { - var text = '<span class="thumbsup">👍</span>'; - var html = '<span class="thumbsup">👍</span>'; + test('newlines 2', () => { + const text = '<span class="thumbsup">👍</span>'; + const html = + '<span class="thumbsup">👍</span>'; assert.equal(builder._addNewlines(text, html), '<span clas' + GrDiffBuilder.LINE_FEED_HTML + @@ -122,23 +124,23 @@ 'n>'); }); - test('newlines 3', function() { - var text = '01234\t56789'; - var html = '01234<span>\t</span>56789'; + test('newlines 3', () => { + const text = '01234\t56789'; + const html = '01234<span>\t</span>56789'; assert.equal(builder._addNewlines(text, html), '01234<span>\t</span>5' + GrDiffBuilder.LINE_FEED_HTML + '6789'); }); - test('_addNewlines not called if line_wrapping is true', function(done) { + test('_addNewlines not called if line_wrapping is true', done => { builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50}; - var text = (new Array(52)).join('a'); + const text = (new Array(52)).join('a'); - var line = {text: text, highlights: []}; - var newLineStub = sinon.stub(builder, '_addNewlines'); + const line = {text, highlights: []}; + const newLineStub = sinon.stub(builder, '_addNewlines'); builder._createTextEl(line); - flush(function() { + flush(() => { assert.isFalse(newLineStub.called); newLineStub.restore(); done(); @@ -146,33 +148,33 @@ }); test('_addNewlines called if line_wrapping is true and meets other ' + - 'conditions', function(done) { + 'conditions', done => { builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50}; - var text = (new Array(52)).join('a'); + const text = (new Array(52)).join('a'); - var line = {text: text, highlights: []}; - var newLineStub = sinon.stub(builder, '_addNewlines'); + const line = {text, highlights: []}; + const newLineStub = sinon.stub(builder, '_addNewlines'); builder._createTextEl(line); - flush(function() { + flush(() => { assert.isTrue(newLineStub.called); newLineStub.restore(); done(); }); }); - test('_createTextEl linewrap with tabs', function() { - var text = _.times(7, _.constant('\t')).join('') + '!'; - var line = {text: text, highlights: []}; - var el = builder._createTextEl(line); - var tabEl = el.querySelector('.contentText > .br'); + test('_createTextEl linewrap with tabs', () => { + const text = _.times(7, _.constant('\t')).join('') + '!'; + const line = {text, highlights: []}; + const el = builder._createTextEl(line); + const tabEl = el.querySelector('.contentText > .br'); assert.isOk(tabEl); assert.equal( el.querySelector('.contentText .tab:nth-child(2)').nextSibling, tabEl); }); - test('text length with tabs and unicode', function() { + test('text length with tabs and unicode', () => { assert.equal(builder._textLength('12345', 4), 5); assert.equal(builder._textLength('\t\t12', 4), 10); assert.equal(builder._textLength('abc💢123', 4), 7); @@ -186,9 +188,9 @@ assert.equal(builder._textLength('\t\t\t\t\t', 20), 100); }); - test('tab wrapper insertion', function() { - var html = 'abc\tdef'; - var wrapper = builder._getTabWrapper( + test('tab wrapper insertion', () => { + const html = 'abc\tdef'; + const wrapper = builder._getTabWrapper( builder._prefs.tab_size - 3, builder._prefs.show_tabs); assert.ok(wrapper); @@ -202,12 +204,12 @@ true)); }); - test('comments', function() { - var line = new GrDiffLine(GrDiffLine.Type.BOTH); + test('comments', () => { + const line = new GrDiffLine(GrDiffLine.Type.BOTH); line.beforeNumber = 3; line.afterNumber = 5; - var comments = {left: [], right: []}; + let comments = {left: [], right: []}; assert.deepEqual(builder._getCommentsForLine(comments, line), []); assert.deepEqual(builder._getCommentsForLine(comments, line, GrDiffBuilder.Side.LEFT), []); @@ -229,19 +231,19 @@ {id: 'r5', line: 5, __commentSide: 'right'}]); assert.deepEqual(builder._getCommentsForLine(comments, line, GrDiffBuilder.Side.LEFT), [{id: 'l3', line: 3, - __commentSide: 'left'}]); + __commentSide: 'left'}]); assert.deepEqual(builder._getCommentsForLine(comments, line, GrDiffBuilder.Side.RIGHT), [{id: 'r5', line: 5, - __commentSide: 'right'}]); + __commentSide: 'right'}]); }); - test('comment thread group creation', function() { - var l3 = {id: 'l3', line: 3, updated: '2016-08-09 00:42:32.000000000', - __commentSide: 'left'}; - var l5 = {id: 'l5', line: 5, updated: '2016-08-09 00:42:32.000000000', - __commentSide: 'left'}; - var r5 = {id: 'r5', line: 5, updated: '2016-08-09 00:42:32.000000000', - __commentSide: 'right'}; + test('comment thread group creation', () => { + const l3 = {id: 'l3', line: 3, updated: '2016-08-09 00:42:32.000000000', + __commentSide: 'left'}; + const l5 = {id: 'l5', line: 5, updated: '2016-08-09 00:42:32.000000000', + __commentSide: 'left'}; + const r5 = {id: 'r5', line: 5, updated: '2016-08-09 00:42:32.000000000', + __commentSide: 'right'}; builder._comments = { meta: { @@ -267,10 +269,10 @@ assert.deepEqual(threadGroupEl.comments, comments); } - var line = new GrDiffLine(GrDiffLine.Type.BOTH); + let line = new GrDiffLine(GrDiffLine.Type.BOTH); line.beforeNumber = 5; line.afterNumber = 5; - var threadGroupEl = builder._commentThreadGroupForLine(line); + let threadGroupEl = builder._commentThreadGroupForLine(line); checkThreadGroupProps(threadGroupEl, '3', false, [l5, r5]); threadGroupEl = @@ -306,51 +308,51 @@ line.beforeNumber = 3; line.afterNumber = 5; threadGroupEl = builder._commentThreadGroupForLine(line); - checkThreadGroupProps(threadGroupEl, '3', false, [l3, r5]); + checkThreadGroupProps(threadGroupEl, '3', false, [l3, r5]); }); - suite('_isTotal', function() { - test('is total for add', function() { - var group = new GrDiffGroup(GrDiffGroup.Type.DELTA); - for (var idx = 0; idx < 10; idx++) { + suite('_isTotal', () => { + test('is total for add', () => { + const group = new GrDiffGroup(GrDiffGroup.Type.DELTA); + for (let idx = 0; idx < 10; idx++) { group.addLine(new GrDiffLine(GrDiffLine.Type.ADD)); } assert.isTrue(GrDiffBuilder.prototype._isTotal(group)); }); - test('is total for remove', function() { - var group = new GrDiffGroup(GrDiffGroup.Type.DELTA); - for (var idx = 0; idx < 10; idx++) { + test('is total for remove', () => { + const group = new GrDiffGroup(GrDiffGroup.Type.DELTA); + for (let idx = 0; idx < 10; idx++) { group.addLine(new GrDiffLine(GrDiffLine.Type.REMOVE)); } assert.isTrue(GrDiffBuilder.prototype._isTotal(group)); }); - test('not total for empty', function() { - var group = new GrDiffGroup(GrDiffGroup.Type.BOTH); + test('not total for empty', () => { + const group = new GrDiffGroup(GrDiffGroup.Type.BOTH); assert.isFalse(GrDiffBuilder.prototype._isTotal(group)); }); - test('not total for non-delta', function() { - var group = new GrDiffGroup(GrDiffGroup.Type.DELTA); - for (var idx = 0; idx < 10; idx++) { + test('not total for non-delta', () => { + const group = new GrDiffGroup(GrDiffGroup.Type.DELTA); + for (let idx = 0; idx < 10; idx++) { group.addLine(new GrDiffLine(GrDiffLine.Type.BOTH)); } assert.isFalse(GrDiffBuilder.prototype._isTotal(group)); }); }); - suite('intraline differences', function() { - var el; - var str; - var annotateElementSpy; - var layer; + suite('intraline differences', () => { + let el; + let str; + let annotateElementSpy; + let layer; function slice(str, start, end) { return Array.from(str).slice(start, end).join(''); } - setup(function() { + setup(() => { el = fixture('div-with-text'); str = el.textContent; annotateElementSpy = sinon.spy(GrAnnotation, 'annotateElement'); @@ -358,12 +360,12 @@ ._createIntralineLayer(); }); - teardown(function() { + teardown(() => { annotateElementSpy.restore(); }); - test('annotate no highlights', function() { - var line = { + test('annotate no highlights', () => { + const line = { text: str, highlights: [], }; @@ -377,19 +379,19 @@ assert.equal(str, el.childNodes[0].textContent); }); - test('annotate with highlights', function() { - var line = { + test('annotate with highlights', () => { + const line = { text: str, highlights: [ {startIndex: 6, endIndex: 12}, {startIndex: 18, endIndex: 22}, ], }; - var str0 = slice(str, 0, 6); - var str1 = slice(str, 6, 12); - var str2 = slice(str, 12, 18); - var str3 = slice(str, 18, 22); - var str4 = slice(str, 22); + const str0 = slice(str, 0, 6); + const str1 = slice(str, 6, 12); + const str2 = slice(str, 12, 18); + const str3 = slice(str, 18, 22); + const str4 = slice(str, 22); layer.annotate(el, line); @@ -412,16 +414,16 @@ assert.equal(el.childNodes[4].textContent, str4); }); - test('annotate without endIndex', function() { - var line = { + test('annotate without endIndex', () => { + const line = { text: str, highlights: [ {startIndex: 28}, ], }; - var str0 = slice(str, 0, 28); - var str1 = slice(str, 28); + const str0 = slice(str, 0, 28); + const str1 = slice(str, 28); layer.annotate(el, line); @@ -435,8 +437,8 @@ assert.equal(el.childNodes[1].textContent, str1); }); - test('annotate ignores empty highlights', function() { - var line = { + test('annotate ignores empty highlights', () => { + const line = { text: str, highlights: [ {startIndex: 28, endIndex: 28}, @@ -449,20 +451,20 @@ assert.equal(el.childNodes.length, 1); }); - test('annotate handles unicode', function() { + test('annotate handles unicode', () => { // Put some unicode into the string: str = str.replace(/\s/g, '💢'); el.textContent = str; - var line = { + const line = { text: str, highlights: [ {startIndex: 6, endIndex: 12}, ], }; - var str0 = slice(str, 0, 6); - var str1 = slice(str, 6, 12); - var str2 = slice(str, 12); + const str0 = slice(str, 0, 6); + const str1 = slice(str, 6, 12); + const str2 = slice(str, 12); layer.annotate(el, line); @@ -479,20 +481,20 @@ assert.equal(el.childNodes[2].textContent, str2); }); - test('annotate handles unicode w/o endIndex', function() { + test('annotate handles unicode w/o endIndex', () => { // Put some unicode into the string: str = str.replace(/\s/g, '💢'); el.textContent = str; - var line = { + const line = { text: str, highlights: [ {startIndex: 6}, ], }; - var str0 = slice(str, 0, 6); - var str1 = slice(str, 6); + const str0 = slice(str, 0, 6); + const str1 = slice(str, 6); layer.annotate(el, line); @@ -507,87 +509,92 @@ }); }); - suite('tab indicators', function() { - var sandbox; - var element; - var layer; + suite('tab indicators', () => { + let sandbox; + let element; + let layer; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); element = fixture('basic'); element._showTabs = true; layer = element._createTabIndicatorLayer(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('does nothing with empty line', function() { - var line = {text: ''}; - var el = document.createElement('div'); - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + test('does nothing with empty line', () => { + const line = {text: ''}; + const el = document.createElement('div'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.isFalse(annotateElementStub.called); }); - test('does nothing with no tabs', function() { - var str = 'lorem ipsum no tabs'; - var line = {text: str}; - var el = document.createElement('div'); + test('does nothing with no tabs', () => { + const str = 'lorem ipsum no tabs'; + const line = {text: str}; + const el = document.createElement('div'); el.textContent = str; - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.isFalse(annotateElementStub.called); }); - test('annotates tab at beginning', function() { - var str = '\tlorem upsum'; - var line = {text: str}; - var el = document.createElement('div'); + test('annotates tab at beginning', () => { + const str = '\tlorem upsum'; + const line = {text: str}; + const el = document.createElement('div'); el.textContent = str; - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.equal(annotateElementStub.callCount, 1); - var args = annotateElementStub.getCalls()[0].args; + const args = annotateElementStub.getCalls()[0].args; assert.equal(args[0], el); assert.equal(args[1], 0, 'offset of tab indicator'); assert.equal(args[2], 1, 'length of tab indicator'); assert.include(args[3], 'tab-indicator'); }); - test('does not annotate when disabled', function() { + test('does not annotate when disabled', () => { element._showTabs = false; - var str = '\tlorem upsum'; - var line = {text: str}; - var el = document.createElement('div'); + const str = '\tlorem upsum'; + const line = {text: str}; + const el = document.createElement('div'); el.textContent = str; - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.isFalse(annotateElementStub.called); }); - test('annotates multiple in beginning', function() { - var str = '\t\tlorem upsum'; - var line = {text: str}; - var el = document.createElement('div'); + test('annotates multiple in beginning', () => { + const str = '\t\tlorem upsum'; + const line = {text: str}; + const el = document.createElement('div'); el.textContent = str; - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.equal(annotateElementStub.callCount, 2); - var args = annotateElementStub.getCalls()[0].args; + let args = annotateElementStub.getCalls()[0].args; assert.equal(args[0], el); assert.equal(args[1], 0, 'offset of tab indicator'); assert.equal(args[2], 1, 'length of tab indicator'); @@ -600,17 +607,18 @@ assert.include(args[3], 'tab-indicator'); }); - test('annotates intermediate tabs', function() { - var str = 'lorem\tupsum'; - var line = {text: str}; - var el = document.createElement('div'); + test('annotates intermediate tabs', () => { + const str = 'lorem\tupsum'; + const line = {text: str}; + const el = document.createElement('div'); el.textContent = str; - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.equal(annotateElementStub.callCount, 1); - var args = annotateElementStub.getCalls()[0].args; + const args = annotateElementStub.getCalls()[0].args; assert.equal(args[0], el); assert.equal(args[1], 5, 'offset of tab indicator'); assert.equal(args[2], 1, 'length of tab indicator'); @@ -618,108 +626,115 @@ }); }); - suite('trailing whitespace', function() { - var sandbox; - var element; - var layer; + suite('trailing whitespace', () => { + let sandbox; + let element; + let layer; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); element = fixture('basic'); element._showTrailingWhitespace = true; layer = element._createTrailingWhitespaceLayer(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('does nothing with empty line', function() { - var line = {text: ''}; - var el = document.createElement('div'); - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + test('does nothing with empty line', () => { + const line = {text: ''}; + const el = document.createElement('div'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.isFalse(annotateElementStub.called); }); - test('does nothing with no trailing whitespace', function() { - var str = 'lorem ipsum blah blah'; - var line = {text: str}; - var el = document.createElement('div'); + test('does nothing with no trailing whitespace', () => { + const str = 'lorem ipsum blah blah'; + const line = {text: str}; + const el = document.createElement('div'); el.textContent = str; - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.isFalse(annotateElementStub.called); }); - test('annotates trailing spaces', function() { - var str = 'lorem ipsum '; - var line = {text: str}; - var el = document.createElement('div'); + test('annotates trailing spaces', () => { + const str = 'lorem ipsum '; + const line = {text: str}; + const el = document.createElement('div'); el.textContent = str; - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.isTrue(annotateElementStub.called); assert.equal(annotateElementStub.lastCall.args[1], 11); assert.equal(annotateElementStub.lastCall.args[2], 3); }); - test('annotates trailing tabs', function() { - var str = 'lorem ipsum\t\t\t'; - var line = {text: str}; - var el = document.createElement('div'); + test('annotates trailing tabs', () => { + const str = 'lorem ipsum\t\t\t'; + const line = {text: str}; + const el = document.createElement('div'); el.textContent = str; - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.isTrue(annotateElementStub.called); assert.equal(annotateElementStub.lastCall.args[1], 11); assert.equal(annotateElementStub.lastCall.args[2], 3); }); - test('annotates mixed trailing whitespace', function() { - var str = 'lorem ipsum\t \t'; - var line = {text: str}; - var el = document.createElement('div'); + test('annotates mixed trailing whitespace', () => { + const str = 'lorem ipsum\t \t'; + const line = {text: str}; + const el = document.createElement('div'); el.textContent = str; - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.isTrue(annotateElementStub.called); assert.equal(annotateElementStub.lastCall.args[1], 11); assert.equal(annotateElementStub.lastCall.args[2], 3); }); - test('unicode preceding trailing whitespace', function() { - var str = '💢\t'; - var line = {text: str}; - var el = document.createElement('div'); + test('unicode preceding trailing whitespace', () => { + const str = '💢\t'; + const line = {text: str}; + const el = document.createElement('div'); el.textContent = str; - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.isTrue(annotateElementStub.called); assert.equal(annotateElementStub.lastCall.args[1], 1); assert.equal(annotateElementStub.lastCall.args[2], 1); }); - test('does not annotate when disabled', function() { + test('does not annotate when disabled', () => { element._showTrailingWhitespace = false; - var str = 'lorem upsum\t \t '; - var line = {text: str}; - var el = document.createElement('div'); + const str = 'lorem upsum\t \t '; + const line = {text: str}; + const el = document.createElement('div'); el.textContent = str; - var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); layer.annotate(el, line); assert.isFalse(annotateElementStub.called); }); }); - suite('rendering', function() { - var content; - var outputEl; - var sandbox; + suite('rendering', () => { + let content; + let outputEl; + let sandbox; - setup(function(done) { + setup(done => { sandbox = sinon.sandbox.create(); - var prefs = { + const prefs = { line_length: 10, show_tabs: true, tab_size: 4, @@ -729,13 +744,13 @@ content = [ { a: ['all work and no play make andybons a dull boy'], - b: ['elgoog elgoog elgoog'] + b: ['elgoog elgoog elgoog'], }, { ab: [ 'Non eram nescius, Brute, cum, quae summis ingeniis ', 'exquisitaque doctrina philosophi Graeco sermone tractavissent', - ] + ], }, ]; stub('gr-reporting', { @@ -744,30 +759,30 @@ }); element = fixture('basic'); outputEl = element.queryEffectiveChildren('#diffTable'); - sandbox.stub(element, '_getDiffBuilder', function() { - var builder = new GrDiffBuilder( - {content: content}, {left: [], right: []}, prefs, outputEl); + sandbox.stub(element, '_getDiffBuilder', () => { + const builder = new GrDiffBuilder( + {content}, {left: [], right: []}, prefs, outputEl); sandbox.stub(builder, 'addColumns'); builder.buildSectionElement = function(group) { - var section = document.createElement('stub'); - section.textContent = group.lines.reduce(function(acc, line) { + const section = document.createElement('stub'); + section.textContent = group.lines.reduce((acc, line) => { return acc + line.text; }, ''); return section; }; return builder; }); - element.diff = {content: content}; + element.diff = {content}; element.render({left: [], right: []}, prefs).then(done); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('reporting', function(done) { - var timeStub = element.$.reporting.time; - var timeEndStub = element.$.reporting.timeEnd; + test('reporting', done => { + const timeStub = element.$.reporting.time; + const timeEndStub = element.$.reporting.timeEnd; assert.isTrue(timeStub.calledWithExactly('Diff Total Render')); assert.isTrue(timeStub.calledWithExactly('Diff Content Render')); assert.isTrue(timeStub.calledWithExactly('Diff Syntax Render')); @@ -777,43 +792,43 @@ done(); }); - test('renderSection', function() { - var section = outputEl.querySelector('stub:nth-of-type(2)'); - var prevInnerHTML = section.innerHTML; + test('renderSection', () => { + let section = outputEl.querySelector('stub:nth-of-type(2)'); + const prevInnerHTML = section.innerHTML; section.innerHTML = 'wiped'; element._builder.renderSection(section); section = outputEl.querySelector('stub:nth-of-type(2)'); assert.equal(section.innerHTML, prevInnerHTML); }); - test('addColumns is called', function(done) { + test('addColumns is called', done => { element.render({left: [], right: []}, {}).then(done); assert.isTrue(element._builder.addColumns.called); }); - test('getSectionsByLineRange one line', function() { - var section = outputEl.querySelector('stub:nth-of-type(2)'); - var sections = element._builder.getSectionsByLineRange(1, 1, 'left'); + test('getSectionsByLineRange one line', () => { + const section = outputEl.querySelector('stub:nth-of-type(2)'); + const sections = element._builder.getSectionsByLineRange(1, 1, 'left'); assert.equal(sections.length, 1); assert.strictEqual(sections[0], section); }); - test('getSectionsByLineRange over diff', function() { - var section = [ + test('getSectionsByLineRange over diff', () => { + const section = [ outputEl.querySelector('stub:nth-of-type(2)'), outputEl.querySelector('stub:nth-of-type(3)'), ]; - var sections = element._builder.getSectionsByLineRange(1, 2, 'left'); + const sections = element._builder.getSectionsByLineRange(1, 2, 'left'); assert.equal(sections.length, 2); assert.strictEqual(sections[0], section[0]); assert.strictEqual(sections[1], section[1]); }); - test('render-start and render are fired', function(done) { - var dispatchEventStub = sinon.stub(element, 'dispatchEvent'); - element.render({left: [], right: []}, {}).then(function() { - var firedEventTypes = dispatchEventStub.getCalls() - .map(function(c) { return c.args[0].type; }); + test('render-start and render are fired', done => { + const dispatchEventStub = sinon.stub(element, 'dispatchEvent'); + element.render({left: [], right: []}, {}).then(() => { + const firedEventTypes = dispatchEventStub.getCalls() + .map(c => { return c.args[0].type; }); assert.include(firedEventTypes, 'render-start'); assert.include(firedEventTypes, 'render-content'); assert.include(firedEventTypes, 'render'); @@ -821,18 +836,18 @@ }); }); - test('rendering normal-sized diff does not disable syntax', function() { + test('rendering normal-sized diff does not disable syntax', () => { assert.isTrue(element.$.syntaxLayer.enabled); }); - test('rendering large diff disables syntax', function(done) { + test('rendering large diff disables syntax', done => { // Before it renders, set the first diff line to 500 '*' characters. element.diff.content[0].a = [new Array(501).join('*')]; - element.addEventListener('render', function() { + element.addEventListener('render', () => { assert.isFalse(element.$.syntaxLayer.enabled); done(); }); - var prefs = { + const prefs = { line_length: 10, show_tabs: true, tab_size: 4, @@ -841,15 +856,23 @@ }; element.render({left: [], right: []}, prefs); }); + + test('cancel', () => { + const processorCancelStub = sandbox.stub(element.$.processor, 'cancel'); + const syntaxCancelStub = sandbox.stub(element.$.syntaxLayer, 'cancel'); + element.cancel(); + assert.isTrue(processorCancelStub.called); + assert.isTrue(syntaxCancelStub.called); + }); }); - suite('mock-diff', function() { - var element; - var builder; - var diff; - var prefs; + suite('mock-diff', () => { + let element; + let builder; + let diff; + let prefs; - setup(function(done) { + setup(done => { element = fixture('mock-diff'); diff = document.createElement('mock-diff-response').diffResponse; element.diff = diff; @@ -860,14 +883,14 @@ tab_size: 4, }; - element.render({left: [], right: []}, prefs).then(function() { + element.render({left: [], right: []}, prefs).then(() => { builder = element._builder; done(); }); }); - test('getContentByLine', function() { - var actual; + test('getContentByLine', () => { + let actual; actual = builder.getContentByLine(2, 'left'); assert.equal(actual.textContent, diff.content[0].ab[1]); @@ -882,19 +905,19 @@ assert.equal(actual.textContent, diff.content[1].b[0]); }); - test('findLinesByRange', function() { - var lines = []; - var elems = []; - var start = 6; - var end = 10; - var count = end - start + 1; + test('findLinesByRange', () => { + const lines = []; + const elems = []; + const start = 6; + const end = 10; + const count = end - start + 1; builder.findLinesByRange(start, end, 'right', lines, elems); assert.equal(lines.length, count); assert.equal(elems.length, count); - for (var i = 0; i < 5; i++) { + for (let i = 0; i < 5; i++) { assert.instanceOf(lines[i], GrDiffLine); assert.equal(lines[i].afterNumber, start + i); assert.instanceOf(elems[i], HTMLElement); @@ -902,59 +925,59 @@ } }); - test('_renderContentByRange', function() { - var spy = sinon.spy(builder, '_createTextEl'); - var start = 9; - var end = 14; - var count = end - start + 1; + test('_renderContentByRange', () => { + const spy = sinon.spy(builder, '_createTextEl'); + const start = 9; + const end = 14; + const count = end - start + 1; builder._renderContentByRange(start, end, 'left'); assert.equal(spy.callCount, count); - spy.getCalls().forEach(function(call, i) { + spy.getCalls().forEach((call, i) => { assert.equal(call.args[0].beforeNumber, start + i); }); spy.restore(); }); - test('_getNextContentOnSide side-by-side left', function() { - var startElem = builder.getContentByLine(5, 'left', + test('_getNextContentOnSide side-by-side left', () => { + const startElem = builder.getContentByLine(5, 'left', element.$.diffTable); - var expectedStartString = diff.content[2].ab[0]; - var expectedNextString = diff.content[2].ab[1]; + const expectedStartString = diff.content[2].ab[0]; + const expectedNextString = diff.content[2].ab[1]; assert.equal(startElem.textContent, expectedStartString); - var nextElem = builder._getNextContentOnSide(startElem, + const nextElem = builder._getNextContentOnSide(startElem, 'left'); assert.equal(nextElem.textContent, expectedNextString); }); - test('_getNextContentOnSide side-by-side right', function() { - var startElem = builder.getContentByLine(5, 'right', + test('_getNextContentOnSide side-by-side right', () => { + const startElem = builder.getContentByLine(5, 'right', element.$.diffTable); - var expectedStartString = diff.content[1].b[0]; - var expectedNextString = diff.content[1].b[1]; + const expectedStartString = diff.content[1].b[0]; + const expectedNextString = diff.content[1].b[1]; assert.equal(startElem.textContent, expectedStartString); - var nextElem = builder._getNextContentOnSide(startElem, + const nextElem = builder._getNextContentOnSide(startElem, 'right'); assert.equal(nextElem.textContent, expectedNextString); }); - test('_getNextContentOnSide unified left', function(done) { + test('_getNextContentOnSide unified left', done => { // Re-render as unified: element.viewMode = 'UNIFIED_DIFF'; - element.render({left: [], right: []}, prefs).then(function() { + element.render({left: [], right: []}, prefs).then(() => { builder = element._builder; - var startElem = builder.getContentByLine(5, 'left', + const startElem = builder.getContentByLine(5, 'left', element.$.diffTable); - var expectedStartString = diff.content[2].ab[0]; - var expectedNextString = diff.content[2].ab[1]; + const expectedStartString = diff.content[2].ab[0]; + const expectedNextString = diff.content[2].ab[1]; assert.equal(startElem.textContent, expectedStartString); - var nextElem = builder._getNextContentOnSide(startElem, + const nextElem = builder._getNextContentOnSide(startElem, 'left'); assert.equal(nextElem.textContent, expectedNextString); @@ -962,19 +985,19 @@ }); }); - test('_getNextContentOnSide unified right', function(done) { + test('_getNextContentOnSide unified right', done => { // Re-render as unified: element.viewMode = 'UNIFIED_DIFF'; - element.render({left: [], right: []}, prefs).then(function() { + element.render({left: [], right: []}, prefs).then(() => { builder = element._builder; - var startElem = builder.getContentByLine(5, 'right', + const startElem = builder.getContentByLine(5, 'right', element.$.diffTable); - var expectedStartString = diff.content[1].b[0]; - var expectedNextString = diff.content[1].b[1]; + const expectedStartString = diff.content[1].b[0]; + const expectedNextString = diff.content[1].b[1]; assert.equal(startElem.textContent, expectedStartString); - var nextElem = builder._getNextContentOnSide(startElem, + const nextElem = builder._getNextContentOnSide(startElem, 'right'); assert.equal(nextElem.textContent, expectedNextString); @@ -982,11 +1005,11 @@ }); }); - test('_escapeHTML', function() { - var input = '<script>alert("XSS");<' + '/script>'; - var expected = '<script>alert("XSS");' + + test('_escapeHTML', () => { + let input = '<script>alert("XSS");<' + '/script>'; + let expected = '<script>alert("XSS");' + '</script>'; - var result = GrDiffBuilder.prototype._escapeHTML(input); + let result = GrDiffBuilder.prototype._escapeHTML(input); assert.equal(result, expected); input = '& < > " \' / `';
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html index 30dfacf..66a0229 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.html
@@ -16,10 +16,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-diff-comment-thread-group"> <template> - <style> + <style include="shared-styles"> :host { display: block; white-space: normal;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js index df75d52..7899fc7 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group.js
@@ -21,7 +21,7 @@ changeNum: String, comments: { type: Array, - value: function() { return []; }, + value() { return []; }, }, patchForNewThreads: String, projectConfig: Object, @@ -32,7 +32,7 @@ }, _threads: { type: Array, - value: function() { return []; }, + value() { return []; }, }, }, @@ -40,16 +40,16 @@ '_commentsChanged(comments.*)', ], - addNewThread: function(locationRange) { + addNewThread(locationRange) { this.push('_threads', { comments: [], - locationRange: locationRange, + locationRange, patchNum: this.patchForNewThreads, }); }, - removeThread: function(locationRange) { - for (var i = 0; i < this._threads.length; i++) { + removeThread(locationRange) { + for (let i = 0; i < this._threads.length; i++) { if (this._threads[i].locationRange === locationRange) { this.splice('_threads', i, 1); return; @@ -57,10 +57,10 @@ } }, - getThreadForRange: function(rangeToCheck) { - var threads = [].filter.call( + getThreadForRange(rangeToCheck) { + const threads = [].filter.call( Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread'), - function(thread) { + thread => { return thread.locationRange === rangeToCheck; }); if (threads.length === 1) { @@ -68,13 +68,13 @@ } }, - _commentsChanged: function() { + _commentsChanged() { this._threads = this._getThreadGroups(this.comments); }, - _sortByDate: function(threadGroups) { + _sortByDate(threadGroups) { if (!threadGroups.length) { return; } - return threadGroups.sort(function(a, b) { + return threadGroups.sort((a, b) => { // If a comment is a draft, it doesn't have a start_datetime yet. // Assume it is newer than the comment it is being compared to. if (!a.start_datetime) { @@ -88,7 +88,7 @@ }); }, - _calculateLocationRange: function(range, comment) { + _calculateLocationRange(range, comment) { return 'range-' + range.start_line + '-' + range.start_character + '-' + range.end_line + '-' + @@ -102,15 +102,15 @@ * This is needed for switching between side-by-side and unified views when * there are unsaved drafts. */ - _getPatchNum: function(comment) { + _getPatchNum(comment) { return comment.patchNum || this.patchForNewThreads; }, - _getThreadGroups: function(comments) { - var threadGroups = {}; + _getThreadGroups(comments) { + const threadGroups = {}; - comments.forEach(function(comment) { - var locationRange; + for (const comment of comments) { + let locationRange; if (!comment.range) { locationRange = 'line-' + comment.__commentSide; } else { @@ -123,18 +123,18 @@ threadGroups[locationRange] = { start_datetime: comment.updated, comments: [comment], - locationRange: locationRange, + locationRange, commentSide: comment.__commentSide, patchNum: this._getPatchNum(comment), }; } - }.bind(this)); + } - var threadGroupArr = []; - var threadGroupKeys = Object.keys(threadGroups); - threadGroupKeys.forEach(function(threadGroupKey) { + const threadGroupArr = []; + const threadGroupKeys = Object.keys(threadGroups); + for (const threadGroupKey of threadGroupKeys) { threadGroupArr.push(threadGroups[threadGroupKey]); - }); + } return this._sortByDate(threadGroupArr); },
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html index 53a8e81..c2738460 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-diff-comment-thread-group.html"> <script>void(0);</script> @@ -34,25 +34,25 @@ </test-fixture> <script> - suite('gr-diff-comment-thread-group tests', function() { - var element; - var sandbox; + suite('gr-diff-comment-thread-group tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(false); }, + getLoggedIn() { return Promise.resolve(false); }, }); element = fixture('basic'); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('_getThreadGroups', function() { + test('_getThreadGroups', () => { element.patchForNewThreads = 3; - var comments = [ + const comments = [ { id: 'sallys_confession', message: 'i like you, jack', @@ -66,23 +66,23 @@ }, ]; - var expectedThreadGroups = [ + let expectedThreadGroups = [ { start_datetime: '2015-12-23 15:00:20.396000000', commentSide: 'left', comments: [{ - id: 'sallys_confession', - message: 'i like you, jack', - updated: '2015-12-23 15:00:20.396000000', - __commentSide: 'left', - }, { - id: 'jacks_reply', - message: 'i like you, too', - updated: '2015-12-24 15:00:20.396000000', - __commentSide: 'left', - }], + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-23 15:00:20.396000000', + __commentSide: 'left', + }, { + id: 'jacks_reply', + message: 'i like you, too', + updated: '2015-12-24 15:00:20.396000000', + __commentSide: 'left', + }], locationRange: 'line-left', - patchNum: 3 + patchNum: 3, }, ]; @@ -91,33 +91,33 @@ // Patch num should get inherited from comment rather comments.push({ - id: 'betsys_confession', - message: 'i like you, jack', - updated: '2015-12-24 15:00:10.396000000', - range: { - start_line: 1, - start_character: 1, - end_line: 1, - end_character: 2, - }, - __commentSide: 'left', - }); + id: 'betsys_confession', + message: 'i like you, jack', + updated: '2015-12-24 15:00:10.396000000', + range: { + start_line: 1, + start_character: 1, + end_line: 1, + end_character: 2, + }, + __commentSide: 'left', + }); expectedThreadGroups = [ { start_datetime: '2015-12-23 15:00:20.396000000', commentSide: 'left', comments: [{ - id: 'sallys_confession', - message: 'i like you, jack', - updated: '2015-12-23 15:00:20.396000000', - __commentSide: 'left', - }, { - id: 'jacks_reply', - message: 'i like you, too', - updated: '2015-12-24 15:00:20.396000000', - __commentSide: 'left', - }], + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-23 15:00:20.396000000', + __commentSide: 'left', + }, { + id: 'jacks_reply', + message: 'i like you, too', + updated: '2015-12-24 15:00:20.396000000', + __commentSide: 'left', + }], patchNum: 3, locationRange: 'line-left', }, @@ -145,8 +145,8 @@ expectedThreadGroups); }); - test('_sortByDate', function() { - var threadGroups = [ + test('_sortByDate', () => { + let threadGroups = [ { start_datetime: '2015-12-23 15:00:20.396000000', comments: [], @@ -159,12 +159,12 @@ }, ]; - var expectedResult = [ + let expectedResult = [ { start_datetime: '2015-12-22 15:00:10.396000000', comments: [], locationRange: 'range-1-1-1-2', - },{ + }, { start_datetime: '2015-12-23 15:00:20.396000000', comments: [], locationRange: 'line', @@ -175,7 +175,7 @@ // When a comment doesn't have a date, the one without the date should be // last. - var threadGroups = [ + threadGroups = [ { start_datetime: '2015-12-23 15:00:20.396000000', comments: [], @@ -187,7 +187,7 @@ }, ]; - var expectedResult = [ + expectedResult = [ { start_datetime: '2015-12-23 15:00:20.396000000', comments: [], @@ -198,22 +198,25 @@ locationRange: 'range-1-1-1-2', }, ]; + + assert.deepEqual(element._sortByDate(threadGroups), expectedResult); }); - test('_calculateLocationRange', function() { - var comment = {__commentSide: 'left'}; - var range = { + test('_calculateLocationRange', () => { + const comment = {__commentSide: 'left'}; + const range = { start_line: 1, start_character: 2, end_line: 3, end_character: 4, }; assert.equal( - element._calculateLocationRange(range, comment), 'range-1-2-3-4-left'); + element._calculateLocationRange(range, comment), + 'range-1-2-3-4-left'); }); - test('thread groups are updated when comments change', function() { - var commentsChangedStub = sandbox.stub(element, '_commentsChanged'); + test('thread groups are updated when comments change', () => { + const commentsChangedStub = sandbox.stub(element, '_commentsChanged'); element.comments = []; element.comments.push({ id: 'sallys_confession', @@ -223,16 +226,16 @@ assert(commentsChangedStub.called); }); - test('addNewThread', function() { - var locationRange = 'range-1-2-3-4'; + test('addNewThread', () => { + const locationRange = 'range-1-2-3-4'; element._threads = [{locationRange: 'line'}]; element.addNewThread(locationRange); assert(element._threads.length, 2); }); - test('_getPatchNum', function() { + test('_getPatchNum', () => { element.patchForNewThreads = 3; - var comment = { + const comment = { id: 'sallys_confession', message: 'i like you, jack', updated: '2015-12-23 15:00:20.396000000', @@ -242,11 +245,11 @@ assert.equal(element._getPatchNum(comment), 4); }); - test('removeThread', function() { - var locationRange = 'range-1-2-3-4'; + test('removeThread', () => { + const locationRange = 'range-1-2-3-4'; element._threads = [ {locationRange: 'range-1-2-3-4', comments: []}, - {locationRange: 'line', comments: []} + {locationRange: 'line', comments: []}, ]; flushAsynchronousOperations(); element.removeThread(locationRange);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html index c19b643..f5614a9 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.html
@@ -18,10 +18,11 @@ <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../../shared/gr-storage/gr-storage.html"> <link rel="import" href="../gr-diff-comment/gr-diff-comment.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-diff-comment-thread"> <template> - <style> + <style include="shared-styles"> :host { border: 1px solid #bbb; display: block; @@ -56,7 +57,7 @@ robot-button-disabled="[[_hideActions(_showActions, _lastComment)]]" change-num="[[changeNum]]" patch-num="[[patchNum]]" - draft="[[comment.__draft]]" + draft="[[_isDraft(comment)]]" show-actions="[[_showActions]]" comment-side="[[comment.__commentSide]]" side="[[comment.side]]"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js index be88e476..96f1bac 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -14,8 +14,8 @@ (function() { 'use strict'; - var UNRESOLVED_EXPAND_COUNT = 5; - var NEWLINE_PATTERN = /\n/g; + const UNRESOLVED_EXPAND_COUNT = 5; + const NEWLINE_PATTERN = /\n/g; Polymer({ is: 'gr-diff-comment-thread', @@ -30,12 +30,12 @@ changeNum: String, comments: { type: Array, - value: function() { return []; }, + value() { return []; }, }, locationRange: String, keyEventTarget: { type: Object, - value: function() { return document.body; }, + value() { return document.body; }, }, commentSide: String, patchNum: String, @@ -71,17 +71,17 @@ 'e shift+e': '_handleEKey', }, - attached: function() { - this._getLoggedIn().then(function(loggedIn) { + attached() { + this._getLoggedIn().then(loggedIn => { this._showActions = loggedIn; - }.bind(this)); + }); this._setInitialExpandedState(); }, - addOrEditDraft: function(opt_lineNum, opt_range) { - var lastComment = this.comments[this.comments.length - 1] || {}; + addOrEditDraft(opt_lineNum, opt_range) { + const lastComment = this.comments[this.comments.length - 1] || {}; if (lastComment.__draft) { - var commentEl = this._commentElWithDraftID( + const commentEl = this._commentElWithDraftID( lastComment.id || lastComment.__draftID); commentEl.editing = true; @@ -89,25 +89,25 @@ // actions are available. commentEl.collapsed = false; } else { - var range = opt_range ? opt_range : + const range = opt_range ? opt_range : lastComment ? lastComment.range : undefined; - var unresolved = lastComment ? lastComment.unresolved : undefined; + const unresolved = lastComment ? lastComment.unresolved : undefined; this.addDraft(opt_lineNum, range, unresolved); } }, - addDraft: function(opt_lineNum, opt_range, opt_unresolved) { - var draft = this._newDraft(opt_lineNum, opt_range); + addDraft(opt_lineNum, opt_range, opt_unresolved) { + const draft = this._newDraft(opt_lineNum, opt_range); draft.__editing = true; draft.unresolved = opt_unresolved === false ? opt_unresolved : true; this.push('comments', draft); }, - _getLoggedIn: function() { + _getLoggedIn() { return this.$.restAPI.getLoggedIn(); }, - _commentsChanged: function(changeRecord) { + _commentsChanged(changeRecord) { this._orderedComments = this._sortedComments(this.comments); if (this._orderedComments.length) { this._lastComment = this._getLastComment(); @@ -115,15 +115,15 @@ } }, - _hideActions: function(_showActions, _lastComment) { + _hideActions(_showActions, _lastComment) { return !_showActions || !_lastComment || !!_lastComment.__draft; }, - _getLastComment: function() { + _getLastComment() { return this._orderedComments[this._orderedComments.length - 1] || {}; }, - _handleEKey: function(e) { + _handleEKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } // Don’t preventDefault in this case because it will render the event @@ -136,12 +136,12 @@ } }, - _expandCollapseComments: function(actionIsCollapse) { - var comments = + _expandCollapseComments(actionIsCollapse) { + const comments = Polymer.dom(this.root).querySelectorAll('gr-diff-comment'); - comments.forEach(function(comment) { + for (const comment of comments) { comment.collapsed = actionIsCollapse; - }); + } }, /** @@ -149,33 +149,32 @@ * {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the * thread is unresolved. */ - _setInitialExpandedState: function() { - var comment; + _setInitialExpandedState() { + let comment; if (this._orderedComments) { - for (var i = 0; i < this._orderedComments.length; i++) { + for (let i = 0; i < this._orderedComments.length; i++) { comment = this._orderedComments[i]; comment.collapsed = this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT || !this._unresolved; } } - }, - _sortedComments: function(comments) { - return comments.slice().sort(function(c1, c2) { - var c1Date = c1.__date || util.parseDate(c1.updated); - var c2Date = c2.__date || util.parseDate(c2.updated); - var dateCompare = c1Date - c2Date; + _sortedComments(comments) { + return comments.slice().sort((c1, c2) => { + const c1Date = c1.__date || util.parseDate(c1.updated); + const c2Date = c2.__date || util.parseDate(c2.updated); + const dateCompare = c1Date - c2Date; if (!c1.id || !c1.id.localeCompare) { return 0; } // If same date, fall back to sorting by id. return dateCompare ? dateCompare : c1.id.localeCompare(c2.id); }); }, - _createReplyComment: function(parent, content, opt_isEditing, + _createReplyComment(parent, content, opt_isEditing, opt_unresolved) { - var reply = this._newReply( + const reply = this._newReply( this._orderedComments[this._orderedComments.length - 1].id, parent.line, content, @@ -184,7 +183,7 @@ // If there is currently a comment in an editing state, add an attribute // so that the gr-diff-comment knows not to populate the draft text. - for (var i = 0; i < this.comments.length; i++) { + for (let i = 0; i < this.comments.length; i++) { if (this.comments[i].__editing) { reply.__otherEditing = true; break; @@ -199,62 +198,66 @@ if (!opt_isEditing) { // Allow the reply to render in the dom-repeat. - this.async(function() { - var commentEl = this._commentElWithDraftID(reply.__draftID); + this.async(() => { + const commentEl = this._commentElWithDraftID(reply.__draftID); commentEl.save(); }, 1); } }, - _processCommentReply: function(opt_quote) { - var comment = this._lastComment; - var quoteStr; + _isDraft(comment) { + return !!comment.__draft; + }, + + _processCommentReply(opt_quote) { + const comment = this._lastComment; + let quoteStr; if (opt_quote) { - var msg = comment.message; + const msg = comment.message; quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n'; } this._createReplyComment(comment, quoteStr, true, comment.unresolved); }, - _handleCommentReply: function(e) { + _handleCommentReply(e) { this._processCommentReply(); }, - _handleCommentQuote: function(e) { + _handleCommentQuote(e) { this._processCommentReply(true); }, - _handleCommentAck: function(e) { - var comment = this._lastComment; + _handleCommentAck(e) { + const comment = this._lastComment; this._createReplyComment(comment, 'Ack', false, false); }, - _handleCommentDone: function(e) { - var comment = this._lastComment; + _handleCommentDone(e) { + const comment = this._lastComment; this._createReplyComment(comment, 'Done', false, false); }, - _handleCommentFix: function(e) { - var comment = e.detail.comment; - var msg = comment.message; - var quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n'; - var response = quoteStr + 'Please Fix'; + _handleCommentFix(e) { + const comment = e.detail.comment; + const msg = comment.message; + const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n'; + const response = quoteStr + 'Please Fix'; this._createReplyComment(comment, response, false, true); }, - _commentElWithDraftID: function(id) { - var els = Polymer.dom(this.root).querySelectorAll('gr-diff-comment'); - for (var i = 0; i < els.length; i++) { - if (els[i].comment.id === id || els[i].comment.__draftID === id) { - return els[i]; + _commentElWithDraftID(id) { + const els = Polymer.dom(this.root).querySelectorAll('gr-diff-comment'); + for (const el of els) { + if (el.comment.id === id || el.comment.__draftID === id) { + return el; } } return null; }, - _newReply: function(inReplyTo, opt_lineNum, opt_message, opt_unresolved, - opt_range) { - var d = this._newDraft(opt_lineNum); + _newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved, + opt_range) { + const d = this._newDraft(opt_lineNum); d.in_reply_to = inReplyTo; d.range = opt_range; if (opt_message != null) { @@ -266,8 +269,8 @@ return d; }, - _newDraft: function(opt_lineNum, opt_range) { - var d = { + _newDraft(opt_lineNum, opt_range) { + const d = { __draft: true, __draftID: Math.random().toString(36), __date: new Date(), @@ -290,15 +293,15 @@ return d; }, - _getSide: function(isOnParent) { + _getSide(isOnParent) { if (isOnParent) { return 'PARENT'; } return 'REVISION'; }, - _handleCommentDiscard: function(e) { - var diffCommentEl = Polymer.dom(e).rootTarget; - var comment = diffCommentEl.comment; - var idx = this._indexOf(comment, this.comments); + _handleCommentDiscard(e) { + const diffCommentEl = Polymer.dom(e).rootTarget; + const comment = diffCommentEl.comment; + const idx = this._indexOf(comment, this.comments); if (idx == -1) { throw Error('Cannot find comment ' + JSON.stringify(diffCommentEl.comment)); @@ -310,23 +313,23 @@ // Check to see if there are any other open comments getting edited and // set the local storage value to its message value. - for (var i = 0; i < this.comments.length; i++) { - if (this.comments[i].__editing) { - var commentLocation = { + for (const changeComment of this.comments) { + if (changeComment.__editing) { + const commentLocation = { changeNum: this.changeNum, patchNum: this.patchNum, - path: this.comments[i].path, - line: this.comments[i].line, + path: changeComment.path, + line: changeComment.line, }; return this.$.storage.setDraftComment(commentLocation, - this.comments[i].message); + changeComment.message); } } }, - _handleCommentUpdate: function(e) { - var comment = e.detail.comment; - var index = this._indexOf(comment, this.comments); + _handleCommentUpdate(e) { + const comment = e.detail.comment; + const index = this._indexOf(comment, this.comments); if (index === -1) { // This should never happen: comment belongs to another thread. console.warn('Comment update for another comment thread.'); @@ -335,9 +338,9 @@ this.set(['comments', index], comment); }, - _indexOf: function(comment, arr) { - for (var i = 0; i < arr.length; i++) { - var c = arr[i]; + _indexOf(comment, arr) { + for (let i = 0; i < arr.length; i++) { + const c = arr[i]; if ((c.__draftID != null && c.__draftID == comment.__draftID) || (c.id != null && c.id == comment.id)) { return i; @@ -346,7 +349,7 @@ return -1; }, - _computeHostClass: function(unresolved) { + _computeHostClass(unresolved) { return unresolved ? 'unresolved' : ''; }, });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html index 546308e..5a65f58 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-diff-comment-thread.html"> <script>void(0);</script> @@ -40,25 +40,25 @@ </test-fixture> <script> - suite('gr-diff-comment-thread tests', function() { - var element; - var sandbox; + suite('gr-diff-comment-thread tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(false); }, + getLoggedIn() { return Promise.resolve(false); }, }); sandbox = sinon.sandbox.create(); element = fixture('basic'); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('comments are sorted correctly', function() { - var comments = [ + test('comments are sorted correctly', () => { + const comments = [ { id: 'jacks_reply', message: 'i like you, too', @@ -86,9 +86,9 @@ id: 'sallys_mission', message: 'i have to find santa', updated: '2015-12-24 15:00:20.396000000', - } + }, ]; - var results = element._sortedComments(comments); + const results = element._sortedComments(comments); assert.deepEqual(results, [ { id: 'sally_to_dr_finklestein', @@ -117,11 +117,11 @@ message: 'i like you, too', in_reply_to: 'sallys_confession', updated: '2015-12-25 15:00:20.396000000', - } + }, ]); }); - test('addOrEditDraft w/ edit draft', function() { + test('addOrEditDraft w/ edit draft', () => { element.comments = [{ id: 'jacks_reply', message: 'i like you, too', @@ -129,9 +129,9 @@ updated: '2015-12-25 15:00:20.396000000', __draft: true, }]; - var commentElStub = sandbox.stub(element, '_commentElWithDraftID', - function() { return {}; }); - var addDraftStub = sandbox.stub(element, 'addDraft'); + const commentElStub = sandbox.stub(element, '_commentElWithDraftID', + () => { return {}; }); + const addDraftStub = sandbox.stub(element, 'addDraft'); element.addOrEditDraft(123); @@ -139,11 +139,11 @@ assert.isFalse(addDraftStub.called); }); - test('addOrEditDraft w/o edit draft', function() { + test('addOrEditDraft w/o edit draft', () => { element.comments = []; - var commentElStub = sandbox.stub(element, '_commentElWithDraftID', - function() { return {}; }); - var addDraftStub = sandbox.stub(element, 'addDraft'); + const commentElStub = sandbox.stub(element, '_commentElWithDraftID', + () => { return {}; }); + const addDraftStub = sandbox.stub(element, 'addDraft'); element.addOrEditDraft(123); @@ -151,40 +151,41 @@ assert.isTrue(addDraftStub.called); }); - test('_hideActions', function() { - var showActions = true; - var lastComment = {}; + test('_hideActions', () => { + let showActions = true; + const lastComment = {}; assert.equal(element._hideActions(showActions, lastComment), false); showActions = false; assert.equal(element._hideActions(showActions, lastComment), true); - var showActions = true; + showActions = true; lastComment.__draft = true; assert.equal(element._hideActions(showActions, lastComment), true); }); }); - suite('comment action tests', function() { - var element; + suite('comment action tests', () => { + let element; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(false); }, - saveDiffDraft: function() { + getLoggedIn() { return Promise.resolve(false); }, + saveDiffDraft() { return Promise.resolve({ ok: true, - text: function() { return Promise.resolve(')]}\'\n' + - JSON.stringify({ - id: '7afa4931_de3d65bd', - path: '/path/to/file.txt', - line: 5, - in_reply_to: 'baf0414d_60047215', - updated: '2015-12-21 02:01:10.850000000', - message: 'Done', - })); + text() { + return Promise.resolve(')]}\'\n' + + JSON.stringify({ + id: '7afa4931_de3d65bd', + path: '/path/to/file.txt', + line: 5, + in_reply_to: 'baf0414d_60047215', + updated: '2015-12-21 02:01:10.850000000', + message: 'Done', + })); }, }); }, - deleteDiffDraft: function() { return Promise.resolve({ok: true}); }, + deleteDiffDraft() { return Promise.resolve({ok: true}); }, }); element = fixture('withComment'); element.comments = [{ @@ -200,15 +201,15 @@ flushAsynchronousOperations(); }); - test('reply', function(done) { - var commentEl = element.$$('gr-diff-comment'); + test('reply', done => { + const commentEl = element.$$('gr-diff-comment'); assert.ok(commentEl); - var replyBtn = element.$.replyBtn; + const replyBtn = element.$.replyBtn; MockInteractions.tap(replyBtn); flushAsynchronousOperations(); - var drafts = element._orderedComments.filter(function(c) { + const drafts = element._orderedComments.filter(c => { return c.__draft == true; }); assert.equal(drafts.length, 1); @@ -217,16 +218,16 @@ done(); }); - test('quote reply', function(done) { - var commentEl = element.$$('gr-diff-comment'); + test('quote reply', done => { + const commentEl = element.$$('gr-diff-comment'); assert.ok(commentEl); - var quoteBtn = element.$.quoteBtn; + const quoteBtn = element.$.quoteBtn; MockInteractions.tap(quoteBtn); flushAsynchronousOperations(); - var drafts = element._orderedComments.filter(function(c) { - return c.__draft == true; + const drafts = element._orderedComments.filter(c => { + return c.__draft == true; }); assert.equal(drafts.length, 1); assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n'); @@ -234,7 +235,7 @@ done(); }); - test('quote reply multiline', function(done) { + test('quote reply multiline', done => { element.comments = [{ author: { name: 'Mr. Peanutbutter', @@ -247,14 +248,14 @@ }]; flushAsynchronousOperations(); - var commentEl = element.$$('gr-diff-comment'); + const commentEl = element.$$('gr-diff-comment'); assert.ok(commentEl); - var quoteBtn = element.$.quoteBtn; + const quoteBtn = element.$.quoteBtn; MockInteractions.tap(quoteBtn); flushAsynchronousOperations(); - var drafts = element._orderedComments.filter(function(c) { + const drafts = element._orderedComments.filter(c => { return c.__draft == true; }); assert.equal(drafts.length, 1); @@ -264,17 +265,17 @@ done(); }); - test('ack', function(done) { + test('ack', done => { element.changeNum = '42'; element.patchNum = '1'; - var commentEl = element.$$('gr-diff-comment'); + const commentEl = element.$$('gr-diff-comment'); assert.ok(commentEl); - var ackBtn = element.$.ackBtn; + const ackBtn = element.$.ackBtn; MockInteractions.tap(ackBtn); - flush(function() { - var drafts = element.comments.filter(function(c) { + flush(() => { + const drafts = element.comments.filter(c => { return c.__draft == true; }); assert.equal(drafts.length, 1); @@ -285,16 +286,16 @@ }); }); - test('done', function(done) { + test('done', done => { element.changeNum = '42'; element.patchNum = '1'; - var commentEl = element.$$('gr-diff-comment'); + const commentEl = element.$$('gr-diff-comment'); assert.ok(commentEl); - var doneBtn = element.$.doneBtn; + const doneBtn = element.$.doneBtn; MockInteractions.tap(doneBtn); - flush(function() { - var drafts = element.comments.filter(function(c) { + flush(() => { + const drafts = element.comments.filter(c => { return c.__draft == true; }); assert.equal(drafts.length, 1); @@ -305,13 +306,13 @@ }); }); - test('please fix', function(done) { + test('please fix', done => { element.changeNum = '42'; element.patchNum = '1'; - var commentEl = element.$$('gr-diff-comment'); + const commentEl = element.$$('gr-diff-comment'); assert.ok(commentEl); - commentEl.addEventListener('create-fix-comment', function() { - var drafts = element._orderedComments.filter(function(c) { + commentEl.addEventListener('create-fix-comment', () => { + const drafts = element._orderedComments.filter(c => { return c.__draft == true; }); assert.equal(drafts.length, 1); @@ -325,21 +326,21 @@ {bubbles: false}); }); - test('discard', function(done) { + test('discard', done => { element.changeNum = '42'; element.patchNum = '1'; element.push('comments', element._newReply( - element.comments[0].id, - element.comments[0].line, - element.comments[0].path, - 'it’s pronouced jiff, not giff')); + element.comments[0].id, + element.comments[0].line, + element.comments[0].path, + 'it’s pronouced jiff, not giff')); flushAsynchronousOperations(); - var draftEl = + const draftEl = Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1]; assert.ok(draftEl); - draftEl.addEventListener('comment-discard', function() { - var drafts = element.comments.filter(function(c) { + draftEl.addEventListener('comment-discard', () => { + const drafts = element.comments.filter(c => { return c.__draft == true; }); assert.equal(drafts.length, 0); @@ -348,9 +349,7 @@ draftEl.fire('comment-discard', null, {bubbles: false}); }); - test('first editing comment does not add __otherEditing attribute', - function() { - var commentEl = element.$$('gr-diff-comment'); + test('first editing comment does not add __otherEditing attribute', () => { element.comments = [{ author: { name: 'Mr. Peanutbutter', @@ -363,19 +362,19 @@ __draft: true, }]; - var replyBtn = element.$.replyBtn; + const replyBtn = element.$.replyBtn; MockInteractions.tap(replyBtn); flushAsynchronousOperations(); - var editing = element._orderedComments.filter(function(c) { + const editing = element._orderedComments.filter(c => { return c.__editing == true; }); assert.equal(editing.length, 1); assert.equal(!!editing[0].__otherEditing, false); }); - test('When not editing other comments, local storage not set after discard', - function(done) { + test('When not editing other comments, local storage not set' + + ' after discard', done => { element.changeNum = '42'; element.patchNum = '1'; element.comments = [{ @@ -413,13 +412,13 @@ updated: '2015-12-08 19:48:33.843000000', __draft: true, }]; - var storageStub = sinon.stub(element.$.storage, 'setDraftComment'); + const storageStub = sinon.stub(element.$.storage, 'setDraftComment'); flushAsynchronousOperations(); - var draftEl = - Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1]; + const draftEl = + Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1]; assert.ok(draftEl); - draftEl.addEventListener('comment-discard', function() { + draftEl.addEventListener('comment-discard', () => { assert.isFalse(storageStub.called); storageStub.restore(); done(); @@ -427,9 +426,9 @@ draftEl.fire('comment-discard', null, {bubbles: false}); }); - test('comment-update', function() { - var commentEl = element.$$('gr-diff-comment'); - var updatedComment = { + test('comment-update', () => { + const commentEl = element.$$('gr-diff-comment'); + const updatedComment = { id: element.comments[0].id, foo: 'bar', }; @@ -437,88 +436,81 @@ assert.strictEqual(element.comments[0], updatedComment); }); - suite('jack and sally comment data test consolidation', function() { - var getComments = function() { - return Polymer.dom(element.root).querySelectorAll('gr-diff-comment'); - }; - - setup(function() { + suite('jack and sally comment data test consolidation', () => { + setup(() => { element.comments = [ - { - id: 'jacks_reply', - message: 'i like you, too', - in_reply_to: 'sallys_confession', - updated: '2015-12-25 15:00:20.396000000', - unresolved: false, - }, { - id: 'sallys_confession', - in_reply_to: 'nonexistent_comment', - message: 'i like you, jack', - updated: '2015-12-24 15:00:20.396000000', - }, { - id: 'sally_to_dr_finklestein', - in_reply_to: 'nonexistent_comment', - message: 'i’m running away', - updated: '2015-10-31 09:00:20.396000000', - }, { - id: 'sallys_defiance', - message: 'i will poison you so i can get away', - updated: '2015-10-31 15:00:20.396000000', - }]; + { + id: 'jacks_reply', + message: 'i like you, too', + in_reply_to: 'sallys_confession', + updated: '2015-12-25 15:00:20.396000000', + unresolved: false, + }, { + id: 'sallys_confession', + in_reply_to: 'nonexistent_comment', + message: 'i like you, jack', + updated: '2015-12-24 15:00:20.396000000', + }, { + id: 'sally_to_dr_finklestein', + in_reply_to: 'nonexistent_comment', + message: 'i’m running away', + updated: '2015-10-31 09:00:20.396000000', + }, { + id: 'sallys_defiance', + message: 'i will poison you so i can get away', + updated: '2015-10-31 15:00:20.396000000', + }]; }); - test('orphan replies', function() { + test('orphan replies', () => { assert.equal(4, element._orderedComments.length); }); - test('keyboard shortcuts', function() { - var expandCollapseStub = sinon.stub(element, '_expandCollapseComments'); + test('keyboard shortcuts', () => { + const expandCollapseStub = + sinon.stub(element, '_expandCollapseComments'); MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e'); assert.isTrue(expandCollapseStub.lastCall.calledWith(false)); MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e'); assert.isTrue(expandCollapseStub.lastCall.calledWith(true)); - expandCollapseStub.restore(); }); - test('comment in_reply_to is either null or most recent comment id', - function() { + test('comment in_reply_to is either null or most recent comment', () => { element._createReplyComment(element.comments[3], 'dummy', true); flushAsynchronousOperations(); assert.equal(element._orderedComments.length, 5); assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply'); }); - test('resolvable comments', function() { + test('resolvable comments', () => { assert.isFalse(element._unresolved); element._createReplyComment(element.comments[3], 'dummy', true, true); flushAsynchronousOperations(); assert.isTrue(element._unresolved); }); - test('_setInitialExpandedState', function() { + test('_setInitialExpandedState', () => { element._unresolved = true; element._setInitialExpandedState(); - var comments = getComments(); - for (var i = 0; i < element.comments.length; i++) { + for (let i = 0; i < element.comments.length; i++) { assert.isFalse(element.comments[i].collapsed); } element._unresolved = false; element._setInitialExpandedState(); - var comments = getComments(); - for (var i = 0; i < element.comments.length; i++) { + for (let i = 0; i < element.comments.length; i++) { assert.isTrue(element.comments[i].collapsed); } }); }); - test('_computeHostClass', function() { + test('_computeHostClass', () => { assert.equal(element._computeHostClass(true), 'unresolved'); assert.equal(element._computeHostClass(false), ''); }); - test('addDraft sets unresolved state correctly', function() { - var unresolved = true; + test('addDraft sets unresolved state correctly', () => { + let unresolved = true; element.comments = []; element.addDraft(null, null, unresolved); assert.equal(element.comments[0].unresolved, true); @@ -533,15 +525,15 @@ assert.equal(element.comments[0].unresolved, true); }); - test('_newDraft', function() { + test('_newDraft', () => { element.commentSide = 'left'; element.patchNum = 3; - var draft = element._newDraft(); + const draft = element._newDraft(); assert.equal(draft.__commentSide, 'left'); assert.equal(draft.patchNum, 3); }); - test('new comment gets created', function() { + test('new comment gets created', () => { element.comments = []; element.addOrEditDraft(1); assert.equal(element.comments.length, 1); @@ -552,7 +544,7 @@ assert.equal(element.comments.length, 2); }); - test('unresolved label', function() { + test('unresolved label', () => { element._unresolved = false; assert.isTrue(element.$.unresolvedLabel.hasAttribute('hidden')); element._unresolved = true;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html index 4c00132..2d08fc6 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -15,18 +15,24 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> +<link rel="import" href="../../shared/gr-textarea/gr-textarea.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> <link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html"> -<link rel="import" href="../../shared/gr-storage/gr-storage.html"> +<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../shared/gr-storage/gr-storage.html"> <link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html"> +<link rel="import" href="../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html"> +<link rel="import" href="../../../styles/shared-styles.html"> +<script src="../../../scripts/rootElement.js"></script> <dom-module id="gr-diff-comment"> <template> - <style> + <style include="shared-styles"> :host { display: block; font-family: var(--font-family); @@ -35,13 +41,13 @@ padding: 2px; }; } - :host[disabled] { + :host([disabled]) { pointer-events: none; } - :host[disabled] .container { + :host([disabled]) .container { opacity: .5; } - :host[is-robot-comment] { + :host([is-robot-comment]) { background-color: #cfe8fc; } .header { @@ -72,6 +78,8 @@ .date { justify-content: flex-end; margin-left: 5px; + min-width: 4.5em; + text-align: right; white-space: nowrap; } a.date:link, @@ -117,7 +125,6 @@ display: none; } .editing .editMessage { - background-color: #fff; display: block; } .show-hide { @@ -163,7 +170,7 @@ } #container.collapsed .actions, #container.collapsed gr-formatted-text, - #container.collapsed iron-autogrow-textarea { + #container.collapsed gr-textarea { display: none; } .resolve, @@ -177,6 +184,19 @@ color: #333; font-size: 12px; } + gr-confirm-dialog .main { + background-color: #fef; + display: flex; + flex-direction: column; + width: 100%; + } + #deleteBtn { + display: none; + margin-top: .5em; + } + #deleteBtn.showDeleteButtons { + display: block; + } </style> <div id="container" class="container" @@ -215,14 +235,14 @@ [[comment.robot_id]] </div> </template> - <iron-autogrow-textarea + <gr-textarea id="editTextarea" class="editMessage" autocomplete="on" + monospace disabled="{{disabled}}" rows="4" - bind-value="{{_messageText}}" - on-keydown="_handleTextareaKeydown"></iron-autogrow-textarea> + text="{{_messageText}}"></gr-textarea> <gr-formatted-text class="message" content="[[comment.message]]" no-trailing-margin="[[!comment.__draft]]" @@ -256,6 +276,12 @@ <div class="action unresolved hideOnPublished" hidden$="[[resolved]]"> Unresolved </div> + <gr-button + id="deleteBtn" + class$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]" + on-tap="_handleCommentDelete"> + Delete + </gr-button> </div> <div class="actions robotActions" hidden$="[[!_showRobotActions]]"> <gr-button class="action fix" @@ -265,6 +291,12 @@ </gr-button> </div> </div> + <gr-overlay id="overlay" with-backdrop> + <gr-confirm-delete-comment-dialog id="confirmDeleteComment" + on-confirm="_handleConfirmDeleteComment" + on-cancel="_handleCancelDeleteComment"> + </gr-confirm-delete-comment-dialog> + </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-storage id="storage"></gr-storage> </template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js index 0791193..15d5464 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -14,7 +14,7 @@ (function() { 'use strict'; - var STORAGE_DEBOUNCE_INTERVAL = 400; + const STORAGE_DEBOUNCE_INTERVAL = 400; Polymer({ is: 'gr-diff-comment', @@ -90,6 +90,10 @@ }, projectConfig: Object, robotButtonDisabled: Boolean, + _isAdmin: { + type: Boolean, + value: false, + }, _xhrPromise: Object, // Used for testing. _messageText: { @@ -112,48 +116,65 @@ '_calculateActionstoShow(showActions, isRobotComment)', ], - attached: function() { + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + ], + + keyBindings: { + 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey', + 'esc': '_handleEsc', + }, + + attached() { if (this.editing) { this.collapsed = false; } else if (this.comment) { this.collapsed = this.comment.collapsed; } + this._getIsAdmin().then(isAdmin => { + this._isAdmin = isAdmin; + }); }, - detached: function() { + detached() { this.cancelDebouncer('fire-update'); + this.$.editTextarea.closeDropdown(); }, - _computeShowHideText: function(collapsed) { + _computeShowHideText(collapsed) { return collapsed ? '◀' : '▼'; }, - _calculateActionstoShow: function(showActions, isRobotComment) { + _calculateActionstoShow(showActions, isRobotComment) { this._showHumanActions = showActions && !isRobotComment; this._showRobotActions = showActions && isRobotComment; }, - _isRobotComment: function(comment) { + _isRobotComment(comment) { this.isRobotComment = !!comment.robot_id; }, - isOnParent: function() { + isOnParent() { return this.side === 'PARENT'; }, - save: function() { + _getIsAdmin() { + return this.$.restAPI.getIsAdmin(); + }, + + save() { this.comment.message = this._messageText; this.disabled = true; this._eraseDraftComment(); - this._xhrPromise = this._saveDraft(this.comment).then(function(response) { + this._xhrPromise = this._saveDraft(this.comment).then(response => { this.disabled = false; if (!response.ok) { return response; } - return this.$.restAPI.getResponseObject(response).then(function(obj) { - var comment = obj; + return this.$.restAPI.getResponseObject(response).then(obj => { + const comment = obj; comment.__draft = true; // Maintain the ephemeral draft ID for identification by other // elements. @@ -165,14 +186,18 @@ this.editing = false; this._fireSave(); return obj; - }.bind(this)); - }.bind(this)).catch(function(err) { + }); + }).catch(err => { this.disabled = false; throw err; - }.bind(this)); + }); }, - _eraseDraftComment: function() { + _eraseDraftComment() { + // Prevents a race condition in which removing the draft comment occurs + // prior to it being saved. + this.cancelDebouncer('store'); + this.$.storage.eraseDraftComment({ changeNum: this.changeNum, patchNum: this._getPatchNum(), @@ -182,7 +207,7 @@ }); }, - _commentChanged: function(comment) { + _commentChanged(comment) { this.editing = !!comment.__editing; this.resolved = !comment.unresolved; if (this.editing) { // It's a new draft/reply, notify. @@ -190,41 +215,31 @@ } }, - _getEventPayload: function(opt_mixin) { - var payload = { + _getEventPayload(opt_mixin) { + return Object.assign({}, opt_mixin, { comment: this.comment, patchNum: this.patchNum, - }; - for (var k in opt_mixin) { - payload[k] = opt_mixin[k]; - } - return payload; + }); }, - _fireSave: function() { + _fireSave() { this.fire('comment-save', this._getEventPayload()); }, - _fireUpdate: function() { - this.debounce('fire-update', function() { + _fireUpdate() { + this.debounce('fire-update', () => { this.fire('comment-update', this._getEventPayload()); }); }, - _draftChanged: function(draft) { + _draftChanged(draft) { this.$.container.classList.toggle('draft', draft); }, - _editingChanged: function(editing, previousValue) { + _editingChanged(editing, previousValue) { this.$.container.classList.toggle('editing', editing); if (editing) { - var textarea = this.$.editTextarea.textarea; - // Put the cursor at the end always. - textarea.selectionStart = textarea.value.length; - textarea.selectionEnd = textarea.selectionStart; - this.async(function() { - textarea.focus(); - }.bind(this)); + this.$.editTextarea.putCursorAtEnd(); } if (this.comment && this.comment.id) { this.$$('.cancel').hidden = !editing; @@ -238,39 +253,37 @@ } }, - _computeLinkToComment: function(comment) { + _computeLinkToComment(comment) { return '#' + comment.line; }, - _computeSaveDisabled: function(draft) { + _computeDeleteButtonClass(isAdmin, draft) { + return isAdmin && !draft ? 'showDeleteButtons' : ''; + }, + + _computeSaveDisabled(draft) { return draft == null || draft.trim() == ''; }, - _handleTextareaKeydown: function(e) { - switch (e.keyCode) { - case 13: // 'enter' - if (this._messageText.length !== 0 && (e.metaKey || e.ctrlKey)) { - this._handleSave(e); - } - break; - case 27: // 'esc' - if (this._messageText.length === 0) { - this._handleCancel(e); - } - break; - case 83: // 's' - if (this._messageText.length !== 0 && e.ctrlKey) { - this._handleSave(e); - } - break; + _handleSaveKey(e) { + if (this._messageText.length) { + e.preventDefault(); + this._handleSave(e); } }, - _handleToggleCollapsed: function() { + _handleEsc(e) { + if (!this._messageText.length) { + e.preventDefault(); + this._handleCancel(e); + } + }, + + _handleToggleCollapsed() { this.collapsed = !this.collapsed; }, - _toggleCollapseClass: function(collapsed) { + _toggleCollapseClass(collapsed) { if (collapsed) { this.$.container.classList.add('collapsed'); } else { @@ -278,20 +291,20 @@ } }, - _commentMessageChanged: function(message) { + _commentMessageChanged(message) { this._messageText = message || ''; }, - _messageTextChanged: function(newValue, oldValue) { + _messageTextChanged(newValue, oldValue) { if (!this.comment || (this.comment && this.comment.id)) { return; } // Keep comment.message in sync so that gr-diff-comment-thread is aware // of the current message in the case that another comment is deleted. this.comment.message = this._messageText || ''; - this.debounce('store', function() { - var message = this._messageText; + this.debounce('store', () => { + const message = this._messageText; - var commentLocation = { + const commentLocation = { changeNum: this.changeNum, patchNum: this._getPatchNum(), path: this.comment.path, @@ -310,9 +323,9 @@ }, STORAGE_DEBOUNCE_INTERVAL); }, - _handleLinkTap: function(e) { + _handleLinkTap(e) { e.preventDefault(); - var hash = this._computeLinkToComment(this.comment); + const hash = this._computeLinkToComment(this.comment); // Don't add the hash to the window history if it's already there. // Otherwise you mess up expected back button behavior. if (window.location.hash == hash) { return; } @@ -321,52 +334,51 @@ page.show(window.location.pathname + hash, null, false); }, - _handleReply: function(e) { + _handleReply(e) { e.preventDefault(); this.fire('create-reply-comment', this._getEventPayload(), {bubbles: false}); }, - _handleQuote: function(e) { + _handleQuote(e) { e.preventDefault(); this.fire('create-reply-comment', this._getEventPayload({quote: true}), {bubbles: false}); }, - _handleFix: function(e) { + _handleFix(e) { e.preventDefault(); this.fire('create-fix-comment', this._getEventPayload({quote: true}), {bubbles: false}); }, - _handleAck: function(e) { + _handleAck(e) { e.preventDefault(); this.fire('create-ack-comment', this._getEventPayload(), {bubbles: false}); }, - _handleDone: function(e) { + _handleDone(e) { e.preventDefault(); this.fire('create-done-comment', this._getEventPayload(), {bubbles: false}); }, - _handleEdit: function(e) { + _handleEdit(e) { e.preventDefault(); this._messageText = this.comment.message; this.editing = true; }, - _handleSave: function(e) { + _handleSave(e) { e.preventDefault(); this.set('comment.__editing', false); this.save(); }, - _handleCancel: function(e) { + _handleCancel(e) { e.preventDefault(); - if (!this.comment.message || - this.comment.message.trim().length === 0) { + if (!this.comment.message || this.comment.message.trim().length === 0) { this._fireDiscard(); return; } @@ -374,12 +386,12 @@ this.editing = false; }, - _fireDiscard: function() { + _fireDiscard() { this.cancelDebouncer('fire-update'); this.fire('comment-discard', this._getEventPayload()); }, - _handleDiscard: function(e) { + _handleDiscard(e) { e.preventDefault(); if (!this.comment.__draft) { throw Error('Cannot discard a non-draft comment.'); @@ -394,32 +406,31 @@ return; } - this._xhrPromise = this._deleteDraft(this.comment).then( - function(response) { - this.disabled = false; - if (!response.ok) { return response; } + this._xhrPromise = this._deleteDraft(this.comment).then(response => { + this.disabled = false; + if (!response.ok) { return response; } - this._fireDiscard(); - }.bind(this)).catch(function(err) { - this.disabled = false; - throw err; - }.bind(this)); + this._fireDiscard(); + }).catch(err => { + this.disabled = false; + throw err; + }); }, - _saveDraft: function(draft) { + _saveDraft(draft) { return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft); }, - _deleteDraft: function(draft) { + _deleteDraft(draft) { return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum, draft); }, - _getPatchNum: function() { + _getPatchNum() { return this.isOnParent() ? 'PARENT' : this.patchNum; }, - _loadLocalDraft: function(changeNum, patchNum, comment) { + _loadLocalDraft(changeNum, patchNum, comment) { // Only apply local drafts to comments that haven't been saved // remotely, and haven't been given a default message already. // @@ -430,8 +441,8 @@ return; } - var draft = this.$.storage.getDraftComment({ - changeNum: changeNum, + const draft = this.$.storage.getDraftComment({ + changeNum, patchNum: this._getPatchNum(), path: comment.path, line: comment.line, @@ -443,21 +454,42 @@ } }, - _handleMouseEnter: function(e) { + _handleMouseEnter(e) { this.fire('comment-mouse-over', this._getEventPayload()); }, - _handleMouseLeave: function(e) { + _handleMouseLeave(e) { this.fire('comment-mouse-out', this._getEventPayload()); }, - _handleToggleResolved: function() { + _handleToggleResolved() { this.resolved = !this.resolved; }, - _toggleResolved: function(resolved) { + _toggleResolved(resolved) { this.comment.unresolved = !resolved; this.fire('comment-update', this._getEventPayload()); }, + + _handleCommentDelete() { + Polymer.dom(Gerrit.getRootElement()).appendChild(this.$.overlay); + this.async(() => { + this.$.overlay.open(); + }, 1); + }, + + _handleCancelDeleteComment() { + Polymer.dom(Gerrit.getRootElement()).removeChild(this.$.overlay); + this.$.overlay.close(); + }, + + _handleConfirmDeleteComment() { + this.$.restAPI.deleteComment( + this.changeNum, this.patchNum, this.comment.id, + this.$.confirmDeleteComment.message).then(newComment => { + this._handleCancelDeleteComment(); + this.comment = newComment; + }); + }, }); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html index 919a64f..c4d1273 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -20,10 +20,10 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../bower_components/page/page.js"></script> <script src="../../../scripts/util.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-diff-comment.html"> <script>void(0);</script> @@ -47,12 +47,12 @@ return getComputedStyle(el).getPropertyValue('display') !== 'none'; } - suite('gr-diff-comment tests', function() { - var element; - var sandbox; - setup(function() { + suite('gr-diff-comment tests', () => { + let element; + let sandbox; + setup(() => { stub('gr-rest-api-interface', { - getAccount: function() { return Promise.resolve(null); }, + getAccount() { return Promise.resolve(null); }, }); element = fixture('basic'); element.comment = { @@ -68,18 +68,18 @@ sandbox = sinon.sandbox.create(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('collapsible comments', function() { + test('collapsible comments', () => { // When a comment (not draft) is loaded, it should be collapsed assert.isTrue(element.collapsed); assert.isFalse(isVisible(element.$$('gr-formatted-text')), 'gr-formatted-text is not visible'); assert.isFalse(isVisible(element.$$('.actions')), 'actions are not visible'); - assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')), + assert.isFalse(isVisible(element.$$('gr-textarea')), 'textarea is not visible'); // The header middle content is only visible when comments are collapsed. @@ -94,27 +94,26 @@ 'gr-formatted-text is visible'); assert.isTrue(isVisible(element.$$('.actions')), 'actions are visible'); - assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')), + assert.isFalse(isVisible(element.$$('gr-textarea')), 'textarea is not visible'); assert.isFalse(isVisible(element.$$('.collapsedContent')), 'header middle content is not visible'); }); - test('clicking on date link does not trigger nav', function() { - var showStub = sinon.stub(page, 'show'); - var dateEl = element.$$('.date'); + test('clicking on date link does not trigger nav', () => { + const showStub = sinon.stub(page, 'show'); + const dateEl = element.$$('.date'); assert.ok(dateEl); MockInteractions.tap(dateEl); - var dest = window.location.pathname + '#5'; + const dest = window.location.pathname + '#5'; assert(showStub.lastCall.calledWithExactly(dest, null, false), 'Should navigate to ' + dest + ' without triggering nav'); showStub.restore(); }); - test('message is not retrieved from storage when other editing is true', - function(done) { - var storageStub = sandbox.stub(element.$.storage, 'getDraftComment'); - var loadSpy = sandbox.spy(element, '_loadLocalDraft'); + test('message is not retrieved from storage when other edits', done => { + const storageStub = sandbox.stub(element.$.storage, 'getDraftComment'); + const loadSpy = sandbox.spy(element, '_loadLocalDraft'); element.changeNum = 1; element.patchNum = 1; @@ -126,17 +125,16 @@ line: 5, __otherEditing: true, }; - flush(function() { + flush(() => { assert.isTrue(loadSpy.called); assert.isFalse(storageStub.called); done(); }); }); - test('message is retrieved from storage when there is no other editing', - function(done) { - var storageStub = sandbox.stub(element.$.storage, 'getDraftComment'); - var loadSpy = sandbox.spy(element, '_loadLocalDraft'); + test('message is retrieved from storage when no other edits', done => { + const storageStub = sandbox.stub(element.$.storage, 'getDraftComment'); + const loadSpy = sandbox.spy(element, '_loadLocalDraft'); element.changeNum = 1; element.patchNum = 1; @@ -147,14 +145,14 @@ }, line: 5, }; - flush(function() { + flush(() => { assert.isTrue(loadSpy.called); assert.isTrue(storageStub.called); done(); }); }); - test('_getPatchNum', function() { + test('_getPatchNum', () => { element.side = 'PARENT'; element.patchNum = 1; assert.equal(element._getPatchNum(), 'PARENT'); @@ -162,13 +160,13 @@ assert.equal(element._getPatchNum(), 1); }); - test('comment expand and collapse', function() { + test('comment expand and collapse', () => { element.collapsed = true; assert.isFalse(isVisible(element.$$('gr-formatted-text')), 'gr-formatted-text is not visible'); assert.isFalse(isVisible(element.$$('.actions')), 'actions are not visible'); - assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')), + assert.isFalse(isVisible(element.$$('gr-textarea')), 'textarea is not visible'); assert.isTrue(isVisible(element.$$('.collapsedContent')), 'header middle content is visible'); @@ -179,14 +177,14 @@ 'gr-formatted-text is visible'); assert.isTrue(isVisible(element.$$('.actions')), 'actions are visible'); - assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')), + assert.isFalse(isVisible(element.$$('gr-textarea')), 'textarea is not visible'); assert.isFalse(isVisible(element.$$('.collapsedContent')), 'header middle content is is not visible'); }); - suite('while editing', function() { - setup(function() { + suite('while editing', () => { + setup(() => { element.editing = true; element._messageText = 'test'; sandbox.stub(element, '_handleCancel'); @@ -194,75 +192,108 @@ flushAsynchronousOperations(); }); - suite('when text is empty', function() { - setup(function() { + suite('when text is empty', () => { + setup(() => { element._messageText = ''; }); - test('esc closes comment when text is empty', function() { + test('esc closes comment when text is empty', () => { MockInteractions.pressAndReleaseKeyOn( element.$.editTextarea, 27); // esc assert.isTrue(element._handleCancel.called); }); - test('ctrl+enter does not save', function() { + test('ctrl+enter does not save', () => { MockInteractions.pressAndReleaseKeyOn( element.$.editTextarea, 13, 'ctrl'); // ctrl + enter assert.isFalse(element._handleSave.called); }); - test('meta+enter does not save', function() { + test('meta+enter does not save', () => { MockInteractions.pressAndReleaseKeyOn( element.$.editTextarea, 13, 'meta'); // meta + enter assert.isFalse(element._handleSave.called); }); - test('ctrl+s does not save', function() { + test('ctrl+s does not save', () => { MockInteractions.pressAndReleaseKeyOn( element.$.editTextarea, 83, 'ctrl'); // ctrl + s assert.isFalse(element._handleSave.called); }); }); - test('esc does not close comment that has content', function() { + test('esc does not close comment that has content', () => { MockInteractions.pressAndReleaseKeyOn( element.$.editTextarea, 27); // esc assert.isFalse(element._handleCancel.called); }); - test('ctrl+enter saves', function() { + test('ctrl+enter saves', () => { MockInteractions.pressAndReleaseKeyOn( element.$.editTextarea, 13, 'ctrl'); // ctrl + enter assert.isTrue(element._handleSave.called); }); - test('meta+enter saves', function() { + test('meta+enter saves', () => { MockInteractions.pressAndReleaseKeyOn( element.$.editTextarea, 13, 'meta'); // meta + enter assert.isTrue(element._handleSave.called); }); - test('ctrl+s saves', function() { + test('ctrl+s saves', () => { MockInteractions.pressAndReleaseKeyOn( element.$.editTextarea, 83, 'ctrl'); // ctrl + s assert.isTrue(element._handleSave.called); }); }); + test('delete comment button for non-admins is hidden', () => { + element._isAdmin = false; + assert.isFalse(element.$$('.action.delete') + .classList.contains('showDeleteButtons')); + }); + + test('delete comment button for admins with draft is hidden', () => { + element._isAdmin = false; + element.draft = true; + assert.isFalse(element.$$('.action.delete') + .classList.contains('showDeleteButtons')); + }); + + test('delete comment', done => { + sandbox.stub( + element.$.restAPI, 'deleteComment').returns(Promise.resolve({})); + sandbox.spy(element.$.overlay, 'open'); + element.changeNum = 42; + element.patchNum = 0xDEADBEEF; + element._isAdmin = true; + assert.isTrue(element.$$('.action.delete') + .classList.contains('showDeleteButtons')); + MockInteractions.tap(element.$$('.action.delete')); + flush(() => { + element.$.overlay.open.lastCall.returnValue.then(() => { + element.$.confirmDeleteComment.message = 'removal reason'; + element._handleConfirmDeleteComment(); + assert.isTrue(element.$.restAPI.deleteComment.calledWith( + 42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason')); + done(); + }); + }); + }); }); - suite('gr-diff-comment draft tests', function() { - var element; - var sandbox; + suite('gr-diff-comment draft tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getAccount: function() { return Promise.resolve(null); }, - saveDiffDraft: function() { + getAccount() { return Promise.resolve(null); }, + saveDiffDraft() { return Promise.resolve({ ok: true, - text: function() { + text() { return Promise.resolve( - ')]}\'\n{' + + ')]}\'\n{' + '"id": "baf0414d_40572e03",' + '"path": "/path/to/file",' + '"line": 5,' + @@ -273,12 +304,12 @@ }, }); }, - removeChangeReviewer: function() { + removeChangeReviewer() { return Promise.resolve({ok: true}); }, }); stub('gr-storage', { - getDraftComment: function() { return null; }, + getDraftComment() { return null; }, }); element = fixture('draft'); element.changeNum = 42; @@ -295,11 +326,11 @@ sandbox = sinon.sandbox.create(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('button visibility states', function() { + test('button visibility states', () => { element.showActions = false; assert.isTrue(element.$$('.humanActions').hasAttribute('hidden')); assert.isTrue(element.$$('.robotActions').hasAttribute('hidden')); @@ -356,13 +387,13 @@ assert.isFalse(element.$$('.robotActions').hasAttribute('hidden')); }); - test('collapsible drafts', function() { + test('collapsible drafts', () => { assert.isTrue(element.collapsed); assert.isFalse(isVisible(element.$$('gr-formatted-text')), 'gr-formatted-text is not visible'); assert.isFalse(isVisible(element.$$('.actions')), 'actions are not visible'); - assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')), + assert.isFalse(isVisible(element.$$('gr-textarea')), 'textarea is not visible'); assert.isTrue(isVisible(element.$$('.collapsedContent')), 'header middle content is visible'); @@ -373,7 +404,7 @@ 'gr-formatted-text is visible'); assert.isTrue(isVisible(element.$$('.actions')), 'actions are visible'); - assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')), + assert.isFalse(isVisible(element.$$('gr-textarea')), 'textarea is not visible'); assert.isFalse(isVisible(element.$$('.collapsedContent')), 'header middle content is is not visible'); @@ -386,7 +417,7 @@ 'gr-formatted-text is not visible'); assert.isTrue(isVisible(element.$$('.actions')), 'actions are visible'); - assert.isTrue(isVisible(element.$$('iron-autogrow-textarea')), + assert.isTrue(isVisible(element.$$('gr-textarea')), 'textarea is visible'); assert.isFalse(isVisible(element.$$('.collapsedContent')), 'header middle content is not visible'); @@ -399,7 +430,7 @@ 'gr-formatted-text is not visible'); assert.isFalse(isVisible(element.$$('.actions')), 'actions are not visible'); - assert.isFalse(isVisible(element.$$('iron-autogrow-textarea')), + assert.isFalse(isVisible(element.$$('gr-textarea')), 'textarea is not visible'); assert.isTrue(isVisible(element.$$('.collapsedContent')), 'header middle content is visible'); @@ -411,32 +442,32 @@ 'gr-formatted-text is not visible'); assert.isTrue(isVisible(element.$$('.actions')), 'actions are visible'); - assert.isTrue(isVisible(element.$$('iron-autogrow-textarea')), + assert.isTrue(isVisible(element.$$('gr-textarea')), 'textarea is visible'); assert.isFalse(isVisible(element.$$('.collapsedContent')), 'header middle content is not visible'); }); - test('draft creation/cancelation', function(done) { + test('draft creation/cancelation', done => { assert.isFalse(element.editing); MockInteractions.tap(element.$$('.edit')); assert.isTrue(element.editing); element._messageText = ''; - var eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment'); + const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment'); // Save should be disabled on an empty message. - var disabled = element.$$('.save').hasAttribute('disabled'); + let disabled = element.$$('.save').hasAttribute('disabled'); assert.isTrue(disabled, 'save button should be disabled.'); element._messageText = ' '; disabled = element.$$('.save').hasAttribute('disabled'); assert.isTrue(disabled, 'save button should be disabled.'); - var updateStub = sinon.stub(); + const updateStub = sinon.stub(); element.addEventListener('comment-update', updateStub); - var numDiscardEvents = 0; - element.addEventListener('comment-discard', function(e) { + let numDiscardEvents = 0; + element.addEventListener('comment-discard', e => { numDiscardEvents++; assert.isFalse(eraseMessageDraftSpy.called); if (numDiscardEvents === 2) { @@ -450,32 +481,32 @@ MockInteractions.pressAndReleaseKeyOn(element.$.editTextarea, 27); // esc }); - test('draft discard removes message from storage', function(done) { + test('draft discard removes message from storage', done => { element._messageText = ''; - var eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment'); + const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment'); - var numDiscardEvents = 0; - element.addEventListener('comment-discard', function(e) { + element.addEventListener('comment-discard', e => { assert.isTrue(eraseMessageDraftSpy.called); done(); }); MockInteractions.tap(element.$$('.discard')); }); - test('ctrl+s saves comment', function(done) { - var stub = sinon.stub(element, 'save', function() { + test('ctrl+s saves comment', done => { + const stub = sinon.stub(element, 'save', () => { assert.isTrue(stub.called); stub.restore(); done(); }); element._messageText = 'is that the horse from horsing around??'; MockInteractions.pressAndReleaseKeyOn( - element.$.editTextarea.textarea, - 83, 'ctrl'); // 'ctrl + s' + element.$.editTextarea.$.textarea.textarea, + 83, 'ctrl'); // 'ctrl + s' }); - test('draft saving/editing', function(done) { - var fireStub = sinon.stub(element, 'fire'); + test('draft saving/editing', done => { + const fireStub = sinon.stub(element, 'fire'); + const cancelDebounce = sandbox.stub(element, 'cancelDebouncer'); element.draft = true; MockInteractions.tap(element.$$('.edit')); @@ -483,7 +514,7 @@ element.flushDebouncer('fire-update'); element.flushDebouncer('store'); assert(fireStub.calledWith('comment-update'), - 'comment-update should be sent'); + 'comment-update should be sent'); assert.deepEqual(fireStub.lastCall.args, [ 'comment-update', { comment: { @@ -504,9 +535,11 @@ assert.isTrue(element.disabled, 'Element should be disabled when creating draft.'); - element._xhrPromise.then(function(draft) { + element._xhrPromise.then(draft => { assert(fireStub.calledWith('comment-save'), - 'comment-save should be sent'); + 'comment-save should be sent'); + assert(cancelDebounce.calledWith('store')); + assert.deepEqual(fireStub.lastCall.args[1], { comment: { __commentSide: 'right', @@ -522,10 +555,10 @@ patchNum: 1, }); assert.isFalse(element.disabled, - 'Element should be enabled when done creating draft.'); + 'Element should be enabled when done creating draft.'); assert.equal(draft.message, 'saved!'); assert.isFalse(element.editing); - }).then(function() { + }).then(() => { MockInteractions.tap(element.$$('.edit')); element._messageText = 'You’ll be delivering a package to Chapek 9, ' + 'a world where humans are killed on sight.'; @@ -533,7 +566,7 @@ assert.isTrue(element.disabled, 'Element should be disabled when updating draft.'); - element._xhrPromise.then(function(draft) { + element._xhrPromise.then(draft => { assert.isFalse(element.disabled, 'Element should be enabled when done updating draft.'); assert.equal(draft.message, 'saved!'); @@ -544,26 +577,26 @@ }); }); - test('clicking on date link does not trigger nav', function() { - var showStub = sinon.stub(page, 'show'); - var dateEl = element.$$('.date'); + test('clicking on date link does not trigger nav', () => { + const showStub = sinon.stub(page, 'show'); + const dateEl = element.$$('.date'); assert.ok(dateEl); MockInteractions.tap(dateEl); - var dest = window.location.pathname + '#5'; + const dest = window.location.pathname + '#5'; assert(showStub.lastCall.calledWithExactly(dest, null, false), 'Should navigate to ' + dest + ' without triggering nav'); showStub.restore(); }); - test('proper event fires on resolve', function(done) { - element.addEventListener('comment-update', function(e) { + test('proper event fires on resolve', done => { + element.addEventListener('comment-update', e => { assert.isTrue(e.detail.comment.unresolved); done(); }); MockInteractions.tap(element.$$('.resolve input')); }); - test('resolved comment state indicated by checkbox', function() { + test('resolved comment state indicated by checkbox', () => { element.comment = {unresolved: false}; assert.isTrue(element.$$('.resolve input').checked); element.comment = {unresolved: true};
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html index 2d0786a..3ae3159 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.html
@@ -23,6 +23,7 @@ id="cursorManager" scroll-behavior="[[_scrollBehavior]]" cursor-target-class="target-row" + focus-on-move target="{{diffRow}}"></gr-cursor-manager> </template> <script src="gr-diff-cursor.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js index e40ccf3..e47db8f 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -14,23 +14,23 @@ (function() { 'use strict'; - var DiffSides = { + const DiffSides = { LEFT: 'left', RIGHT: 'right', }; - var DiffViewMode = { + const DiffViewMode = { SIDE_BY_SIDE: 'SIDE_BY_SIDE', UNIFIED: 'UNIFIED_DIFF', }; - var ScrollBehavior = { + const ScrollBehavior = { KEEP_VISIBLE: 'keep-visible', NEVER: 'never', }; - var LEFT_SIDE_CLASS = 'target-side-left'; - var RIGHT_SIDE_CLASS = 'target-side-right'; + const LEFT_SIDE_CLASS = 'target-side-left'; + const RIGHT_SIDE_CLASS = 'target-side-right'; Polymer({ is: 'gr-diff-cursor', @@ -54,9 +54,7 @@ */ diffs: { type: Array, - value: function() { - return []; - }, + value() { return []; }, }, /** @@ -87,30 +85,30 @@ '_diffsChanged(diffs.splices)', ], - attached: function() { + attached() { // Catch when users are scrolling as the view loads. this.listen(window, 'scroll', '_handleWindowScroll'); }, - detached: function() { + detached() { this.unlisten(window, 'scroll', '_handleWindowScroll'); }, - moveLeft: function() { + moveLeft() { this.side = DiffSides.LEFT; if (this._isTargetBlank()) { this.moveUp(); } }, - moveRight: function() { + moveRight() { this.side = DiffSides.RIGHT; if (this._isTargetBlank()) { this.moveUp(); } }, - moveDown: function() { + moveDown() { if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { this.$.cursorManager.next(this._rowHasSide.bind(this)); } else { @@ -118,7 +116,7 @@ } }, - moveUp: function() { + moveUp() { if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { this.$.cursorManager.previous(this._rowHasSide.bind(this)); } else { @@ -126,31 +124,31 @@ } }, - moveToNextChunk: function() { + moveToNextChunk() { this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this), - function(target) { + target => { return target.parentNode.scrollHeight; }); this._fixSide(); }, - moveToPreviousChunk: function() { + moveToPreviousChunk() { this.$.cursorManager.previous(this._isFirstRowOfChunk.bind(this)); this._fixSide(); }, - moveToNextCommentThread: function() { + moveToNextCommentThread() { this.$.cursorManager.next(this._rowHasThread.bind(this)); this._fixSide(); }, - moveToPreviousCommentThread: function() { + moveToPreviousCommentThread() { this.$.cursorManager.previous(this._rowHasThread.bind(this)); this._fixSide(); }, - moveToLineNumber: function(number, side) { - var row = this._findRowByNumber(number, side); + moveToLineNumber(number, side, opt_path) { + const row = this._findRowByNumberAndFile(number, side, opt_path); if (row) { this.side = side; this.$.cursorManager.setCursor(row); @@ -161,8 +159,8 @@ * Get the line number element targeted by the cursor row and side. * @return {DOMElement} */ - getTargetLineElement: function() { - var lineElSelector = '.lineNum'; + getTargetLineElement() { + let lineElSelector = '.lineNum'; if (!this.diffRow) { return; @@ -175,20 +173,20 @@ return this.diffRow.querySelector(lineElSelector); }, - getTargetDiffElement: function() { + getTargetDiffElement() { // Find the parent diff element of the cursor row. - for (var diff = this.diffRow; diff; diff = diff.parentElement) { + for (let diff = this.diffRow; diff; diff = diff.parentElement) { if (diff.tagName === 'GR-DIFF') { return diff; } } return null; }, - moveToFirstChunk: function() { + moveToFirstChunk() { this.$.cursorManager.moveToStart(); this.moveToNextChunk(); }, - reInitCursor: function() { + reInitCursor() { this._updateStops(); if (this.initialLineNumber) { this.moveToLineNumber(this.initialLineNumber, this.side); @@ -198,14 +196,14 @@ } }, - _handleWindowScroll: function() { + _handleWindowScroll() { if (this._listeningForScroll) { this._scrollBehavior = ScrollBehavior.NEVER; this._listeningForScroll = false; } }, - handleDiffUpdate: function() { + handleDiffUpdate() { this._updateStops(); if (!this.diffRow) { @@ -215,7 +213,7 @@ this._listeningForScroll = false; }, - _handleDiffRenderStart: function() { + _handleDiffRenderStart() { this._listeningForScroll = true; }, @@ -225,12 +223,12 @@ * Returns an empty string if an address is not available. * @return {String} */ - getAddress: function() { + getAddress() { if (!this.diffRow) { return ''; } // Get the line-number cell targeted by the cursor. If the mode is unified // then prefer the revision cell if available. - var cell; + let cell; if (this._getViewMode() === DiffViewMode.UNIFIED) { cell = this.diffRow.querySelector('.lineNum.right'); if (!cell) { @@ -241,13 +239,13 @@ } if (!cell) { return ''; } - var number = cell.getAttribute('data-value'); + const number = cell.getAttribute('data-value'); if (!number || number === 'FILE') { return ''; } return (cell.matches('.left') ? 'b' : '') + number; }, - _getViewMode: function() { + _getViewMode() { if (!this.diffRow) { return null; } @@ -259,20 +257,20 @@ } }, - _rowHasSide: function(row) { - var selector = (this.side === DiffSides.LEFT ? '.left' : '.right') + + _rowHasSide(row) { + const selector = (this.side === DiffSides.LEFT ? '.left' : '.right') + ' + .content'; return !!row.querySelector(selector); }, - _isFirstRowOfChunk: function(row) { - var parentClassList = row.parentNode.classList; + _isFirstRowOfChunk(row) { + const parentClassList = row.parentNode.classList; return parentClassList.contains('section') && parentClassList.contains('delta') && !row.previousSibling; }, - _rowHasThread: function(row) { + _rowHasThread(row) { return row.querySelector('gr-diff-comment-thread'); }, @@ -280,7 +278,7 @@ * If we jumped to a row where there is no content on the current side then * switch to the alternate side. */ - _fixSide: function() { + _fixSide() { if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE && this._isTargetBlank()) { this.side = this.side === DiffSides.LEFT ? @@ -288,24 +286,24 @@ } }, - _isTargetBlank: function() { + _isTargetBlank() { if (!this.diffRow) { return false; } - var actions = this._getActionsForRow(); + const actions = this._getActionsForRow(); return (this.side === DiffSides.LEFT && !actions.left) || (this.side === DiffSides.RIGHT && !actions.right); }, - _rowChanged: function(newRow, oldRow) { + _rowChanged(newRow, oldRow) { if (oldRow) { oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS); } this._updateSideClass(); }, - _updateSideClass: function() { + _updateSideClass() { if (!this.diffRow) { return; } @@ -315,12 +313,12 @@ this.diffRow); }, - _isActionType: function(type) { + _isActionType(type) { return type !== 'blank' && type !== 'contextControl'; }, - _getActionsForRow: function() { - var actions = {left: false, right: false}; + _getActionsForRow() { + const actions = {left: false, right: false}; if (this.diffRow) { actions.left = this._isActionType( this.diffRow.getAttribute('left-type')); @@ -330,14 +328,14 @@ return actions; }, - _getStops: function() { + _getStops() { return this.diffs.reduce( - function(stops, diff) { + (stops, diff) => { return stops.concat(diff.getCursorStops()); }, []); }, - _updateStops: function() { + _updateStops() { this.$.cursorManager.stops = this._getStops(); }, @@ -346,14 +344,14 @@ * removed from the cursor. * @private */ - _diffsChanged: function(changeRecord) { + _diffsChanged(changeRecord) { if (!changeRecord) { return; } this._updateStops(); - var splice; - var i; - for (var spliceIdx = 0; + let splice; + let i; + for (let spliceIdx = 0; changeRecord.indexSplices && spliceIdx < changeRecord.indexSplices.length; spliceIdx++) { @@ -371,15 +369,22 @@ i++) { this.unlisten(splice.removed[i], 'render-start', '_handleDiffRenderStart'); - this.unlisten(splice.removed[i], 'render', 'handleDiffUpdate'); + this.unlisten(splice.removed[i], + 'render-content', 'handleDiffUpdate'); } } }, - _findRowByNumber: function(targetNumber, side) { - var stops = this.$.cursorManager.stops; - var selector; - for (var i = 0; i < stops.length; i++) { + _findRowByNumberAndFile(targetNumber, side, opt_path) { + let stops; + if (opt_path) { + const diff = this.diffs.filter(diff => diff.path === opt_path)[0]; + stops = diff.getCursorStops(); + } else { + stops = this.$.cursorManager.stops; + } + let selector; + for (let i = 0; i < stops.length; i++) { selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]'; if (stops[i].querySelector(selector)) { return stops[i];
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html index a77c617..8be6e92 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="../gr-diff/gr-diff.html"> <link rel="import" href="./gr-diff-cursor.html"> <link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html"> @@ -38,17 +38,20 @@ </test-fixture> <script> - suite('gr-diff-cursor tests', function() { - var cursorElement; - var diffElement; - var mockDiffResponse; + suite('gr-diff-cursor tests', () => { + let sandbox; + let cursorElement; + let diffElement; + let mockDiffResponse; - setup(function(done) { + setup(done => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(false); }, + getLoggedIn() { return Promise.resolve(false); }, }); - var fixtureElems = fixture('basic'); + const fixtureElems = fixture('basic'); mockDiffResponse = fixtureElems[0]; diffElement = fixtureElems[1]; cursorElement = fixtureElems[2]; @@ -56,41 +59,43 @@ // Register the diff with the cursor. cursorElement.push('diffs', diffElement); - diffElement.$.restAPI.getDiffPreferences().then(function(prefs) { + diffElement.$.restAPI.getDiffPreferences().then(prefs => { diffElement.prefs = prefs; }); - sinon.stub(diffElement, '_getDiff', function() { + sandbox.stub(diffElement, '_getDiff', () => { return Promise.resolve(mockDiffResponse.diffResponse); }); - sinon.stub(diffElement, '_getDiffComments', function() { + sandbox.stub(diffElement, '_getDiffComments', () => { return Promise.resolve({baseComments: [], comments: []}); }); - sinon.stub(diffElement, '_getDiffDrafts', function() { + sandbox.stub(diffElement, '_getDiffDrafts', () => { return Promise.resolve({baseComments: [], comments: []}); }); - sinon.stub(diffElement, '_getDiffRobotComments', function() { + sandbox.stub(diffElement, '_getDiffRobotComments', () => { return Promise.resolve({baseComments: [], comments: []}); }); - var setupDone = function() { + const setupDone = () => { cursorElement.moveToFirstChunk(); - done(); diffElement.removeEventListener('render', setupDone); + done(); }; diffElement.addEventListener('render', setupDone); diffElement.reload(); }); - test('diff cursor functionality (side-by-side)', function() { + teardown(() => sandbox.restore()); + + test('diff cursor functionality (side-by-side)', () => { // The cursor has been initialized to the first delta. assert.isOk(cursorElement.diffRow); - var firstDeltaRow = diffElement.$$('.section.delta .diff-row'); + const firstDeltaRow = diffElement.$$('.section.delta .diff-row'); assert.equal(cursorElement.diffRow, firstDeltaRow); cursorElement.moveDown(); @@ -104,7 +109,7 @@ assert.equal(cursorElement.diffRow, firstDeltaRow); }); - test('cursor scroll behavior', function() { + test('cursor scroll behavior', () => { cursorElement._handleDiffRenderStart(); assert.equal(cursorElement._scrollBehavior, 'keep-visible'); @@ -115,11 +120,10 @@ assert.equal(cursorElement._scrollBehavior, 'keep-visible'); }); - suite('unified diff', function() { - - setup(function(done) { + suite('unified diff', () => { + setup(done => { // We must allow the diff to re-render after setting the viewMode. - var renderHandler = function() { + const renderHandler = function() { diffElement.removeEventListener('render', renderHandler); cursorElement.reInitCursor(); done(); @@ -128,11 +132,11 @@ diffElement.viewMode = 'UNIFIED_DIFF'; }); - test('diff cursor functionality (unified)', function() { + test('diff cursor functionality (unified)', () => { // The cursor has been initialized to the first delta. assert.isOk(cursorElement.diffRow); - var firstDeltaRow = diffElement.$$('.section.delta .diff-row'); + let firstDeltaRow = diffElement.$$('.section.delta .diff-row'); assert.equal(cursorElement.diffRow, firstDeltaRow); firstDeltaRow = diffElement.$$('.section.delta .diff-row'); @@ -150,19 +154,19 @@ }); }); - test('cursor side functionality', function() { + test('cursor side functionality', () => { // The side only applies to side-by-side mode, which should be the default // mode. assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE'); - var firstDeltaSection = diffElement.$$('.section.delta'); - var firstDeltaRow = firstDeltaSection.querySelector('.diff-row'); + const firstDeltaSection = diffElement.$$('.section.delta'); + const firstDeltaRow = firstDeltaSection.querySelector('.diff-row'); // Because the first delta in this diff is on the right, it should be set // to the right side. assert.equal(cursorElement.side, 'right'); assert.equal(cursorElement.diffRow, firstDeltaRow); - var firstIndex = cursorElement.$.cursorManager.index; + const firstIndex = cursorElement.$.cursorManager.index; // Move the side to the left. Because this delta only has a right side, we // should be moved up to the previous line where there is content on the @@ -186,16 +190,16 @@ firstDeltaSection.nextSibling); }); - test('chunk skip functionality', function() { - var chunks = Polymer.dom(diffElement.root).querySelectorAll( + test('chunk skip functionality', () => { + const chunks = Polymer.dom(diffElement.root).querySelectorAll( '.section.delta'); - var indexOfChunk = function(chunk) { + const indexOfChunk = function(chunk) { return Array.prototype.indexOf.call(chunks, chunk); }; // We should be initialized to the first chunk. Since this chunk only has // content on the right side, our side should be right. - var currentIndex = indexOfChunk(cursorElement.diffRow.parentElement); + let currentIndex = indexOfChunk(cursorElement.diffRow.parentElement); assert.equal(currentIndex, 0); assert.equal(cursorElement.side, 'right'); @@ -204,17 +208,17 @@ // Since this chunk only has content on the left side. we should have been // automatically mvoed over. - var previousIndex = currentIndex; + const previousIndex = currentIndex; currentIndex = indexOfChunk(cursorElement.diffRow.parentElement); assert.equal(currentIndex, previousIndex + 1); assert.equal(cursorElement.side, 'left'); }); - test('initialLineNumber disabled', function(done) { - var moveToNumStub = sinon.stub(cursorElement, 'moveToLineNumber'); - var moveToChunkStub = sinon.stub(cursorElement, 'moveToFirstChunk'); + test('initialLineNumber disabled', done => { + const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber'); + const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk'); - diffElement.addEventListener('render', function() { + diffElement.addEventListener('render', () => { assert.isFalse(moveToNumStub.called); assert.isTrue(moveToChunkStub.called); done(); @@ -223,11 +227,11 @@ diffElement.reload(); }); - test('initialLineNumber enabled', function(done) { - var moveToNumStub = sinon.stub(cursorElement, 'moveToLineNumber'); - var moveToChunkStub = sinon.stub(cursorElement, 'moveToFirstChunk'); + test('initialLineNumber enabled', done => { + const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber'); + const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk'); - diffElement.addEventListener('render', function() { + diffElement.addEventListener('render', () => { assert.isFalse(moveToChunkStub.called); assert.isTrue(moveToNumStub.called); assert.equal(moveToNumStub.lastCall.args[0], 10); @@ -241,7 +245,7 @@ diffElement.reload(); }); - test('getAddress', function() { + test('getAddress', () => { // It should initialize to the first chunk: line 5 of the revision. assert.equal(cursorElement.getAddress(), '5'); @@ -266,13 +270,22 @@ assert.equal(cursorElement.getAddress(), ''); }); - test('_findRowByNumber', function() { + test('_findRowByNumberAndFile', () => { // Get the first ab row after the first chunk. - var row = Polymer.dom(diffElement.root).querySelectorAll('tr')[8]; + const row = Polymer.dom(diffElement.root).querySelectorAll('tr')[8]; // It should be line 8 on the right, but line 5 on the left. - assert.equal(cursorElement._findRowByNumber(8, 'right'), row); - assert.equal(cursorElement._findRowByNumber(5, 'left'), row); + assert.equal(cursorElement._findRowByNumberAndFile(8, 'right'), row); + assert.equal(cursorElement._findRowByNumberAndFile(5, 'left'), row); + }); + + test('expand context updates stops', done => { + sandbox.spy(cursorElement, 'handleDiffUpdate'); + MockInteractions.tap(diffElement.$$('.showContext')); + flush(() => { + assert.isTrue(cursorElement.handleDiffUpdate.called); + done(); + }); }); }); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js index bb5b938..9afbf2b 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
@@ -18,12 +18,12 @@ if (window.GrAnnotation) { return; } // TODO(wyatta): refactor this to be <MARK> rather than <HL>. - var ANNOTATION_TAG = 'HL'; + const ANNOTATION_TAG = 'HL'; // Astral code point as per https://mathiasbynens.be/notes/javascript-unicode - var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/; + const REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/; - var GrAnnotation = { + const GrAnnotation = { /** * The DOM API textContent.length calculation is broken when the text @@ -31,11 +31,11 @@ * @param {Text} A text node. * @return {Number} The length of the text. */ - getLength: function(node) { + getLength(node) { return this.getStringLength(node.textContent); }, - getStringLength: function(str) { + getStringLength(str) { return str.replace(REGEX_ASTRAL_SYMBOL, '_').length; }, @@ -44,14 +44,12 @@ * element. If the element has child elements, the range is split and * applied as deeply as possible. */ - annotateElement: function(parent, offset, length, cssClass) { - var nodes = [].slice.apply(parent.childNodes); - var node; - var nodeLength; - var subLength; + annotateElement(parent, offset, length, cssClass) { + const nodes = [].slice.apply(parent.childNodes); + let nodeLength; + let subLength; - for (var i = 0; i < nodes.length; i++) { - node = nodes[i]; + for (const node of nodes) { nodeLength = this.getLength(node); // If the current node is completely before the offset. @@ -85,8 +83,8 @@ * * @return {!Element} Wrapped node. */ - wrapInHighlight: function(node, cssClass) { - var hl; + wrapInHighlight(node, cssClass) { + let hl; if (node.tagName === ANNOTATION_TAG) { hl = node; hl.classList.add(cssClass); @@ -108,7 +106,7 @@ * @param {string} cssClass * @param {boolean=} opt_firstPart */ - splitAndWrapInHighlight: function(node, offset, cssClass, opt_firstPart) { + splitAndWrapInHighlight(node, offset, cssClass, opt_firstPart) { if (this.getLength(node) === offset || offset === 0) { return this.wrapInHighlight(node, cssClass); } else { @@ -130,14 +128,14 @@ * @param {number} offset * @return {!Node} Trailing Node. */ - splitNode: function(element, offset) { + splitNode(element, offset) { if (element instanceof Text) { return this.splitTextNode(element, offset); } - var tail = element.cloneNode(false); + const tail = element.cloneNode(false); element.parentElement.insertBefore(tail, element.nextSibling); // Skip nodes before offset. - var node = element.firstChild; + let node = element.firstChild; while (node && this.getLength(node) <= offset || this.getLength(node) === 0) { @@ -163,17 +161,17 @@ * @param {number} offset * @return {!Text} Trailing Text Node. */ - splitTextNode: function(node, offset) { + splitTextNode(node, offset) { if (node.textContent.match(REGEX_ASTRAL_SYMBOL)) { // TODO (viktard): Polyfill Array.from for IE10. - var head = Array.from(node.textContent); - var tail = head.splice(offset); - var parent = node.parentNode; + const head = Array.from(node.textContent); + const tail = head.splice(offset); + const parent = node.parentNode; // Split the content of the original node. node.textContent = head.join(''); - var tailNode = document.createTextNode(tail.join('')); + const tailNode = document.createTextNode(tail.join('')); if (parent) { parent.insertBefore(tailNode, node.nextSibling); } @@ -183,8 +181,8 @@ } }, - _annotateText: function(node, offset, length, cssClass) { - var nodeLength = this.getLength(node); + _annotateText(node, offset, length, cssClass) { + const nodeLength = this.getLength(node); // There are four cases: // 1) Entire node is highlighted.
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html index 0a03539..b237685 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="gr-annotation.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <script>void(0);</script> @@ -33,18 +33,18 @@ </test-fixture> <script> - suite('annotation', function() { - var str; - var parent; - var textNode; + suite('annotation', () => { + let str; + let parent; + let textNode; - setup(function() { + setup(() => { parent = fixture('basic'); textNode = parent.childNodes[0]; str = textNode.textContent; }); - test('_annotateText Case 1', function() { + test('_annotateText Case 1', () => { GrAnnotation._annotateText(textNode, 0, str.length, 'foobar'); assert.equal(parent.childNodes.length, 1); @@ -54,10 +54,10 @@ assert.equal(parent.childNodes[0].childNodes[0].textContent, str); }); - test('_annotateText Case 2', function() { - var length = 12; - var substr = str.substr(0, length); - var remainder = str.substr(length); + test('_annotateText Case 2', () => { + const length = 12; + const substr = str.substr(0, length); + const remainder = str.substr(length); GrAnnotation._annotateText(textNode, 0, length, 'foobar'); @@ -72,11 +72,11 @@ assert.equal(parent.childNodes[1].textContent, remainder); }); - test('_annotateText Case 3', function() { - var index = 12; - var length = str.length - index; - var remainder = str.substr(0, index); - var substr = str.substr(index); + test('_annotateText Case 3', () => { + const index = 12; + const length = str.length - index; + const remainder = str.substr(0, index); + const substr = str.substr(index); GrAnnotation._annotateText(textNode, index, length, 'foobar'); @@ -91,13 +91,13 @@ assert.equal(parent.childNodes[1].childNodes[0].textContent, substr); }); - test('_annotateText Case 4', function() { - var index = str.indexOf('dolor'); - var length = 'dolor '.length; + test('_annotateText Case 4', () => { + const index = str.indexOf('dolor'); + const length = 'dolor '.length; - var remainderPre = str.substr(0, index); - var substr = str.substr(index, length); - var remainderPost = str.substr(index + length); + const remainderPre = str.substr(0, index); + const substr = str.substr(index, length); + const remainderPost = str.substr(index + length); GrAnnotation._annotateText(textNode, index, length, 'foobar'); @@ -115,42 +115,42 @@ assert.equal(parent.childNodes[2].textContent, remainderPost); }); - test('_annotateElement design doc example', function() { - var layers = [ + test('_annotateElement design doc example', () => { + const layers = [ 'amet, ', 'inceptos ', 'amet, ', - 'et, suspendisse ince' + 'et, suspendisse ince', ]; // Apply the layers successively. - layers.forEach(function(layer, i) { + layers.forEach((layer, i) => { GrAnnotation.annotateElement( - parent, str.indexOf(layer), layer.length, 'layer-' + (i + 1)); + parent, str.indexOf(layer), layer.length, `layer-${i + 1}`); }); assert.equal(parent.textContent, str); // Layer 1: - var layer1 = parent.querySelectorAll('.layer-1'); + const layer1 = parent.querySelectorAll('.layer-1'); assert.equal(layer1.length, 1); assert.equal(layer1[0].textContent, layers[0]); assert.equal(layer1[0].parentElement, parent); // Layer 2: - var layer2 = parent.querySelectorAll('.layer-2'); + const layer2 = parent.querySelectorAll('.layer-2'); assert.equal(layer2.length, 1); assert.equal(layer2[0].textContent, layers[1]); assert.equal(layer2[0].parentElement, parent); // Layer 3: - var layer3 = parent.querySelectorAll('.layer-3'); + const layer3 = parent.querySelectorAll('.layer-3'); assert.equal(layer3.length, 1); assert.equal(layer3[0].textContent, layers[2]); assert.equal(layer3[0].parentElement, layer1[0]); // Layer 4: - var layer4 = parent.querySelectorAll('.layer-4'); + const layer4 = parent.querySelectorAll('.layer-4'); assert.equal(layer4.length, 3); assert.equal(layer4[0].textContent, 'et, '); @@ -168,13 +168,13 @@ layers[3]); }); - test('splitTextNode', function() { - var helloString = 'hello'; - var asciiString = 'ASCII'; - var unicodeString = 'Unic💢de'; + test('splitTextNode', () => { + const helloString = 'hello'; + const asciiString = 'ASCII'; + const unicodeString = 'Unic💢de'; - var node; - var tail; + let node; + let tail; // Non-unicode path: node = document.createTextNode(helloString + asciiString);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html index 814a760..7b9954d 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.html
@@ -16,11 +16,12 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../gr-selection-action-box/gr-selection-action-box.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-diff-highlight"> <template> - <style> - .contentWrapper ::content { + <style include="shared-styles"> + :host { position: relative; } .contentWrapper ::content .range { @@ -31,6 +32,13 @@ background-color: rgba(255,255,0,0.5); display: inline; } + gr-selection-action-box { + /** + * Needs z-index to apear above wrapped content, since it's inseted + * into DOM before it. + */ + z-index: 10; + } </style> <div class="contentWrapper"> <content></content>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js index e32d7d6..891794c 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -42,7 +42,7 @@ return this._cachedDiffBuilder; }, - _enableSelectionObserver: function(loggedIn, isAttached) { + _enableSelectionObserver(loggedIn, isAttached) { if (loggedIn && isAttached) { this.listen(document, 'selectionchange', '_handleSelectionChange'); } else { @@ -50,11 +50,11 @@ } }, - isRangeSelected: function() { + isRangeSelected() { return !!this.$$('gr-selection-action-box'); }, - _handleSelectionChange: function() { + _handleSelectionChange() { // Can't use up or down events to handle selection started and/or ended in // in comment threads or outside of diff. // Debounce removeActionBox to give it a chance to react to click/tap. @@ -62,31 +62,31 @@ this.debounce('selectionChange', this._handleSelection, 200); }, - _handleCommentMouseOver: function(e) { - var comment = e.detail.comment; + _handleCommentMouseOver(e) { + const comment = e.detail.comment; if (!comment.range) { return; } - var lineEl = this.diffBuilder.getLineElByChild(e.target); - var side = this.diffBuilder.getSideByLineEl(lineEl); - var index = this._indexOfComment(side, comment); + const lineEl = this.diffBuilder.getLineElByChild(e.target); + const side = this.diffBuilder.getSideByLineEl(lineEl); + const index = this._indexOfComment(side, comment); if (index !== undefined) { this.set(['comments', side, index, '__hovering'], true); } }, - _handleCommentMouseOut: function(e) { - var comment = e.detail.comment; + _handleCommentMouseOut(e) { + const comment = e.detail.comment; if (!comment.range) { return; } - var lineEl = this.diffBuilder.getLineElByChild(e.target); - var side = this.diffBuilder.getSideByLineEl(lineEl); - var index = this._indexOfComment(side, comment); + const lineEl = this.diffBuilder.getLineElByChild(e.target); + const side = this.diffBuilder.getSideByLineEl(lineEl); + const index = this._indexOfComment(side, comment); if (index !== undefined) { this.set(['comments', side, index, '__hovering'], false); } }, - _indexOfComment: function(side, comment) { - var idProp = comment.id ? 'id' : '__draftID'; - for (var i = 0; i < this.comments[side].length; i++) { + _indexOfComment(side, comment) { + const idProp = comment.id ? 'id' : '__draftID'; + for (let i = 0; i < this.comments[side].length; i++) { if (comment[idProp] && this.comments[side][i][idProp] === comment[idProp]) { return i; @@ -94,8 +94,49 @@ } }, - _normalizeRange: function(domRange) { - var range = GrRangeNormalizer.normalize(domRange); + /** + * Get current normalized selection. + * Merges multiple ranges, accounts for triple click, accounts for + * syntax highligh, convert native DOM Range objects to Gerrit concepts + * (line, side, etc). + * @return {{ + * start: { + * node: Node, + * side: string, + * line: Number, + * column: Number + * }, + * end: { + * node: Node, + * side: string, + * line: Number, + * column: Number + * } + * }} + */ + _getNormalizedRange() { + const selection = window.getSelection(); + const rangeCount = selection.rangeCount; + if (rangeCount === 0) { + return null; + } else if (rangeCount === 1) { + return this._normalizeRange(selection.getRangeAt(0)); + } else { + const startRange = this._normalizeRange(selection.getRangeAt(0)); + const endRange = this._normalizeRange( + selection.getRangeAt(rangeCount - 1)); + return { + start: startRange.start, + end: endRange.end, + }; + } + }, + + /** + * Normalize a specific DOM Range. + */ + _normalizeRange(domRange) { + const range = GrRangeNormalizer.normalize(domRange); return this._fixTripleClickSelection({ start: this._normalizeSelectionSide( range.startContainer, range.startOffset), @@ -115,19 +156,19 @@ * @param {!Range} domRange DOM Range object * @return {!Object} fixed normalized range */ - _fixTripleClickSelection: function(range, domRange) { + _fixTripleClickSelection(range, domRange) { if (!range.start) { // Selection outside of current diff. return range; } - var start = range.start; - var end = range.end; - var endsAtOtherSideLineNum = + const start = range.start; + const end = range.end; + const endsAtOtherSideLineNum = domRange.endOffset === 0 && domRange.endContainer.nodeName === 'TD' && (domRange.endContainer.classList.contains('left') || domRange.endContainer.classList.contains('right')); - var endsOnOtherSideStart = endsAtOtherSideLineNum || + const endsOnOtherSideStart = endsAtOtherSideLineNum || end && end.column === 0 && end.line === start.line && @@ -160,33 +201,33 @@ * column: Number * }} */ - _normalizeSelectionSide: function(node, offset) { - var column; + _normalizeSelectionSide(node, offset) { + let column; if (!this.contains(node)) { return; } - var lineEl = this.diffBuilder.getLineElByChild(node); + const lineEl = this.diffBuilder.getLineElByChild(node); if (!lineEl) { return; } - var side = this.diffBuilder.getSideByLineEl(lineEl); + const side = this.diffBuilder.getSideByLineEl(lineEl); if (!side) { return; } - var line = this.diffBuilder.getLineNumberByChild(lineEl); + const line = this.diffBuilder.getLineNumberByChild(lineEl); if (!line) { return; } - var contentText = this.diffBuilder.getContentByLineEl(lineEl); + const contentText = this.diffBuilder.getContentByLineEl(lineEl); if (!contentText) { return; } - var contentTd = contentText.parentElement; + const contentTd = contentText.parentElement; if (!contentTd.contains(node)) { node = contentText; column = 0; } else { - var thread = contentTd.querySelector('gr-diff-comment-thread'); + const thread = contentTd.querySelector('gr-diff-comment-thread'); if (thread && thread.contains(node)) { column = this._getLength(contentText); node = contentText; @@ -196,28 +237,24 @@ } return { - node: node, - side: side, - line: line, - column: column, + node, + side, + line, + column, }; }, - _handleSelection: function() { - var selection = window.getSelection(); - if (selection.rangeCount != 1) { + _handleSelection() { + const normalizedRange = this._getNormalizedRange(); + if (!normalizedRange) { return; } - var range = selection.getRangeAt(0); - if (range.collapsed) { - return; - } - var normalizedRange = this._normalizeRange(range); - var start = normalizedRange.start; + const domRange = window.getSelection().getRangeAt(0); + const start = normalizedRange.start; if (!start) { return; } - var end = normalizedRange.end; + const end = normalizedRange.end; if (!end) { return; } @@ -229,8 +266,9 @@ // TODO (viktard): Drop empty first and last lines from selection. - var actionBox = document.createElement('gr-selection-action-box'); - Polymer.dom(this.root).appendChild(actionBox); + const actionBox = document.createElement('gr-selection-action-box'); + const root = Polymer.dom(this.root); + root.insertBefore(actionBox, root.firstElementChild); actionBox.range = { startLine: start.line, startChar: start.column, @@ -239,7 +277,7 @@ }; actionBox.side = start.side; if (start.line === end.line) { - actionBox.placeAbove(range); + actionBox.placeAbove(domRange); } else if (start.node instanceof Text) { actionBox.placeAbove(start.node.splitText(start.column)); start.node.parentElement.normalize(); // Undo splitText from above. @@ -251,22 +289,22 @@ } }, - _createComment: function(e) { + _createComment(e) { this._removeActionBox(); }, - _removeActionBoxDebounced: function() { + _removeActionBoxDebounced() { this.debounce('removeActionBox', this._removeActionBox, 10); }, - _removeActionBox: function() { - var actionBox = this.$$('gr-selection-action-box'); + _removeActionBox() { + const actionBox = this.$$('gr-selection-action-box'); if (actionBox) { Polymer.dom(this.root).removeChild(actionBox); } }, - _convertOffsetToColumn: function(el, offset) { + _convertOffsetToColumn(el, offset) { if (el instanceof Element && el.classList.contains('content')) { return offset; } @@ -290,16 +328,16 @@ * @param {function(Node):boolean} callback * @param {Object=} opt_flags If flags.left is true, traverse left. */ - _traverseContentSiblings: function(startNode, callback, opt_flags) { - var travelLeft = opt_flags && opt_flags.left; - var node = startNode; + _traverseContentSiblings(startNode, callback, opt_flags) { + const travelLeft = opt_flags && opt_flags.left; + let node = startNode; while (node) { if (node instanceof Element && node.tagName !== 'HL' && node.tagName !== 'SPAN') { break; } - var nextNode = travelLeft ? node.previousSibling : node.nextSibling; + const nextNode = travelLeft ? node.previousSibling : node.nextSibling; if (callback(node)) { break; } @@ -314,7 +352,7 @@ * @param {!Node} node * @return {number} */ - _getLength: function(node) { + _getLength(node) { if (node instanceof Element && node.classList.contains('content')) { return this._getLength(node.querySelector('.contentText')); } else {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html index 827437c..b63b9a4 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-diff-highlight.html"> <script>void(0);</script> @@ -124,39 +123,39 @@ </test-fixture> <script> - suite('gr-diff-highlight', function() { - var element; - var sandbox; + suite('gr-diff-highlight', () => { + let element; + let sandbox; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); element = fixture('basic')[1]; }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - suite('selectionchange event handling', function() { - var emulateSelection = function() { + suite('selectionchange event handling', () => { + const emulateSelection = function() { document.dispatchEvent(new CustomEvent('selectionchange')); element.flushDebouncer('selectionChange'); element.flushDebouncer('removeActionBox'); }; - setup(function() { + setup(() => { sandbox.stub(element, '_handleSelection'); sandbox.stub(element, '_removeActionBox'); }); - test('enabled if logged in', function() { + test('enabled if logged in', () => { element.loggedIn = true; emulateSelection(); assert.isTrue(element._handleSelection.called); assert.isTrue(element._removeActionBox.called); }); - test('ignored if logged out', function() { + test('ignored if logged out', () => { element.loggedIn = false; emulateSelection(); assert.isFalse(element._handleSelection.called); @@ -164,10 +163,10 @@ }); }); - suite('comment events', function() { - var builder; + suite('comment events', () => { + let builder; - setup(function() { + setup(() => { builder = { getContentsByLineRange: sandbox.stub().returns([]), getLineElByChild: sandbox.stub().returns({}), @@ -176,25 +175,25 @@ element._cachedDiffBuilder = builder; }); - test('comment-mouse-over from line comments is ignored', function() { + test('comment-mouse-over from line comments is ignored', () => { sandbox.stub(element, 'set'); element.fire('comment-mouse-over', {comment: {}}); assert.isFalse(element.set.called); }); - test('comment-mouse-over from ranged comment causes set', function() { + test('comment-mouse-over from ranged comment causes set', () => { sandbox.stub(element, 'set'); sandbox.stub(element, '_indexOfComment').returns(0); element.fire('comment-mouse-over', {comment: {range: {}}}); assert.isTrue(element.set.called); }); - test('comment-mouse-out from line comments is ignored', function() { + test('comment-mouse-out from line comments is ignored', () => { element.fire('comment-mouse-over', {comment: {}}); assert.isFalse(builder.getContentsByLineRange.called); }); - test('on create-comment action box is removed', function() { + test('on create-comment action box is removed', () => { sandbox.stub(element, '_removeActionBox'); element.fire('create-comment', { comment: { @@ -205,21 +204,21 @@ }); }); - suite('selection', function() { - var diff; - var builder; - var contentStubs; + suite('selection', () => { + let diff; + let builder; + let contentStubs; - var stubContent = function(line, side, opt_child) { - var contentTd = diff.querySelector( - '.' + side + '.lineNum[data-value="' + line + '"] ~ .content'); - var contentText = contentTd.querySelector('.contentText'); - var lineEl = diff.querySelector( - '.' + side + '.lineNum[data-value="' + line + '"]'); + const stubContent = (line, side, opt_child) => { + const contentTd = diff.querySelector( + `.${side}.lineNum[data-value="${line}"] ~ .content`); + const contentText = contentTd.querySelector('.contentText'); + const lineEl = diff.querySelector( + `.${side}.lineNum[data-value="${line}"]`); contentStubs.push({ - lineEl: lineEl, - contentTd: contentTd, - contentText: contentText, + lineEl, + contentTd, + contentText, }); builder.getContentByLineEl.withArgs(lineEl).returns(contentText); builder.getLineNumberByChild.withArgs(lineEl).returns(line); @@ -228,34 +227,29 @@ return contentText; }; - var emulateSelection = function( - startNode, startOffset, endNode, endOffset) { - var selection = window.getSelection(); - var range = document.createRange(); + const emulateSelection = (startNode, startOffset, endNode, endOffset) => { + const selection = window.getSelection(); + const range = document.createRange(); range.setStart(startNode, startOffset); range.setEnd(endNode, endOffset); selection.addRange(range); element._handleSelection(); }; - var getActionRange = function() { - return Polymer.dom(element.root).querySelector( - 'gr-selection-action-box').range; - }; + const getActionRange = () => + Polymer.dom(element.root).querySelector( + 'gr-selection-action-box').range; - var getActionSide = function() { - return Polymer.dom(element.root).querySelector( - 'gr-selection-action-box').side; - }; + const getActionSide = () => + Polymer.dom(element.root).querySelector( + 'gr-selection-action-box').side; - var getLineElByChild = function(node) { - var stubs = contentStubs.find(function(stub) { - return stub.contentTd.contains(node); - }); + const getLineElByChild = node => { + const stubs = contentStubs.find(stub => stub.contentTd.contains(node)); return stubs && stubs.lineEl; }; - setup(function() { + setup(() => { contentStubs = []; stub('gr-selection-action-box', { placeAbove: sandbox.stub(), @@ -264,20 +258,20 @@ builder = { getContentByLine: sandbox.stub(), getContentByLineEl: sandbox.stub(), - getLineElByChild: getLineElByChild, + getLineElByChild, getLineNumberByChild: sandbox.stub(), getSideByLineEl: sandbox.stub(), }; element._cachedDiffBuilder = builder; }); - teardown(function() { + teardown(() => { contentStubs = null; window.getSelection().removeAllRanges(); }); - test('single line', function() { - var content = stubContent(138, 'left'); + test('single line', () => { + const content = stubContent(138, 'left'); emulateSelection(content.firstChild, 5, content.firstChild, 12); assert.isTrue(element.isRangeSelected()); assert.deepEqual(getActionRange(), { @@ -289,9 +283,9 @@ assert.equal(getActionSide(), 'left'); }); - test('multiline', function() { - var startContent = stubContent(119, 'right'); - var endContent = stubContent(120, 'right'); + test('multiline', () => { + const startContent = stubContent(119, 'right'); + const endContent = stubContent(120, 'right'); emulateSelection( startContent.firstChild, 10, endContent.lastChild, 7); assert.isTrue(element.isRangeSelected()); @@ -304,9 +298,40 @@ assert.equal(getActionSide(), 'right'); }); - test('multiline grow end highlight over tabs', function() { - var startContent = stubContent(119, 'right'); - var endContent = stubContent(120, 'right'); + test('multiple ranges aka firefox implementation', () => { + const startContent = stubContent(119, 'right'); + const endContent = stubContent(120, 'right'); + + const startRange = document.createRange(); + startRange.setStart(startContent.firstChild, 10); + startRange.setEnd(startContent.firstChild, 11); + + const endRange = document.createRange(); + endRange.setStart(endContent.lastChild, 6); + endRange.setEnd(endContent.lastChild, 7); + + const getRangeAtStub = sandbox.stub(); + getRangeAtStub + .onFirstCall().returns(startRange) + .onSecondCall().returns(endRange); + sandbox.stub(window, 'getSelection').returns({ + rangeCount: 2, + getRangeAt: getRangeAtStub, + removeAllRanges: sandbox.stub(), + }); + element._handleSelection(); + assert.isTrue(element.isRangeSelected()); + assert.deepEqual(getActionRange(), { + startLine: 119, + startChar: 10, + endLine: 120, + endChar: 36, + }); + }); + + test('multiline grow end highlight over tabs', () => { + const startContent = stubContent(119, 'right'); + const endContent = stubContent(120, 'right'); emulateSelection(startContent.firstChild, 10, endContent.firstChild, 2); assert.isTrue(element.isRangeSelected()); assert.deepEqual(getActionRange(), { @@ -318,16 +343,16 @@ assert.equal(getActionSide(), 'right'); }); - test('collapsed', function() { - var content = stubContent(138, 'left'); + test('collapsed', () => { + const content = stubContent(138, 'left'); emulateSelection(content.firstChild, 5, content.firstChild, 5); assert.isOk(window.getSelection().getRangeAt(0).startContainer); assert.isFalse(element.isRangeSelected()); }); - test('starts inside hl', function() { - var content = stubContent(140, 'left'); - var hl = content.querySelector('.foo'); + test('starts inside hl', () => { + const content = stubContent(140, 'left'); + const hl = content.querySelector('.foo'); emulateSelection(hl.firstChild, 2, hl.nextSibling, 7); assert.isTrue(element.isRangeSelected()); assert.deepEqual(getActionRange(), { @@ -339,9 +364,9 @@ assert.equal(getActionSide(), 'left'); }); - test('ends inside hl', function() { - var content = stubContent(140, 'left'); - var hl = content.querySelector('.bar'); + test('ends inside hl', () => { + const content = stubContent(140, 'left'); + const hl = content.querySelector('.bar'); emulateSelection(hl.previousSibling, 2, hl.firstChild, 3); assert.isTrue(element.isRangeSelected()); assert.deepEqual(getActionRange(), { @@ -352,9 +377,9 @@ }); }); - test('multiple hl', function() { - var content = stubContent(140, 'left'); - var hl = content.querySelectorAll('hl')[4]; + test('multiple hl', () => { + const content = stubContent(140, 'left'); + const hl = content.querySelectorAll('hl')[4]; emulateSelection(content.firstChild, 2, hl.firstChild, 2); assert.isTrue(element.isRangeSelected()); assert.deepEqual(getActionRange(), { @@ -366,34 +391,34 @@ assert.equal(getActionSide(), 'left'); }); - test('starts outside of diff', function() { - var contentText = stubContent(140, 'left'); - var contentTd = contentText.parentElement; + test('starts outside of diff', () => { + const contentText = stubContent(140, 'left'); + const contentTd = contentText.parentElement; emulateSelection(contentTd.previousElementSibling, 0, contentText.firstChild, 2); assert.isFalse(element.isRangeSelected()); }); - test('ends outside of diff', function() { - var content = stubContent(140, 'left'); + test('ends outside of diff', () => { + const content = stubContent(140, 'left'); emulateSelection(content.nextElementSibling.firstChild, 2, content.firstChild, 2); assert.isFalse(element.isRangeSelected()); }); - test('starts and ends on different sides', function() { - var startContent = stubContent(140, 'left'); - var endContent = stubContent(130, 'right'); + test('starts and ends on different sides', () => { + const startContent = stubContent(140, 'left'); + const endContent = stubContent(130, 'right'); emulateSelection(startContent.firstChild, 2, endContent.firstChild, 2); assert.isFalse(element.isRangeSelected()); }); - test('starts in comment thread element', function() { - var startContent = stubContent(140, 'left'); - var comment = startContent.parentElement.querySelector( + test('starts in comment thread element', () => { + const startContent = stubContent(140, 'left'); + const comment = startContent.parentElement.querySelector( 'gr-diff-comment-thread'); - var endContent = stubContent(141, 'left'); + const endContent = stubContent(141, 'left'); emulateSelection(comment.firstChild, 2, endContent.firstChild, 4); assert.isTrue(element.isRangeSelected()); assert.deepEqual(getActionRange(), { @@ -405,9 +430,9 @@ assert.equal(getActionSide(), 'left'); }); - test('ends in comment thread element', function() { - var content = stubContent(140, 'left'); - var comment = content.parentElement.querySelector( + test('ends in comment thread element', () => { + const content = stubContent(140, 'left'); + const comment = content.parentElement.querySelector( 'gr-diff-comment-thread'); emulateSelection(content.firstChild, 4, comment.firstChild, 1); assert.isTrue(element.isRangeSelected()); @@ -420,27 +445,27 @@ assert.equal(getActionSide(), 'left'); }); - test('starts in context element', function() { - var contextControl = + test('starts in context element', () => { + const contextControl = diff.querySelector('.contextControl').querySelector('gr-button'); - var content = stubContent(146, 'right'); + const content = stubContent(146, 'right'); emulateSelection(contextControl, 0, content.firstChild, 7); // TODO (viktard): Select nearest line. assert.isFalse(element.isRangeSelected()); }); - test('ends in context element', function() { - var contextControl = + test('ends in context element', () => { + const contextControl = diff.querySelector('.contextControl').querySelector('gr-button'); - var content = stubContent(141, 'left'); + const content = stubContent(141, 'left'); emulateSelection(content.firstChild, 2, contextControl, 1); // TODO (viktard): Select nearest line. assert.isFalse(element.isRangeSelected()); }); - test('selection containing context element', function() { - var startContent = stubContent(130, 'right'); - var endContent = stubContent(146, 'right'); + test('selection containing context element', () => { + const startContent = stubContent(130, 'right'); + const endContent = stubContent(146, 'right'); emulateSelection(startContent.firstChild, 3, endContent.firstChild, 14); assert.isTrue(element.isRangeSelected()); assert.deepEqual(getActionRange(), { @@ -452,8 +477,8 @@ assert.equal(getActionSide(), 'right'); }); - test('ends at a tab', function() { - var content = stubContent(140, 'left'); + test('ends at a tab', () => { + const content = stubContent(140, 'left'); emulateSelection( content.firstChild, 1, content.querySelector('span'), 0); assert.isTrue(element.isRangeSelected()); @@ -466,8 +491,8 @@ assert.equal(getActionSide(), 'left'); }); - test('starts at a tab', function() { - var content = stubContent(140, 'left'); + test('starts at a tab', () => { + const content = stubContent(140, 'left'); emulateSelection( content.querySelectorAll('hl')[3], 0, content.querySelectorAll('span')[1].nextSibling, 1); @@ -481,42 +506,43 @@ assert.equal(getActionSide(), 'left'); }); - test('properly accounts for syntax highlighting', function() { - var content = stubContent(140, 'left'); - var spy = sinon.spy(element, '_normalizeRange'); + test('properly accounts for syntax highlighting', () => { + const content = stubContent(140, 'left'); + const spy = sinon.spy(element, '_normalizeRange'); emulateSelection( content.querySelectorAll('hl')[3], 0, content.querySelectorAll('span')[1], 0); - var spyCall = spy.getCall(0); - var range = window.getSelection().getRangeAt(0); + const spyCall = spy.getCall(0); + const range = window.getSelection().getRangeAt(0); assert.notDeepEqual(spyCall.returnValue, range); }); - test('GrRangeNormalizer._getTextOffset computes text offset', function() { - var content = stubContent(140, 'left'); - var child = content.lastChild.lastChild; - var result = GrRangeNormalizer._getTextOffset(content, child); + test('GrRangeNormalizer._getTextOffset computes text offset', () => { + let content = stubContent(140, 'left'); + let child = content.lastChild.lastChild; + let result = GrRangeNormalizer._getTextOffset(content, child); assert.equal(result, 75); content = stubContent(146, 'right'); child = content.lastChild; result = GrRangeNormalizer._getTextOffset(content, child); assert.equal(result, 0); }); + // TODO (viktard): Selection starts in line number. // TODO (viktard): Empty lines in selection start. // TODO (viktard): Empty lines in selection end. // TODO (viktard): Only empty lines selected. // TODO (viktard): Unified mode. - suite('triple click', function() { - test('_fixTripleClickSelection', function() { - var fakeRange = { + suite('triple click', () => { + test('_fixTripleClickSelection', () => { + const fakeRange = { startContainer: '', startOffset: '', endContainer: '', - endOffset: '' + endOffset: '', }; - var fixedRange = {}; + const fixedRange = {}; sandbox.stub(GrRangeNormalizer, 'normalize').returns(fakeRange); sandbox.stub(element, '_normalizeSelectionSide'); sandbox.stub(element, '_fixTripleClickSelection').returns(fixedRange); @@ -524,9 +550,9 @@ assert.isTrue(element._fixTripleClickSelection.called); }); - test('left pane', function() { - var startNode = stubContent(138, 'left'); - var endNode = + test('left pane', () => { + const startNode = stubContent(138, 'left'); + const endNode = stubContent(119, 'right').parentElement.previousElementSibling; builder.getLineNumberByChild.withArgs(endNode).returns(119); emulateSelection(startNode, 0, endNode, 0); @@ -538,9 +564,9 @@ }); }); - test('right pane', function() { - var startNode = stubContent(119, 'right'); - var endNode = + test('right pane', () => { + const startNode = stubContent(119, 'right'); + const endNode = stubContent(140, 'left').parentElement.previousElementSibling; emulateSelection(startNode, 0, endNode, 0); assert.deepEqual(getActionRange(), {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js index e870169..0b0131e 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-range-normalizer.js
@@ -18,9 +18,9 @@ if (window.GrRangeNormalizer) { return; } // Astral code point as per https://mathiasbynens.be/notes/javascript-unicode - var REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/; + const REGEX_ASTRAL_SYMBOL = /[\uD800-\uDBFF][\uDC00-\uDFFF]/; - var GrRangeNormalizer = { + const GrRangeNormalizer = { /** * Remap DOM range to whole lines of a diff if necessary. If the start or * end containers are DOM elements that are singular pieces of syntax @@ -31,23 +31,23 @@ * @return {Object} A modified version of the range that correctly accounts * for syntax highlighting. */ - normalize: function(range) { - var startContainer = this._getContentTextParent(range.startContainer); - var startOffset = range.startOffset + this._getTextOffset(startContainer, - range.startContainer); - var endContainer = this._getContentTextParent(range.endContainer); - var endOffset = range.endOffset + this._getTextOffset(endContainer, + normalize(range) { + const startContainer = this._getContentTextParent(range.startContainer); + const startOffset = range.startOffset + + this._getTextOffset(startContainer, range.startContainer); + const endContainer = this._getContentTextParent(range.endContainer); + const endOffset = range.endOffset + this._getTextOffset(endContainer, range.endContainer); return { - startContainer: startContainer, - startOffset: startOffset, - endContainer: endContainer, - endOffset: endOffset, + startContainer, + startOffset, + endContainer, + endOffset, }; }, - _getContentTextParent: function(target) { - var element = target; + _getContentTextParent(target) { + let element = target; if (element.nodeName === '#text') { element = element.parentElement; } @@ -69,18 +69,18 @@ * @param {!Element} child The child element being searched for. * @return {number} */ - _getTextOffset: function(node, child) { - var count = 0; - var stack = [node]; + _getTextOffset(node, child) { + let count = 0; + let stack = [node]; while (stack.length) { - var n = stack.pop(); + const n = stack.pop(); if (n === child) { break; } if (n.childNodes && n.childNodes.length !== 0) { - var arr = []; - for (var i = 0; i < n.childNodes.length; i++) { - arr.push(n.childNodes[i]); + const arr = []; + for (const childNode of n.childNodes) { + arr.push(childNode); } arr.reverse(); stack = stack.concat(arr); @@ -97,7 +97,7 @@ * @param {Text} A text node. * @return {Number} The length of the text. */ - _getLength: function(node) { + _getLength(node) { return node.textContent.replace(REGEX_ASTRAL_SYMBOL, '_').length; }, };
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html index fe57c43..99a7054 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.html
@@ -17,15 +17,18 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/iron-input/iron-input.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../../shared/gr-storage/gr-storage.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-diff-preferences"> <template> - <style> + <style include="shared-styles"> :host { display: block; } - :host[disabled] { + :host([disabled]) { opacity: .5; pointer-events: none; } @@ -70,71 +73,79 @@ color: #888; } </style> - <div class="header"> - Diff View Preferences - </div> - <div class="mainContainer"> - <div class="pref"> - <label for="contextSelect">Context</label> - <select id="contextSelect" on-change="_handleContextSelectChange"> - <option value="3">3 lines</option> - <option value="10">10 lines</option> - <option value="25">25 lines</option> - <option value="50">50 lines</option> - <option value="75">75 lines</option> - <option value="100">100 lines</option> - <option value="-1">Whole file</option> - </select> + + <gr-overlay id="prefsOverlay" with-backdrop> + <div class="header"> + Diff View Preferences </div> - <div class="pref"> - <label for="lineWrappingInput">Fit to screen</label> - <input - is="iron-input" - type="checkbox" - id="lineWrappingInput" - on-tap="_handlelineWrappingTap"> + <div class="mainContainer"> + <div class="pref"> + <label for="contextSelect">Context</label> + <select id="contextSelect" on-change="_handleContextSelectChange"> + <option value="3">3 lines</option> + <option value="10">10 lines</option> + <option value="25">25 lines</option> + <option value="50">50 lines</option> + <option value="75">75 lines</option> + <option value="100">100 lines</option> + <option value="-1">Whole file</option> + </select> + </div> + <div class="pref"> + <label for="lineWrappingInput">Fit to screen</label> + <input + is="iron-input" + type="checkbox" + id="lineWrappingInput" + on-tap="_handlelineWrappingTap"> + </div> + <div class="pref" id="columnsPref" + hidden$="[[_newPrefs.line_wrapping]]"> + <label for="columnsInput">Diff width</label> + <input is="iron-input" type="number" id="columnsInput" + prevent-invalid-input + allowed-pattern="[0-9]" + bind-value="{{_newPrefs.line_length}}"> + </div> + <div class="pref"> + <label for="tabSizeInput">Tab width</label> + <input is="iron-input" type="number" id="tabSizeInput" + prevent-invalid-input + allowed-pattern="[0-9]" + bind-value="{{_newPrefs.tab_size}}"> + </div> + <div class="pref" hidden$="[[!_newPrefs.font_size]]"> + <label for="fontSizeInput">Font size</label> + <input is="iron-input" type="number" id="fontSizeInput" + prevent-invalid-input + allowed-pattern="[0-9]" + bind-value="{{_newPrefs.font_size}}"> + </div> + <div class="pref"> + <label for="showTabsInput">Show tabs</label> + <input is="iron-input" type="checkbox" id="showTabsInput" + on-tap="_handleShowTabsTap"> + </div> + <div class="pref"> + <label for="showTrailingWhitespaceInput"> + Show trailing whitespace</label> + <input is="iron-input" type="checkbox" + id="showTrailingWhitespaceInput" + on-tap="_handleShowTrailingWhitespaceTap"> + </div> + <div class="pref"> + <label for="syntaxHighlightInput">Syntax highlighting</label> + <input is="iron-input" type="checkbox" id="syntaxHighlightInput" + on-tap="_handleSyntaxHighlightTap"> + </div> </div> - <div class="pref" id="columnsPref" hidden$="[[_newPrefs.line_wrapping]]"> - <label for="columnsInput">Diff width</label> - <input is="iron-input" type="number" id="columnsInput" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{_newPrefs.line_length}}"> + <div class="actions"> + <gr-button id="saveButton" primary on-tap="_handleSave">Save</gr-button> + <gr-button id="cancelButton" on-tap="_handleCancel">Cancel</gr-button> </div> - <div class="pref"> - <label for="tabSizeInput">Tab width</label> - <input is="iron-input" type="number" id="tabSizeInput" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{_newPrefs.tab_size}}"> - </div> - <div class="pref" hidden$="[[!_newPrefs.font_size]]"> - <label for="fontSizeInput">Font size</label> - <input is="iron-input" type="number" id="fontSizeInput" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{_newPrefs.font_size}}"> - </div> - <div class="pref"> - <label for="showTabsInput">Show tabs</label> - <input is="iron-input" type="checkbox" id="showTabsInput" - on-tap="_handleShowTabsTap"> - </div> - <div class="pref"> - <label for="showTrailingWhitespaceInput">Show trailing whitespace</label> - <input is="iron-input" type="checkbox" id="showTrailingWhitespaceInput" - on-tap="_handleShowTrailingWhitespaceTap"> - </div> - <div class="pref"> - <label for="syntaxHighlightInput">Syntax highlighting</label> - <input is="iron-input" type="checkbox" id="syntaxHighlightInput" - on-tap="_handleSyntaxHighlightTap"> - </div> - </div> - <div class="actions"> - <gr-button id="saveButton" primary on-tap="_handleSave">Save</gr-button> - <gr-button id="cancelButton" on-tap="_handleCancel">Cancel</gr-button> - </div> + </overlay> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + <gr-storage id="storage"></gr-storage> </template> <script src="gr-diff-preferences.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js index fd2a6f5..20833d0 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences.js
@@ -17,18 +17,6 @@ Polymer({ is: 'gr-diff-preferences', - /** - * Fired when the user presses the save button. - * - * @event save - */ - - /** - * Fired when the user presses the cancel button. - * - * @event cancel - */ - properties: { prefs: { type: Object, @@ -53,20 +41,19 @@ '_localPrefsChanged(localPrefs.*)', ], - getFocusStops: function() { + getFocusStops() { return { start: this.$.contextSelect, end: this.$.cancelButton, }; }, - resetFocus: function() { + resetFocus() { this.$.contextSelect.focus(); }, - _prefsChanged: function(changeRecord) { - var prefs = changeRecord.base; - // TODO(andybons): This is not supported in IE. Implement a polyfill. + _prefsChanged(changeRecord) { + const prefs = changeRecord.base; // NOTE: Object.assign is NOT automatically a deep copy. If prefs adds // an object as a value, it must be marked enumerable. this._newPrefs = Object.assign({}, prefs); @@ -77,43 +64,71 @@ this.$.syntaxHighlightInput.checked = prefs.syntax_highlighting; }, - _localPrefsChanged: function(changeRecord) { - var localPrefs = changeRecord.base || {}; - // TODO(viktard): This is not supported in IE. Implement a polyfill. + _localPrefsChanged(changeRecord) { + const localPrefs = changeRecord.base || {}; this._newLocalPrefs = Object.assign({}, localPrefs); }, - _handleContextSelectChange: function(e) { - var selectEl = Polymer.dom(e).rootTarget; + _handleContextSelectChange(e) { + const selectEl = Polymer.dom(e).rootTarget; this.set('_newPrefs.context', parseInt(selectEl.value, 10)); }, - _handleShowTabsTap: function(e) { + _handleShowTabsTap(e) { this.set('_newPrefs.show_tabs', Polymer.dom(e).rootTarget.checked); }, - _handleShowTrailingWhitespaceTap: function(e) { + _handleShowTrailingWhitespaceTap(e) { this.set('_newPrefs.show_whitespace_errors', Polymer.dom(e).rootTarget.checked); }, - _handleSyntaxHighlightTap: function(e) { + _handleSyntaxHighlightTap(e) { this.set('_newPrefs.syntax_highlighting', Polymer.dom(e).rootTarget.checked); }, - _handlelineWrappingTap: function(e) { + _handlelineWrappingTap(e) { this.set('_newPrefs.line_wrapping', Polymer.dom(e).rootTarget.checked); }, - _handleSave: function() { + _handleSave(e) { + e.stopPropagation(); this.prefs = this._newPrefs; this.localPrefs = this._newLocalPrefs; - this.fire('save', null, {bubbles: false}); + const el = Polymer.dom(e).rootTarget; + el.disabled = true; + this.$.storage.savePreferences(this._localPrefs); + this._saveDiffPreferences().then(response => { + el.disabled = false; + if (!response.ok) { return response; } + + this.$.prefsOverlay.close(); + }).catch(err => { + el.disabled = false; + }); }, - _handleCancel: function() { - this.fire('cancel', null, {bubbles: false}); + _handleCancel(e) { + e.stopPropagation(); + this.$.prefsOverlay.close(); + }, + + _handlePrefsTap(e) { + e.preventDefault(); + this._openPrefs(); + }, + + open() { + this.$.prefsOverlay.open().then(() => { + const focusStops = this.getFocusStops(); + this.$.prefsOverlay.setFocusStops(focusStops); + this.resetFocus(); + }); + }, + + _saveDiffPreferences() { + return this.$.restAPI.saveDiffPreferences(this.prefs); }, }); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html index 06f617a..f06cd3a 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences/gr-diff-preferences_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-diff-preferences.html"> <script>void(0);</script> @@ -33,14 +32,20 @@ </test-fixture> <script> - suite('gr-diff-preferences tests', function() { - var element; + suite('gr-diff-preferences tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { + sandbox = sinon.sandbox.create(); element = fixture('basic'); }); - test('model changes', function() { + teardown(() => { + sandbox.restore(); + }); + + test('model changes', () => { element.prefs = { context: 10, font_size: 12, @@ -72,7 +77,7 @@ assert.isFalse(element._newPrefs.syntax_highlighting); }); - test('clicking fit to screen hides line length input', function() { + test('clicking fit to screen hides line length input', () => { element.prefs = {line_wrapping: false}; assert.isFalse(element.$.columnsPref.hidden); @@ -84,26 +89,33 @@ assert.isFalse(element.$.columnsPref.hidden); }); - test('clicking save button calls _handleSave function', function() { - var savePrefs = sinon.stub(element, '_handleSave'); + test('clicking save button calls _handleSave function', () => { + const savePrefs = sinon.stub(element, '_handleSave'); MockInteractions.tap(element.$.saveButton); flushAsynchronousOperations(); assert(savePrefs.calledOnce); savePrefs.restore(); }); - test('events', function(done) { - var savePromise = new Promise(function(resolve) { - element.addEventListener('save', function() { resolve(); }); - }); - var cancelPromise = new Promise(function(resolve) { - element.addEventListener('cancel', function() { resolve(); }); - }); - Promise.all([savePromise, cancelPromise]).then(function() { - done(); - }); + test('save button', () => { + element.prefs = { + font_size: '11', + }; + element._newPrefs = { + font_size: '12', + }; + const saveStub = sandbox.stub(element.$.restAPI, 'saveDiffPreferences', + () => { return Promise.resolve(); }); + MockInteractions.tap(element.$$('gr-button[primary]')); + assert.deepEqual(element.prefs, element._newPrefs); + assert.deepEqual(saveStub.lastCall.args[0], element._newPrefs); + }); + + test('cancel button', () => { + const closeStub = sandbox.stub(element.$.prefsOverlay, 'close'); MockInteractions.tap(element.$$('gr-button:not([primary])')); + assert.isTrue(closeStub.called); }); }); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js index 95ff5b7..07b8429 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -14,20 +14,20 @@ (function() { 'use strict'; - var WHOLE_FILE = -1; + const WHOLE_FILE = -1; - var DiffSide = { + const DiffSide = { LEFT: 'left', RIGHT: 'right', }; - var DiffGroupType = { + const DiffGroupType = { ADDED: 'b', BOTH: 'ab', REMOVED: 'a', }; - var DiffHighlights = { + const DiffHighlights = { ADDED: 'edit_b', REMOVED: 'edit_a', }; @@ -36,11 +36,11 @@ * 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 70 is chosen so that it is larger than the default + * 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. */ - var MAX_GROUP_SIZE = 70; + const MAX_GROUP_SIZE = 120; Polymer({ is: 'gr-diff-processor', @@ -66,7 +66,7 @@ */ keyLocations: { type: Object, - value: function() { return {left: {}, right: {}}; }, + value() { return {left: {}, right: {}}; }, }, /** @@ -81,18 +81,18 @@ _isScrolling: Boolean, }, - attached: function() { + attached() { this.listen(window, 'scroll', '_handleWindowScroll'); }, - detached: function() { + detached() { this.cancel(); this.unlisten(window, 'scroll', '_handleWindowScroll'); }, - _handleWindowScroll: function() { + _handleWindowScroll() { this._isScrolling = true; - this.debounce('resetIsScrolling', function() { + this.debounce('resetIsScrolling', () => { this._isScrolling = false; }, 50); }, @@ -103,21 +103,23 @@ * @return {Promise} A promise that resolves when the diff is completely * processed. */ - process: function(content) { - return new Promise(function(resolve) { - this.groups = []; - this.push('groups', this._makeFileComments()); + process(content, isImageDiff) { + this.groups = []; + this.push('groups', this._makeFileComments()); - var state = { + // If image diff, only render the file lines. + if (isImageDiff) { return Promise.resolve(); } + + return new Promise(resolve => { + const state = { lineNums: {left: 0, right: 0}, sectionIndex: 0, }; content = this._splitCommonGroupsWithComments(content); - var currentBatch = 0; - var nextStep = function() { - + let currentBatch = 0; + const nextStep = () => { if (this._isScrolling) { this.async(nextStep, 100); return; @@ -130,11 +132,11 @@ } // Process the next section and incorporate the result. - var result = this._processNext(state, content); - result.groups.forEach(function(group) { + const result = this._processNext(state, content); + for (const group of result.groups) { this.push('groups', group); currentBatch += group.lines.length; - }, this); + } state.lineNums.left += result.lineDelta.left; state.lineNums.right += result.lineDelta.right; @@ -149,13 +151,13 @@ }; nextStep.call(this); - }.bind(this)); + }); }, /** * Cancel any jobs that are running. */ - cancel: function() { + cancel() { if (this._nextStepHandle !== undefined) { this.cancelAsync(this._nextStepHandle); this._nextStepHandle = undefined; @@ -165,29 +167,29 @@ /** * Process the next section of the diff. */ - _processNext: function(state, content) { - var section = content[state.sectionIndex]; + _processNext(state, content) { + const section = content[state.sectionIndex]; - var rows = { + const rows = { both: section[DiffGroupType.BOTH] || null, added: section[DiffGroupType.ADDED] || null, removed: section[DiffGroupType.REMOVED] || null, }; - var highlights = { + const highlights = { added: section[DiffHighlights.ADDED] || null, removed: section[DiffHighlights.REMOVED] || null, }; if (rows.both) { // If it's a shared section. - var sectionEnd = null; + let sectionEnd = null; if (state.sectionIndex === 0) { sectionEnd = 'first'; } else if (state.sectionIndex === content.length - 1) { sectionEnd = 'last'; } - var sharedGroups = this._sharedGroupsFromRows( + const sharedGroups = this._sharedGroupsFromRows( rows.both, content.length > 1 ? this.context : WHOLE_FILE, state.lineNums.left, @@ -202,13 +204,13 @@ groups: sharedGroups, }; } else { // Otherwise it's a delta section. - - var deltaGroup = this._deltaGroupFromRows( + const deltaGroup = this._deltaGroupFromRows( rows.added, rows.removed, state.lineNums.left, state.lineNums.right, highlights); + deltaGroup.dueToRebase = section.due_to_rebase; return { lineDelta: { @@ -232,14 +234,14 @@ * 'last' and null respectively. * @return {Array<GrDiffGroup>} */ - _sharedGroupsFromRows: function(rows, context, startLineNumLeft, + _sharedGroupsFromRows(rows, context, startLineNumLeft, startLineNumRight, opt_sectionEnd) { - var result = []; - var lines = []; - var line; + const result = []; + const lines = []; + let line; // Map each row to a GrDiffLine. - for (var i = 0; i < rows.length; i++) { + for (let i = 0; i < rows.length; i++) { line = new GrDiffLine(GrDiffLine.Type.BOTH); line.text = rows[i]; line.beforeNumber = ++startLineNumLeft; @@ -250,7 +252,7 @@ // Find the hidden range based on the user's context preference. If this // is the first or the last section of the diff, make sure the collapsed // part of the section extends to the edge of the file. - var hiddenRange = [context, rows.length - context]; + const hiddenRange = [context, rows.length - context]; if (opt_sectionEnd === 'first') { hiddenRange[0] = 0; } else if (opt_sectionEnd === 'last') { @@ -259,15 +261,15 @@ // If there is a range to hide. if (context !== WHOLE_FILE && hiddenRange[1] - hiddenRange[0] > 1) { - var linesBeforeCtx = lines.slice(0, hiddenRange[0]); - var hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]); - var linesAfterCtx = lines.slice(hiddenRange[1]); + const linesBeforeCtx = lines.slice(0, hiddenRange[0]); + const hiddenLines = lines.slice(hiddenRange[0], hiddenRange[1]); + const linesAfterCtx = lines.slice(hiddenRange[1]); if (linesBeforeCtx.length > 0) { result.push(new GrDiffGroup(GrDiffGroup.Type.BOTH, linesBeforeCtx)); } - var ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL); + const ctxLine = new GrDiffLine(GrDiffLine.Type.CONTEXT_CONTROL); ctxLine.contextGroup = new GrDiffGroup(GrDiffGroup.Type.BOTH, hiddenLines); result.push(new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL, @@ -292,9 +294,9 @@ * @param {Number} startLineNumRight * @return {GrDiffGroup} */ - _deltaGroupFromRows: function(rowsAdded, rowsRemoved, startLineNumLeft, + _deltaGroupFromRows(rowsAdded, rowsRemoved, startLineNumLeft, startLineNumRight, highlights) { - var lines = []; + let lines = []; if (rowsRemoved) { lines = lines.concat(this._deltaLinesFromRows(GrDiffLine.Type.REMOVE, rowsRemoved, startLineNumLeft, highlights.removed)); @@ -309,7 +311,7 @@ /** * @return {Array<GrDiffLine>} */ - _deltaLinesFromRows: function(lineType, rows, startLineNum, + _deltaLinesFromRows(lineType, rows, startLineNum, opt_highlights) { // Normalize highlights if they have been passed. if (opt_highlights) { @@ -317,9 +319,9 @@ opt_highlights); } - var lines = []; - var line; - for (var i = 0; i < rows.length; i++) { + const lines = []; + let line; + for (let i = 0; i < rows.length; i++) { line = new GrDiffLine(lineType); line.text = rows[i]; if (lineType === GrDiffLine.Type.ADD) { @@ -328,16 +330,15 @@ line.beforeNumber = ++startLineNum; } if (opt_highlights) { - line.highlights = opt_highlights.filter( - function(hl) { return hl.contentIndex === i; }); + line.highlights = opt_highlights.filter(hl => hl.contentIndex === i); } lines.push(line); } return lines; }, - _makeFileComments: function() { - var line = new GrDiffLine(GrDiffLine.Type.BOTH); + _makeFileComments() { + const line = new GrDiffLine(GrDiffLine.Type.BOTH); line.beforeNumber = GrDiffLine.FILE; line.afterNumber = GrDiffLine.FILE; return new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]); @@ -350,30 +351,32 @@ * @param {Object} content The diff content object. * @return {Object} A new diff content object with regions split up. */ - _splitCommonGroupsWithComments: function(content) { - var result = []; - var leftLineNum = 0; - var rightLineNum = 0; + _splitCommonGroupsWithComments(content) { + const result = []; + let leftLineNum = 0; + let rightLineNum = 0; // 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. if (this.context === -1) { - var newContent = []; - content.forEach(function(group) { - if (group.ab) { - newContent.push.apply(newContent, this._breakdownGroup(group)); + const newContent = []; + for (const group of content) { + if (group.ab && group.ab.length > MAX_GROUP_SIZE * 2) { + // Split large shared groups in two, where the first is the maximum + // group size. + newContent.push({ab: group.ab.slice(0, MAX_GROUP_SIZE)}); + newContent.push({ab: group.ab.slice(MAX_GROUP_SIZE)}); } else { newContent.push(group); } - }.bind(this)); + } content = newContent; } // For each section in the diff. - for (var i = 0; i < content.length; i++) { - + for (let i = 0; i < content.length; i++) { // If it isn't a common group, append it as-is and update line numbers. if (!content[i].ab) { if (content[i].a) { @@ -383,25 +386,24 @@ rightLineNum += content[i].b.length; } - this._breakdownGroup(content[i]).forEach(function(group) { + for (const group of this._breakdownGroup(content[i])) { result.push(group); - }); + } continue; } - var chunk = content[i].ab; - var currentChunk = {ab: []}; + const chunk = content[i].ab; + let currentChunk = {ab: []}; // For each line in the common group. - for (var j = 0; j < chunk.length; j++) { + for (const subChunk of chunk) { leftLineNum++; rightLineNum++; // If this line should not be collapsed. if (this.keyLocations[DiffSide.LEFT][leftLineNum] || this.keyLocations[DiffSide.RIGHT][rightLineNum]) { - // If any lines have been accumulated into the chunk leading up to // this non-collapse line, then add them as a chunk and start a new // one. @@ -411,10 +413,10 @@ } // Add the non-collapse line as its own chunk. - result.push({ab: [chunk[j]]}); + result.push({ab: [subChunk]}); } else { // Append the current line to the current chunk. - currentChunk.ab.push(chunk[j]); + currentChunk.ab.push(subChunk); } } @@ -444,14 +446,13 @@ * - endIndex: (optional) Where the highlight should end. If omitted, the * highlight is meant to be a continuation onto the next line. */ - _normalizeIntralineHighlights: function(content, highlights) { - var contentIndex = 0; - var idx = 0; - var normalized = []; - for (var i = 0; i < highlights.length; i++) { - var line = content[contentIndex] + '\n'; - var hl = highlights[i]; - var j = 0; + _normalizeIntralineHighlights(content, highlights) { + let contentIndex = 0; + let idx = 0; + const normalized = []; + for (const hl of highlights) { + let line = content[contentIndex] + '\n'; + let j = 0; while (j < hl[0]) { if (idx === line.length) { idx = 0; @@ -461,8 +462,8 @@ idx++; j++; } - var lineHighlight = { - contentIndex: contentIndex, + let lineHighlight = { + contentIndex, startIndex: idx, }; @@ -473,7 +474,7 @@ line = content[++contentIndex] + '\n'; normalized.push(lineHighlight); lineHighlight = { - contentIndex: contentIndex, + contentIndex, startIndex: idx, }; continue; @@ -494,8 +495,8 @@ * @param {!Object} A raw chunk from a diff response. * @return {!Array<!Array<!Object>>} */ - _breakdownGroup: function(group) { - var key = null; + _breakdownGroup(group) { + let key = null; if (group.a && !group.b) { key = 'a'; } else if (group.b && !group.a) { @@ -507,11 +508,14 @@ if (!key) { return [group]; } return this._breakdown(group[key], MAX_GROUP_SIZE) - .map(function(subgroupLines) { - var subGroup = {}; - subGroup[key] = subgroupLines; - return subGroup; - }); + .map(subgroupLines => { + const subGroup = {}; + subGroup[key] = subgroupLines; + if (group.due_to_rebase) { + subGroup.due_to_rebase = true; + } + return subGroup; + }); }, /** @@ -522,12 +526,12 @@ * @return {!Array<!Array<T>>} * @template T */ - _breakdown: function(array, size) { + _breakdown(array, size) { if (!array.length) { return []; } if (array.length < size) { return [array]; } - var head = array.slice(0, array.length - size); - var tail = array.slice(array.length - size); + 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/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html index 5429f52..fcb8aec 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-diff-processor.html"> <script>void(0);</script> @@ -33,40 +32,40 @@ </test-fixture> <script> - suite('gr-diff-processor tests', function() { - var WHOLE_FILE = -1; - var loremIpsum = 'Lorem ipsum dolor sit amet, ei nonumes vituperata ius. ' + + suite('gr-diff-processor tests', () => { + const WHOLE_FILE = -1; + const loremIpsum = + 'Lorem ipsum dolor sit amet, ei nonumes vituperata ius. ' + 'Duo animal omnesque fabellas et. Id has phaedrum dignissim ' + 'deterruisset, pro ei petentium comprehensam, ut vis solum dicta. ' + 'Eos cu aliquam labores qualisque, usu postea inermis te, et solum ' + 'fugit assum per.'; - var element; - var sandbox; + let element; + let sandbox; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - suite('not logged in', function() { - - setup(function() { + suite('not logged in', () => { + setup(() => { element = fixture('basic'); element.context = 4; }); - test('process loaded content', function(done) { - var content = [ + test('process loaded content', done => { + const content = [ { ab: [ '<!DOCTYPE html>', '<meta charset="utf-8">', - ] + ], }, { a: [ @@ -82,16 +81,16 @@ 'Leela: This is the only place the ship can’t hear us, so ', 'everyone pretend to shower.', 'Fry: Same as every day. Got it.', - ] + ], }, ]; - element.process(content).then(function() { - var groups = element.groups; + element.process(content).then(() => { + const groups = element.groups; assert.equal(groups.length, 4); - var group = groups[0]; + let group = groups[0]; assert.equal(group.type, GrDiffGroup.Type.BOTH); assert.equal(group.lines.length, 1); assert.equal(group.lines[0].text, ''); @@ -144,27 +143,27 @@ }); }); - test('insert context groups', function(done) { - var content = [ + test('insert context groups', done => { + const content = [ {ab: []}, {a: ['all work and no play make andybons a dull boy']}, {ab: []}, {b: ['elgoog elgoog elgoog']}, {ab: []}, ]; - for (var i = 0; i < 100; i++) { + for (let i = 0; i < 100; i++) { content[0].ab.push('all work and no play make jack a dull boy'); content[4].ab.push('all work and no play make jill a dull girl'); } - for (var i = 0; i < 5; i++) { + for (let i = 0; i < 5; i++) { content[2].ab.push('no tv and no beer make homer go crazy'); } - var context = 10; + const context = 10; element.context = context; - element.process(content).then(function() { - var groups = element.groups; + element.process(content).then(() => { + const groups = element.groups; assert.equal(groups[0].type, GrDiffGroup.Type.BOTH); assert.equal(groups[0].lines.length, 1); @@ -175,15 +174,15 @@ assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL); assert.instanceOf(groups[1].lines[0].contextGroup, GrDiffGroup); assert.equal(groups[1].lines[0].contextGroup.lines.length, 90); - groups[1].lines[0].contextGroup.lines.forEach(function(l) { + for (const l of groups[1].lines[0].contextGroup.lines) { assert.equal(l.text, content[0].ab[0]); - }); + } assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); assert.equal(groups[2].lines.length, context); - groups[2].lines.forEach(function(l) { + for (const l of groups[2].lines) { assert.equal(l.text, content[0].ab[0]); - }); + } assert.equal(groups[3].type, GrDiffGroup.Type.DELTA); assert.equal(groups[3].lines.length, 1); @@ -193,9 +192,9 @@ assert.equal(groups[4].type, GrDiffGroup.Type.BOTH); assert.equal(groups[4].lines.length, 5); - groups[4].lines.forEach(function(l) { + for (const l of groups[4].lines) { assert.equal(l.text, content[2].ab[0]); - }); + } assert.equal(groups[5].type, GrDiffGroup.Type.DELTA); assert.equal(groups[5].lines.length, 1); @@ -204,36 +203,36 @@ assert.equal(groups[6].type, GrDiffGroup.Type.BOTH); assert.equal(groups[6].lines.length, context); - groups[6].lines.forEach(function(l) { + for (const l of groups[6].lines) { assert.equal(l.text, content[4].ab[0]); - }); + } assert.equal(groups[7].type, GrDiffGroup.Type.CONTEXT_CONTROL); assert.instanceOf(groups[7].lines[0].contextGroup, GrDiffGroup); assert.equal(groups[7].lines[0].contextGroup.lines.length, 90); - groups[7].lines[0].contextGroup.lines.forEach(function(l) { + for (const l of groups[7].lines[0].contextGroup.lines) { assert.equal(l.text, content[4].ab[0]); - }); + } done(); }); }); - test('insert context groups', function(done) { - var content = [ + test('insert context groups', done => { + const content = [ {a: ['all work and no play make andybons a dull boy']}, {ab: []}, {b: ['elgoog elgoog elgoog']}, ]; - for (var i = 0; i < 50; i++) { + for (let i = 0; i < 50; i++) { content[1].ab.push('no tv and no beer make homer go crazy'); } - var context = 10; + const context = 10; element.context = context; - element.process(content).then(function() { - var groups = element.groups; + element.process(content).then(() => { + const groups = element.groups; assert.equal(groups[0].type, GrDiffGroup.Type.BOTH); assert.equal(groups[0].lines.length, 1); @@ -249,22 +248,22 @@ assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); assert.equal(groups[2].lines.length, context); - groups[2].lines.forEach(function(l) { + for (const l of groups[2].lines) { assert.equal(l.text, content[1].ab[0]); - }); + } assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL); assert.instanceOf(groups[3].lines[0].contextGroup, GrDiffGroup); assert.equal(groups[3].lines[0].contextGroup.lines.length, 30); - groups[3].lines[0].contextGroup.lines.forEach(function(l) { + for (const l of groups[3].lines[0].contextGroup.lines) { assert.equal(l.text, content[1].ab[0]); - }); + } assert.equal(groups[4].type, GrDiffGroup.Type.BOTH); assert.equal(groups[4].lines.length, context); - groups[4].lines.forEach(function(l) { + for (const l of groups[4].lines) { assert.equal(l.text, content[1].ab[0]); - }); + } assert.equal(groups[5].type, GrDiffGroup.Type.DELTA); assert.equal(groups[5].lines.length, 1); @@ -275,14 +274,14 @@ }); }); - test('break up common diff chunks', function() { + test('break up common diff chunks', () => { element.keyLocations = { left: {1: true}, right: {10: true}, }; - var lineNums = {left: 0, right: 0}; + const lineNums = {left: 0, right: 0}; - var content = [ + const content = [ { ab: [ 'Copyright (C) 2015 The Android Open Source Project', @@ -300,10 +299,11 @@ 'either express or implied. See the License for the specific ', 'language governing permissions and limitations under the ' + 'License.', - ] - } + ], + }, ]; - var result = element._splitCommonGroupsWithComments(content, lineNums); + const result = + element._splitCommonGroupsWithComments(content, lineNums); assert.deepEqual(result, [ { ab: ['Copyright (C) 2015 The Android Open Source Project'], @@ -319,11 +319,11 @@ 'http://www.apache.org/licenses/LICENSE-2.0', '', 'Unless required by applicable law or agreed to in writing, ', - ] + ], }, { ab: [ - 'software distributed under the License is distributed on an '], + 'software distributed under the License is distributed on an '], }, { ab: [ @@ -331,47 +331,50 @@ 'either express or implied. See the License for the specific ', 'language governing permissions and limitations under the ' + 'License.', - ] - } + ], + }, ]); }); - test('breaks-down shared chunks w/ whole-file', function() { - var lineNums = {left: 0, right: 0}; - var content = [{ - ab: _.times(75, function() { return '' + Math.random(); }), + test('breaks-down shared chunks w/ whole-file', () => { + const size = 120 * 2 + 5; + const lineNums = {left: 0, right: 0}; + const content = [{ + ab: _.times(size, () => { return `${Math.random()}`; }), }]; element.context = -1; - var result = element._splitCommonGroupsWithComments(content, lineNums); + const result = + element._splitCommonGroupsWithComments(content, lineNums); assert.equal(result.length, 2); - assert.deepEqual(result[0].ab, content[0].ab.slice(0, 5)); - assert.deepEqual(result[1].ab, content[0].ab.slice(5)); + assert.deepEqual(result[0].ab, content[0].ab.slice(0, 120)); + assert.deepEqual(result[1].ab, content[0].ab.slice(120)); }); - test('does not break-down shared chunks w/ context', function() { - var lineNums = {left: 0, right: 0}; - var content = [{ - ab: _.times(75, function() { return '' + Math.random(); }), + test('does not break-down shared chunks w/ context', () => { + const lineNums = {left: 0, right: 0}; + const content = [{ + ab: _.times(75, () => { return `${Math.random()}`; }), }]; element.context = 4; - var result = element._splitCommonGroupsWithComments(content, lineNums); + const result = + element._splitCommonGroupsWithComments(content, lineNums); assert.deepEqual(result, content); }); - test('intraline normalization', function() { + test('intraline normalization', () => { // The content and highlights are in the format returned by the Gerrit // REST API. - var content = [ + let content = [ ' <section class="summary">', ' <gr-linked-text content="' + '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>', ' </section>', ]; - var highlights = [ - [31, 34], [42, 26] + let highlights = [ + [31, 34], [42, 26], ]; - var results = element._normalizeIntralineHighlights(content, + let results = element._normalizeIntralineHighlights(content, highlights); assert.deepEqual(results, [ { @@ -391,7 +394,7 @@ contentIndex: 2, startIndex: 0, endIndex: 6, - } + }, ]); content = [ @@ -438,18 +441,18 @@ contentIndex: 5, startIndex: 12, endIndex: 41, - } + }, ]); }); - test('scrolling pauses rendering', function() { - var contentRow = { + test('scrolling pauses rendering', () => { + const contentRow = { ab: [ '<!DOCTYPE html>', '<meta charset="utf-8">', - ] + ], }; - var content = _.times(200, _.constant(contentRow)); + const content = _.times(200, _.constant(contentRow)); sandbox.stub(element, 'async'); element._isScrolling = true; element.process(content); @@ -459,17 +462,34 @@ assert.equal(element.groups.length, 33); }); - suite('gr-diff-processor helpers', function() { - var rows; + test('image diffs', () => { + const contentRow = { + ab: [ + '<!DOCTYPE html>', + '<meta charset="utf-8">', + ], + }; + const content = _.times(200, _.constant(contentRow)); + sandbox.stub(element, 'async'); + element.process(content, true); + assert.equal(element.groups.length, 1); - setup(function() { + // Image diffs don't process content, just the 'FILE' line. + assert.equal(element.groups[0].lines.length, 1); + }); + + + suite('gr-diff-processor helpers', () => { + let rows; + + setup(() => { rows = loremIpsum.split(' '); }); - test('_sharedGroupsFromRows WHOLE_FILE', function() { - var context = WHOLE_FILE; - var lineNumbers = {left: 10, right: 100}; - var result = element._sharedGroupsFromRows( + test('_sharedGroupsFromRows WHOLE_FILE', () => { + const context = WHOLE_FILE; + const lineNumbers = {left: 10, right: 100}; + const result = element._sharedGroupsFromRows( rows, context, lineNumbers.left, lineNumbers.right, null); // Results in one, uncollapsed group with all rows. @@ -487,11 +507,11 @@ lineNumbers.right + rows.length); }); - test('_sharedGroupsFromRows context', function() { - var context = 10; - var result = element._sharedGroupsFromRows( + test('_sharedGroupsFromRows context', () => { + const context = 10; + const result = element._sharedGroupsFromRows( rows, context, 10, 100, null); - var expectedCollapseSize = rows.length - 2 * context; + const expectedCollapseSize = rows.length - 2 * context; assert.equal(result.length, 3, 'Results in three groups'); @@ -506,11 +526,11 @@ expectedCollapseSize); }); - test('_sharedGroupsFromRows first', function() { - var context = 10; - var result = element._sharedGroupsFromRows( + test('_sharedGroupsFromRows first', () => { + const context = 10; + const result = element._sharedGroupsFromRows( rows, context, 10, 100, 'first'); - var expectedCollapseSize = rows.length - context; + const expectedCollapseSize = rows.length - context; assert.equal(result.length, 2, 'Results in two groups'); @@ -523,11 +543,11 @@ expectedCollapseSize); }); - test('_sharedGroupsFromRows few-rows', function() { + test('_sharedGroupsFromRows few-rows', () => { // Only ten rows. rows = rows.slice(0, 10); - var context = 10; - var result = element._sharedGroupsFromRows( + const context = 10; + const result = element._sharedGroupsFromRows( rows, context, 10, 100, 'first'); // Results in one uncollapsed group with all rows. @@ -535,10 +555,10 @@ assert.equal(result[0].lines.length, rows.length); }); - test('_sharedGroupsFromRows no single line collapse', function() { + test('_sharedGroupsFromRows no single line collapse', () => { rows = rows.slice(0, 7); - var context = 3; - var result = element._sharedGroupsFromRows( + const context = 3; + const result = element._sharedGroupsFromRows( rows, context, 10, 100); // Results in one uncollapsed group with all rows. @@ -546,9 +566,9 @@ assert.equal(result[0].lines.length, rows.length); }); - test('_deltaLinesFromRows', function() { - var startLineNum = 10; - var result = element._deltaLinesFromRows(GrDiffLine.Type.ADD, rows, + test('_deltaLinesFromRows', () => { + const startLineNum = 10; + let result = element._deltaLinesFromRows(GrDiffLine.Type.ADD, rows, startLineNum); assert.equal(result.length, rows.length); @@ -572,54 +592,64 @@ }); }); - suite('_breakdown*', function() { - test('_breakdownGroup breaks down additions', function() { + suite('_breakdown*', () => { + test('_breakdownGroup breaks down additions', () => { sandbox.spy(element, '_breakdown'); - var chunk = {b: ['blah', 'blah', 'blah']}; - var result = element._breakdownGroup(chunk); + const chunk = {b: ['blah', 'blah', 'blah']}; + const result = element._breakdownGroup(chunk); assert.deepEqual(result, [chunk]); assert.isTrue(element._breakdown.called); }); - test('_breakdown common case', function() { - var array = 'Lorem ipsum dolor sit amet, suspendisse inceptos' + test('_breakdownGroup keeps due_to_rebase for broken down additions', + () => { + sandbox.spy(element, '_breakdown'); + const chunk = {b: ['blah', 'blah', 'blah'], due_to_rebase: true}; + const result = element._breakdownGroup(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(' '); - var size = 3; + const size = 3; - var result = element._breakdown(array, size); + const result = element._breakdown(array, size); - result.forEach(function(subResult) { + for (const subResult of result) { assert.isAtMost(subResult.length, size); - }); - var flattened = result - .reduce(function(a, b) { return a.concat(b); }, []); + } + const flattened = result + .reduce((a, b) => { return a.concat(b); }, []); assert.deepEqual(flattened, array); }); - test('_breakdown smaller than size', function() { - var array = 'Lorem ipsum dolor sit amet, suspendisse inceptos' + test('_breakdown smaller than size', () => { + const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos' .split(' '); - var size = 10; - var expected = [array]; + const size = 10; + const expected = [array]; - var result = element._breakdown(array, size); + const result = element._breakdown(array, size); assert.deepEqual(result, expected); }); - test('_breakdown empty', function() { - var array = []; - var size = 10; - var expected = []; + test('_breakdown empty', () => { + const array = []; + const size = 10; + const expected = []; - var result = element._breakdown(array, size); + const result = element._breakdown(array, size); assert.deepEqual(result, expected); }); }); }); - test('detaching cancels', function() { + test('detaching cancels', () => { element = fixture('basic'); sandbox.stub(element, 'cancel'); element.detached();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html index bfddf89..395c958 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.html
@@ -14,10 +14,11 @@ limitations under the License. --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-diff-selection"> <template> - <style> + <style include="shared-styles"> .contentWrapper ::content .content, .contentWrapper ::content .contextControl { -webkit-user-select: none;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js index fa1aeb2..ecddba2 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -18,13 +18,13 @@ * Possible CSS classes indicating the state of selection. Dynamically added/ * removed based on where the user clicks within the diff. */ - var SelectionClass = { + const SelectionClass = { COMMENT: 'selected-comment', LEFT: 'selected-left', RIGHT: 'selected-right', }; - var getNewCache = function() { return {left: null, right: null}; }; + const getNewCache = () => { return {left: null, right: null}; }; Polymer({ is: 'gr-diff-selection', @@ -43,11 +43,11 @@ ], listeners: { - 'copy': '_handleCopy', - 'down': '_handleDown', + copy: '_handleCopy', + down: '_handleDown', }, - attached: function() { + attached() { this.classList.add(SelectionClass.RIGHT); }, @@ -59,19 +59,19 @@ return this._cachedDiffBuilder; }, - _diffChanged: function() { + _diffChanged() { this._linesCache = getNewCache(); }, - _handleDown: function(e) { - var lineEl = this.diffBuilder.getLineElByChild(e.target); + _handleDown(e) { + const lineEl = this.diffBuilder.getLineElByChild(e.target); if (!lineEl) { return; } - var commentSelected = + const commentSelected = this._elementDescendedFromClass(e.target, 'gr-diff-comment'); - var side = this.diffBuilder.getSideByLineEl(lineEl); - var targetClasses = []; + const side = this.diffBuilder.getSideByLineEl(lineEl); + const targetClasses = []; targetClasses.push(side === 'left' ? SelectionClass.LEFT : SelectionClass.RIGHT); @@ -80,23 +80,23 @@ targetClasses.push(SelectionClass.COMMENT); } // Remove any selection classes that do not belong. - for (var key in SelectionClass) { + for (const key in SelectionClass) { if (SelectionClass.hasOwnProperty(key)) { - var className = SelectionClass[key]; - if (targetClasses.indexOf(className) === -1) { + const className = SelectionClass[key]; + if (!targetClasses.includes(className)) { this.classList.remove(SelectionClass[key]); } } } // Add new selection classes iff they are not already present. - for (var i = 0; i < targetClasses.length; i++) { - if (!this.classList.contains(targetClasses[i])) { - this.classList.add(targetClasses[i]); + for (const _class of targetClasses) { + if (!this.classList.contains(_class)) { + this.classList.add(_class); } } }, - _getCopyEventTarget: function(e) { + _getCopyEventTarget(e) { return Polymer.dom(e).rootTarget; }, @@ -108,7 +108,7 @@ * @param {!string} className * @return {boolean} */ - _elementDescendedFromClass: function(element, className) { + _elementDescendedFromClass(element, className) { while (!element.classList.contains(className)) { if (!element.parentElement || element === this.diffBuilder.diffElement) { @@ -119,20 +119,20 @@ return true; }, - _handleCopy: function(e) { - var commentSelected = false; - var target = this._getCopyEventTarget(e); + _handleCopy(e) { + let commentSelected = false; + const target = this._getCopyEventTarget(e); if (target.type === 'textarea') { return; } if (!this._elementDescendedFromClass(target, 'diff-row')) { return; } if (this.classList.contains(SelectionClass.COMMENT)) { commentSelected = true; } - var lineEl = this.diffBuilder.getLineElByChild(target); + const lineEl = this.diffBuilder.getLineElByChild(target); if (!lineEl) { return; } - var side = this.diffBuilder.getSideByLineEl(lineEl); - var text = this._getSelectedText(side, commentSelected); + const side = this.diffBuilder.getSideByLineEl(lineEl); + const text = this._getSelectedText(side, commentSelected); if (text) { e.clipboardData.setData('Text', text); e.preventDefault(); @@ -148,19 +148,20 @@ * @param {boolean} Whether or not a comment is selected. * @return {string} The selected text. */ - _getSelectedText: function(side, commentSelected) { - var sel = window.getSelection(); + _getSelectedText(side, commentSelected) { + const sel = window.getSelection(); if (sel.rangeCount != 1) { return; // No multi-select support yet. } if (commentSelected) { return this._getCommentLines(sel, side); } - var range = GrRangeNormalizer.normalize(sel.getRangeAt(0)); - var startLineEl = this.diffBuilder.getLineElByChild(range.startContainer); - var endLineEl = this.diffBuilder.getLineElByChild(range.endContainer); - var startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10); - var endLineNum = parseInt(endLineEl.getAttribute('data-value'), 10); + const range = GrRangeNormalizer.normalize(sel.getRangeAt(0)); + const startLineEl = + this.diffBuilder.getLineElByChild(range.startContainer); + const endLineEl = this.diffBuilder.getLineElByChild(range.endContainer); + const startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10); + const endLineNum = parseInt(endLineEl.getAttribute('data-value'), 10); return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum, range.endOffset, side); @@ -176,9 +177,9 @@ * @param {!string} side The side that is currently selected. * @return {string} The selected diff text. */ - _getRangeFromDiff: function(startLineNum, startOffset, endLineNum, - endOffset, side) { - var lines = this._getDiffLines(side).slice(startLineNum - 1, endLineNum); + _getRangeFromDiff(startLineNum, startOffset, endLineNum, endOffset, side) { + const lines = + this._getDiffLines(side).slice(startLineNum - 1, endLineNum); if (lines.length) { lines[lines.length - 1] = lines[lines.length - 1] .substring(0, endOffset); @@ -193,17 +194,13 @@ * @param {!string} side The side that is currently selected. * @return {Array.string} An array of strings indexed by line number. */ - _getDiffLines: function(side) { + _getDiffLines(side) { if (this._linesCache[side]) { return this._linesCache[side]; } - var lines = []; - var chunk; - var key = side === 'left' ? 'a' : 'b'; - for (var chunkIndex = 0; - chunkIndex < this.diff.content.length; - chunkIndex++) { - chunk = this.diff.content[chunkIndex]; + let lines = []; + const key = side === 'left' ? 'a' : 'b'; + for (const chunk of this.diff.content) { if (chunk.ab) { lines = lines.concat(chunk.ab); } else if (chunk[key]) { @@ -222,16 +219,16 @@ * @param {!string} side The side that is currently selected. * @return {string} The selected comment text. */ - _getCommentLines: function(sel, side) { - var range = GrRangeNormalizer.normalize(sel.getRangeAt(0)); - var content = []; + _getCommentLines(sel, side) { + const range = GrRangeNormalizer.normalize(sel.getRangeAt(0)); + const content = []; // Query the diffElement for comments. - var messages = this.diffBuilder.diffElement.querySelectorAll( - '.side-by-side [data-side="' + side + - '"] .message *, .unified .message *'); + const messages = this.diffBuilder.diffElement.querySelectorAll( + `.side-by-side [data-side="${side + }"] .message *, .unified .message *`); - for (var i = 0; i < messages.length; i++) { - var el = messages[i]; + for (let i = 0; i < messages.length; i++) { + const el = messages[i]; // Check if the comment element exists inside the selection. if (sel.containsNode(el, true)) { // Padded elements require newlines for accurate spacing. @@ -262,10 +259,10 @@ * @param {Range} range The normalized selection range. * @return {string} The text within the selection. */ - _getTextContentForRange: function(domNode, sel, range) { + _getTextContentForRange(domNode, sel, range) { if (!sel.containsNode(domNode, true)) { return ''; } - var text = ''; + let text = ''; if (domNode instanceof Text) { text = domNode.textContent; if (domNode === range.endContainer) { @@ -275,9 +272,8 @@ text = text.substring(range.startOffset); } } else { - for (var i = 0; i < domNode.childNodes.length; i++) { - text += this._getTextContentForRange(domNode.childNodes[i], - sel, range); + for (const childNode of domNode.childNodes) { + text += this._getTextContentForRange(childNode, sel, range); } } return text;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html index 3eeba90..2a35a39 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-diff-selection.html"> <script>void(0);</script> @@ -101,13 +100,13 @@ </test-fixture> <script> - suite('gr-diff-selection', function() { - var element; - var sandbox; + suite('gr-diff-selection', () => { + let element; + let sandbox; - var emulateCopyOn = function(target) { - var fakeEvent = { - target: target, + const emulateCopyOn = function(target) { + const fakeEvent = { + target, preventDefault: sandbox.stub(), clipboardData: { setData: sandbox.stub(), @@ -118,7 +117,7 @@ return fakeEvent; }; - setup(function() { + setup(() => { element = fixture('basic'); sandbox = sinon.sandbox.create(); sandbox.stub(element, '_getCopyEventTarget'); @@ -145,11 +144,11 @@ }; }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('applies selected-left on left side click', function() { + test('applies selected-left on left side click', () => { element.classList.add('selected-right'); element._cachedDiffBuilder.getSideByLineEl.returns('left'); MockInteractions.down(element); @@ -160,7 +159,7 @@ 'removes selected-right'); }); - test('applies selected-right on right side click', function() { + test('applies selected-right on right side click', () => { element.classList.add('selected-left'); element._cachedDiffBuilder.getSideByLineEl.returns('right'); MockInteractions.down(element); @@ -170,41 +169,41 @@ element.classList.contains('selected-left'), 'removes selected-left'); }); - test('ignores copy for non-content Element', function() { + test('ignores copy for non-content Element', () => { sandbox.stub(element, '_getSelectedText'); emulateCopyOn(element.querySelector('.not-diff-row')); assert.isFalse(element._getSelectedText.called); }); - test('asks for text for left side Elements', function() { + test('asks for text for left side Elements', () => { element._cachedDiffBuilder.getSideByLineEl.returns('left'); sandbox.stub(element, '_getSelectedText'); emulateCopyOn(element.querySelector('div.contentText')); assert.deepEqual(['left', false], element._getSelectedText.lastCall.args); }); - test('reacts to copy for content Elements', function() { + test('reacts to copy for content Elements', () => { sandbox.stub(element, '_getSelectedText'); emulateCopyOn(element.querySelector('div.contentText')); assert.isTrue(element._getSelectedText.called); }); - test('copy event is prevented for content Elements', function() { + test('copy event is prevented for content Elements', () => { sandbox.stub(element, '_getSelectedText'); element._cachedDiffBuilder.getSideByLineEl.returns('left'); element._getSelectedText.returns('test'); - var event = emulateCopyOn(element.querySelector('div.contentText')); + const event = emulateCopyOn(element.querySelector('div.contentText')); assert.isTrue(event.preventDefault.called); }); - test('inserts text into clipboard on copy', function() { + test('inserts text into clipboard on copy', () => { sandbox.stub(element, '_getSelectedText').returns('the text'); - var event = emulateCopyOn(element.querySelector('div.contentText')); + const event = emulateCopyOn(element.querySelector('div.contentText')); assert.deepEqual( ['Text', 'the text'], event.clipboardData.setData.lastCall.args); }); - test('copies content correctly', function() { + test('copies content correctly', () => { // Fetch the line number. element._cachedDiffBuilder.getLineElByChild = function(child) { while (!child.classList.contains('content') && child.parentElement) { @@ -216,9 +215,9 @@ element.classList.add('selected-left'); element.classList.remove('selected-right'); - var selection = window.getSelection(); + const selection = window.getSelection(); selection.removeAllRanges(); - var range = document.createRange(); + const range = document.createRange(); range.setStart(element.querySelector('div.contentText').firstChild, 3); range.setEnd( element.querySelectorAll('div.contentText')[4].firstChild, 2); @@ -226,13 +225,13 @@ assert.equal(element._getSelectedText('left'), 'ba\nzin\nga'); }); - test('copies comments', function() { + test('copies comments', () => { element.classList.add('selected-left'); element.classList.add('selected-comment'); element.classList.remove('selected-right'); - var selection = window.getSelection(); + const selection = window.getSelection(); selection.removeAllRanges(); - var range = document.createRange(); + const range = document.createRange(); range.setStart( element.querySelector('.gr-formatted-text *').firstChild, 3); range.setEnd( @@ -242,14 +241,14 @@ element._getSelectedText('left', true)); }); - test('respects astral chars in comments', function() { + test('respects astral chars in comments', () => { element.classList.add('selected-left'); element.classList.add('selected-comment'); element.classList.remove('selected-right'); - var selection = window.getSelection(); + const selection = window.getSelection(); selection.removeAllRanges(); - var range = document.createRange(); - var nodes = element.querySelectorAll('.gr-formatted-text *'); + const range = document.createRange(); + const nodes = element.querySelectorAll('.gr-formatted-text *'); range.setStart(nodes[2].childNodes[2], 13); range.setEnd(nodes[2].childNodes[2], 23); selection.addRange(range); @@ -257,15 +256,15 @@ element._getSelectedText('left', true)); }); - test('defers to default behavior for textarea', function() { + test('defers to default behavior for textarea', () => { element.classList.add('selected-left'); element.classList.remove('selected-right'); - var selectedTextSpy = sandbox.spy(element, '_getSelectedText'); + const selectedTextSpy = sandbox.spy(element, '_getSelectedText'); emulateCopyOn(element.querySelector('textarea')); assert.isFalse(selectedTextSpy.called); }); - test('regression test for 4794', function() { + test('regression test for 4794', () => { element._cachedDiffBuilder.getLineElByChild = function(child) { while (!child.classList.contains('content') && child.parentElement) { child = child.parentElement; @@ -276,9 +275,9 @@ element.classList.add('selected-right'); element.classList.remove('selected-left'); - var selection = window.getSelection(); + const selection = window.getSelection(); selection.removeAllRanges(); - var range = document.createRange(); + const range = document.createRange(); range.setStart( element.querySelectorAll('div.contentText')[1].firstChild, 4); range.setEnd( @@ -287,12 +286,12 @@ assert.equal(element._getSelectedText('right'), ' other'); }); - suite('_getTextContentForRange', function() { - var selection; - var range; - var nodes; + suite('_getTextContentForRange', () => { + let selection; + let range; + let nodes; - setup(function() { + setup(() => { element.classList.add('selected-left'); element.classList.add('selected-comment'); element.classList.remove('selected-right'); @@ -302,7 +301,7 @@ nodes = element.querySelectorAll('.gr-formatted-text *'); }); - test('multi level element contained in range', function() { + test('multi level element contained in range', () => { range.setStart(nodes[2].childNodes[0], 1); range.setEnd(nodes[2].childNodes[2], 7); selection.addRange(range); @@ -311,7 +310,7 @@ }); - test('multi level element as startContainer of range', function() { + test('multi level element as startContainer of range', () => { range.setStart(nodes[2].childNodes[1], 0); range.setEnd(nodes[2].childNodes[2], 7); selection.addRange(range); @@ -319,7 +318,7 @@ 'a differ'); }); - test('startContainer === endContainer', function() { + test('startContainer === endContainer', () => { range.setStart(nodes[0].firstChild, 2); range.setEnd(nodes[0].firstChild, 12); selection.addRange(range); @@ -328,7 +327,7 @@ }); }); - test('cache is reset when diff changes', function() { + test('cache is reset when diff changes', () => { element._linesCache = {left: 'test', right: 'test'}; element.diff = {}; flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html index 1204170..5056927 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -14,27 +14,35 @@ limitations under the License. --> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> <link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html"> <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> <link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html"> -<link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> +<link rel="import" href="../../shared/gr-fixed-panel/gr-fixed-panel.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../../shared/gr-select/gr-select.html"> -<link rel="import" href="../gr-diff/gr-diff.html"> <link rel="import" href="../gr-diff-cursor/gr-diff-cursor.html"> <link rel="import" href="../gr-diff-preferences/gr-diff-preferences.html"> +<link rel="import" href="../gr-diff/gr-diff.html"> <link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-diff-view"> <template> - <style> + <style include="shared-styles"> :host { background-color: var(--view-background-color); - display: block; + } + gr-diff { + border: none; + } + gr-fixed-panel { + background-color: #fff; + border-bottom: 1px #eee solid; + z-index: 1; } header, .subHeader { @@ -82,6 +90,7 @@ .dropdown-content { background-color: #fff; box-shadow: 0 1px 5px rgba(0, 0, 0, .3); + max-height: 70vh; } .dropdown-content a { cursor: pointer; @@ -132,6 +141,10 @@ .separator { margin: 0 .25em; } + .noOverflow { + display: block; + overflow: auto; + } @media screen and (max-width: 50em) { header { padding: .5em var(--default-horizontal-margin); @@ -178,66 +191,68 @@ } } </style> - <header> - <h3> - <a href$="[[_computeChangePath(_changeNum, _patchRange.*, _change.revisions)]]"> - [[_changeNum]]</a><span>:</span> - <span>[[_change.subject]]</span> - <span class="dash">—</span> - <input id="reviewed" - class="reviewed" - type="checkbox" - on-change="_handleReviewedChange" - hidden$="[[!_loggedIn]]" hidden> - <div class="jumpToFileContainer desktop"> - <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler"> - <span>[[_computeFileDisplayName(_path)]]</span> - <span class="downArrow">▼</span> - </gr-button> - <!-- *-align="" to disable iron-dropdown's element positioning. --> - <iron-dropdown id="dropdown" - allow-outside-scroll - vertical-align="" - horizontal-align=""> - <div class="dropdown-content"> - <template - is="dom-repeat" - items="[[_fileList]]" - as="path" - initial-count="75"> - <a href$="[[_computeDiffURL(_changeNum, _patchRange.*, path)]]" - selected$="[[_computeFileSelected(path, _path)]]" - data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]" - on-tap="_handleFileTap">[[_computeFileDisplayName(path)]]</a> + <gr-fixed-panel + floating-disabled="[[_panelFloatingDisabled]]" + keep-on-scroll + ready-for-measure="[[!_loading]]"> + <header> + <h3> + <a href$="[[_computeChangePath(_changeNum, _patchRange.*, _change.revisions)]]"> + [[_changeNum]]</a><span>:</span> + <span>[[_change.subject]]</span> + <span class="dash">—</span> + <input id="reviewed" + class="reviewed" + type="checkbox" + on-change="_handleReviewedChange" + hidden$="[[!_loggedIn]]" hidden> + <div class="jumpToFileContainer desktop"> + <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler"> + <span>[[_computeFileDisplayName(_path)]]</span> + <span class="downArrow">▼</span> + </gr-button> + <!-- *-align="" to disable iron-dropdown's element positioning. --> + <iron-dropdown id="dropdown" + allow-outside-scroll + vertical-align="" + horizontal-align=""> + <div class="dropdown-content"> + <template + is="dom-repeat" + items="[[_fileList]]" + as="path" + initial-count="75"> + <a href$="[[_computeDiffURL(_changeNum, _patchRange.*, path)]]" + selected$="[[_computeFileSelected(path, _path)]]" + data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]" + on-tap="_handleFileTap">[[_computeFileDisplayName(path)]]</a> + </template> + </div> + </iron-dropdown> + </div> + <div class="mobileJumpToFileContainer mobile"> + <select on-change="_handleMobileSelectChange"> + <template is="dom-repeat" items="[[_fileList]]" as="path"> + <option + value$="[[path]]" + selected$="[[_computeFileSelected(path, _path)]]"> + [[_computeTruncatedFileDisplayName(path)]] + </option> </template> - </div> - </iron-dropdown> + </select> + </div> + </h3> + <div class="navLinks desktop"> + <a class="navLink" + href$="[[_computeNavLinkURL(_path, _fileList, -1, 1)]]">Prev</a> + / + <a class="navLink" + href$="[[_computeUpURL(_changeNum, _patchRange, _change, _change.revisions)]]">Up</a> + / + <a class="navLink" + href$="[[_computeNavLinkURL(_path, _fileList, 1, 1)]]">Next</a> </div> - <div class="mobileJumpToFileContainer mobile"> - <select on-change="_handleMobileSelectChange"> - <template is="dom-repeat" items="[[_fileList]]" as="path"> - <option - value$="[[path]]" - selected$="[[_computeFileSelected(path, _path)]]"> - [[_computeTruncatedFileDisplayName(path)]] - </option> - </template> - </select> - </div> - </h3> - <div class="navLinks desktop"> - <a class="navLink" - href$="[[_computeNavLinkURL(_path, _fileList, -1, 1)]]">Prev</a> - / - <a class="navLink" - href$="[[_computeUpURL(_changeNum, _patchRange, _change, _change.revisions)]]">Up</a> - / - <a class="navLink" - href$="[[_computeNavLinkURL(_path, _fileList, 1, 1)]]">Next</a> - </div> - </header> - <div class="loading" hidden$="[[!_loading]]">Loading...</div> - <div hidden$="[[_loading]]" hidden> + </header> <div class="subHeader"> <div class="patchRangeLeft"> <gr-patch-range-select @@ -265,7 +280,8 @@ <option value="SIDE_BY_SIDE">Side By Side</option> <option value="UNIFIED_DIFF">Unified</option> </select> - <span hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]"> + <span id="diffPrefsContainer" + hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]" hidden> <span class="preferences desktop"> <span hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">/</span> @@ -276,37 +292,35 @@ </span> </div> </div> - <gr-overlay id="prefsOverlay" with-backdrop> - <gr-diff-preferences - id="diffPreferences" - prefs="{{_prefs}}" - local-prefs="{{_localPrefs}}" - on-save="_handlePrefsSave" - on-cancel="_handlePrefsCancel"></gr-diff-preferences> - </gr-overlay> <div class="fileNav mobile"> <a class="mobileNavLink" - href$="[[_computeNavLinkURL(_path, _fileList, -1, 1)]]"><</a> + href$="[[_computeNavLinkURL(_path, _fileList, -1, 1)]]"><</a> <div class="fullFileName mobile">[[_computeFileDisplayName(_path)]] </div> <a class="mobileNavLink" - href$="[[_computeNavLinkURL(_path, _fileList, 1, 1)]]">></a> + href$="[[_computeNavLinkURL(_path, _fileList, 1, 1)]]">></a> </div> - <gr-diff - id="diff" - project="[[_change.project]]" - commit="[[_change.current_revision]]" - is-image-diff="{{_isImageDiff}}" - files-weblinks="{{_filesWeblinks}}" - change-num="[[_changeNum]]" - patch-range="[[_patchRange]]" - path="[[_path]]" - prefs="[[_prefs]]" - project-config="[[_projectConfig]]" - view-mode="[[_diffMode]]" - on-line-selected="_onLineSelected"> - </gr-diff> - </div> + </gr-fixed-panel> + <div class="loading" hidden$="[[!_loading]]">Loading...</div> + <gr-diff + id="diff" + hidden + hidden$="[[_loading]]" + class$="[[_computeDiffClass(_panelFloatingDisabled)]]" + is-image-diff="{{_isImageDiff}}" + files-weblinks="{{_filesWeblinks}}" + change-num="[[_changeNum]]" + patch-range="[[_patchRange]]" + path="[[_path]]" + prefs="[[_prefs]]" + project-config="[[_projectConfig]]" + view-mode="[[_diffMode]]" + on-line-selected="_onLineSelected"> + </gr-diff> + <gr-diff-preferences + id="diffPreferences" + prefs="{{_prefs}}" + local-prefs="{{_localPrefs}}"></gr-diff-preferences> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-storage id="storage"></gr-storage> <gr-diff-cursor id="cursor"></gr-diff-cursor>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js index d1ac842..c34f00f 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -14,17 +14,17 @@ (function() { 'use strict'; - var COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; - var MERGE_LIST_PATH = '/MERGE_LIST'; + const COMMIT_MESSAGE_PATH = '/COMMIT_MSG'; + const MERGE_LIST_PATH = '/MERGE_LIST'; - var COMMENT_SAVE = 'Try again when all comments have saved.'; + const COMMENT_SAVE = 'Try again when all comments have saved.'; - var DiffSides = { + const DiffSides = { LEFT: 'left', RIGHT: 'right', }; - var HASH_PATTERN = /^[ab]?\d+$/; + const HASH_PATTERN = /^[ab]?\d+$/; Polymer({ is: 'gr-diff-view', @@ -51,12 +51,13 @@ }, keyEventTarget: { type: Object, - value: function() { return document.body; }, + value() { return document.body; }, }, changeViewState: { type: Object, notify: true, - value: function() { return {}; }, + value() { return {}; }, + observer: '_changeViewStatehanged', }, _patchRange: Object, @@ -65,7 +66,7 @@ _diff: Object, _fileList: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _path: { type: String, @@ -104,6 +105,10 @@ type: Object, computed: '_computeCommentSkips(_commentMap, _fileList, _path)', }, + _panelFloatingDisabled: { + type: Boolean, + value: () => { return window.PANEL_FLOATING_DISABLED; }, + }, }, behaviors: [ @@ -116,6 +121,7 @@ observers: [ '_getProjectConfig(_change.project)', '_getFiles(_changeNum, _patchRange.*)', + '_setReviewedObserver(_loggedIn, params.*)', ], keyBindings: { @@ -134,85 +140,71 @@ ',': '_handleCommaKey', }, - attached: function() { - this._getLoggedIn().then(function(loggedIn) { + attached() { + this._getLoggedIn().then(loggedIn => { this._loggedIn = loggedIn; - if (loggedIn) { - this._setReviewed(true); - } - }.bind(this)); - if (this.changeViewState.diffMode === null) { - // If screen size is small, always default to unified view. - this.$.restAPI.getPreferences().then(function(prefs) { - this.set('changeViewState.diffMode', prefs.default_diff_view); - }.bind(this)); - } - - if (this._path) { - this.fire('title-change', - {title: this._computeFileDisplayName(this._path)}); - } + }); this.$.cursor.push('diffs', this.$.diff); }, - _getLoggedIn: function() { + _getLoggedIn() { return this.$.restAPI.getLoggedIn(); }, - _getProjectConfig: function(project) { + _getProjectConfig(project) { return this.$.restAPI.getProjectConfig(project).then( - function(config) { + config => { this._projectConfig = config; - }.bind(this)); + }); }, - _getChangeDetail: function(changeNum) { + _getChangeDetail(changeNum) { return this.$.restAPI.getDiffChangeDetail(changeNum).then( - function(change) { + change => { this._change = change; - }.bind(this)); + }); }, - _getFiles: function(changeNum, patchRangeRecord) { - var patchRange = patchRangeRecord.base; + _getFiles(changeNum, patchRangeRecord) { + const patchRange = patchRangeRecord.base; return this.$.restAPI.getChangeFilePathsAsSpeciallySortedArray( - changeNum, patchRange).then(function(files) { + changeNum, patchRange).then(files => { this._fileList = files; - }.bind(this)); + }); }, - _getDiffPreferences: function() { + _getDiffPreferences() { return this.$.restAPI.getDiffPreferences(); }, - _getPreferences: function() { + _getPreferences() { return this.$.restAPI.getPreferences(); }, - _getWindowWidth: function() { + _getWindowWidth() { return window.innerWidth; }, - _handleReviewedChange: function(e) { + _handleReviewedChange(e) { this._setReviewed(Polymer.dom(e).rootTarget.checked); }, - _setReviewed: function(reviewed) { + _setReviewed(reviewed) { this.$.reviewed.checked = reviewed; - this._saveReviewedState(reviewed).catch(function(err) { + this._saveReviewedState(reviewed).catch(err => { alert('Couldn’t change file review status. Check the console ' + 'and contact the PolyGerrit team for assistance.'); throw err; - }.bind(this)); + }); }, - _saveReviewedState: function(reviewed) { + _saveReviewedState(reviewed) { return this.$.restAPI.saveFileReviewed(this._changeNum, this._patchRange.patchNum, this._path, reviewed); }, - _handleEscKey: function(e) { + _handleEscKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -220,21 +212,21 @@ this.$.diff.displayLine = false; }, - _handleShiftLeftKey: function(e) { + _handleShiftLeftKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); this.$.cursor.moveLeft(); }, - _handleShiftRightKey: function(e) { + _handleShiftRightKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); this.$.cursor.moveRight(); }, - _handleUpKey: function(e) { + _handleUpKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } if (e.detail.keyboardEvent.shiftKey && e.detail.keyboardEvent.keyCode === 75) { // 'K' @@ -248,7 +240,7 @@ this.$.cursor.moveUp(); }, - _handleDownKey: function(e) { + _handleDownKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } if (e.detail.keyboardEvent.shiftKey && e.detail.keyboardEvent.keyCode === 74) { // 'J' @@ -262,49 +254,51 @@ this.$.cursor.moveDown(); }, - _moveToPreviousFileWithComment: function() { + _moveToPreviousFileWithComment() { if (this._commentSkips && this._commentSkips.previous) { page.show(this._getDiffURL(this._changeNum, this._patchRange, this._commentSkips.previous)); } }, - _moveToNextFileWithComment: function() { + _moveToNextFileWithComment() { if (this._commentSkips && this._commentSkips.next) { page.show(this._getDiffURL(this._changeNum, this._patchRange, this._commentSkips.next)); } }, - _handleCKey: function(e) { + _handleCKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } if (this.$.diff.isRangeSelected()) { return; } if (this.modifierPressed(e)) { return; } e.preventDefault(); - var line = this.$.cursor.getTargetLineElement(); + const line = this.$.cursor.getTargetLineElement(); if (line) { this.$.diff.addDraftAtLine(line); } }, - _handleLeftBracketKey: function(e) { + _handleLeftBracketKey(e) { + // Check for meta key to avoid overriding native chrome shortcut. if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } + this.getKeyboardEvent(e).metaKey) { return; } e.preventDefault(); this._navToFile(this._path, this._fileList, -1); }, - _handleRightBracketKey: function(e) { + _handleRightBracketKey(e) { + // Check for meta key to avoid overriding native chrome shortcut. if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } + this.getKeyboardEvent(e).metaKey) { return; } e.preventDefault(); this._navToFile(this._path, this._fileList, 1); }, - _handleNKey: function(e) { + _handleNKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); @@ -316,7 +310,7 @@ } }, - _handlePKey: function(e) { + _handlePKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } e.preventDefault(); @@ -328,7 +322,7 @@ } }, - _handleAKey: function(e) { + _handleAKey(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } if (e.detail.keyboardEvent.shiftKey) { // Hide left diff. @@ -351,7 +345,7 @@ this._navToChangeView(); }, - _handleUKey: function(e) { + _handleUKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -359,15 +353,15 @@ this._navToChangeView(); }, - _handleCommaKey: function(e) { + _handleCommaKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } e.preventDefault(); - this._openPrefs(); + this.$.diffPreferences.open(); }, - _navToChangeView: function() { + _navToChangeView() { if (!this._changeNum || !this._patchRange.patchNum) { return; } page.show(this._getChangePath( @@ -376,29 +370,20 @@ this._change && this._change.revisions)); }, - _computeUpURL: function(changeNum, patchRange, change, changeRevisions) { + _computeUpURL(changeNum, patchRange, change, changeRevisions) { return this._getChangePath( changeNum, patchRange, change && changeRevisions); }, - _navToFile: function(path, fileList, direction) { - var url = this._computeNavLinkURL(path, fileList, direction); + _navToFile(path, fileList, direction) { + const url = this._computeNavLinkURL(path, fileList, direction); if (!url) { return; } page.show(this._computeNavLinkURL(path, fileList, direction)); }, - _openPrefs: function() { - this.$.prefsOverlay.open().then(function() { - var diffPreferences = this.$.diffPreferences; - var focusStops = diffPreferences.getFocusStops(); - this.$.prefsOverlay.setFocusStops(focusStops); - this.$.diffPreferences.resetFocus(); - }.bind(this)); - }, - /** * @param {?string} path The path of the current file being shown. * @param {Array.<string>} fileList The list of files in this change and @@ -410,12 +395,14 @@ * @return {?string} The next URL when proceeding in the specified * direction. */ - _computeNavLinkURL: function(path, fileList, direction, opt_noUp) { + _computeNavLinkURL(path, fileList, direction, opt_noUp) { if (!path || fileList.length === 0) { return null; } - var idx = fileList.indexOf(path); + let idx = fileList.indexOf(path); if (idx === -1) { - var file = direction > 0 ? fileList[0] : fileList[fileList.length - 1]; + const file = direction > 0 ? + fileList[0] : + fileList[fileList.length - 1]; return this._getDiffURL(this._changeNum, this._patchRange, file); } @@ -432,7 +419,7 @@ return this._getDiffURL(this._changeNum, this._patchRange, fileList[idx]); }, - _paramsChanged: function(value) { + _paramsChanged(value) { if (value.view != this.tagName.toLowerCase()) { return; } this._loadHash(location.hash); @@ -454,33 +441,48 @@ return; } - var promises = []; + const promises = []; this._localPrefs = this.$.storage.getPreferences(); - promises.push(this._getDiffPreferences().then(function(prefs) { + promises.push(this._getDiffPreferences().then(prefs => { this._prefs = prefs; - }.bind(this))); + })); - promises.push(this._getPreferences().then(function(prefs) { + promises.push(this._getPreferences().then(prefs => { this._userPrefs = prefs; - }.bind(this))); + })); promises.push(this._getChangeDetail(this._changeNum)); - Promise.all(promises).then(function() { + Promise.all(promises).then(() => { this._loading = false; this.$.diff.reload(); - }.bind(this)); + }); - this._loadCommentMap().then(function(commentMap) { + this._loadCommentMap().then(commentMap => { this._commentMap = commentMap; - }.bind(this)); + }); + }, + + _changeViewStatehanged(changeViewState) { + if (changeViewState.diffMode === null) { + // If screen size is small, always default to unified view. + this.$.restAPI.getPreferences().then(prefs => { + this.set('changeViewState.diffMode', prefs.default_diff_view); + }); + } + }, + + _setReviewedObserver(_loggedIn) { + if (_loggedIn) { + this._setReviewed(true); + } }, /** * If the URL hash is a diff address then configure the diff cursor. */ - _loadHash: function(hash) { + _loadHash(hash) { hash = hash.replace(/^#/, ''); if (!HASH_PATTERN.test(hash)) { return; } if (hash[0] === 'a' || hash[0] === 'b') { @@ -492,28 +494,29 @@ this.$.cursor.initialLineNumber = parseInt(hash, 10); }, - _pathChanged: function(path) { + _pathChanged(path) { + if (path) { + this.fire('title-change', + {title: this._computeFileDisplayName(path)}); + } + if (this._fileList.length == 0) { return; } this.set('changeViewState.selectedFileIndex', this._fileList.indexOf(path)); - - if (this._loggedIn) { - this._setReviewed(true); - } }, - _getDiffURL: function(changeNum, patchRange, path) { + _getDiffURL(changeNum, patchRange, path) { return this.getBaseUrl() + '/c/' + changeNum + '/' + this._patchRangeStr(patchRange) + '/' + this.encodeURL(path, true); }, - _computeDiffURL: function(changeNum, patchRangeRecord, path) { + _computeDiffURL(changeNum, patchRangeRecord, path) { return this._getDiffURL(changeNum, patchRangeRecord.base, path); }, - _patchRangeStr: function(patchRange) { - var patchStr = patchRange.patchNum; + _patchRangeStr(patchRange) { + let patchStr = patchRange.patchNum; if (patchRange.basePatchNum != null && patchRange.basePatchNum != 'PARENT') { patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum; @@ -521,25 +524,26 @@ return patchStr; }, - _computeAvailablePatches: function(revisions) { - var patchNums = []; - for (var rev in revisions) { - patchNums.push(revisions[rev]._number); + _computeAvailablePatches(revisions) { + const patchNums = []; + if (!revisions) { return patchNums; } + for (const rev of Object.values(revisions)) { + patchNums.push(rev._number); } - return patchNums.sort(function(a, b) { return a - b; }); + return patchNums.sort((a, b) => { return a - b; }); }, - _getChangePath: function(changeNum, patchRange, revisions) { - var base = this.getBaseUrl() + '/c/' + changeNum + '/'; + _getChangePath(changeNum, patchRange, revisions) { + const base = this.getBaseUrl() + '/c/' + changeNum + '/'; // The change may not have loaded yet, making revisions unavailable. if (!revisions) { return base + this._patchRangeStr(patchRange); } - var latestPatchNum = -1; - for (var rev in revisions) { - latestPatchNum = Math.max(latestPatchNum, revisions[rev]._number); + let latestPatchNum = -1; + for (const rev of Object.values(revisions)) { + latestPatchNum = Math.max(latestPatchNum, rev._number); } if (patchRange.basePatchNum !== 'PARENT' || parseInt(patchRange.patchNum, 10) !== latestPatchNum) { @@ -549,11 +553,11 @@ return base; }, - _computeChangePath: function(changeNum, patchRangeRecord, revisions) { + _computeChangePath(changeNum, patchRangeRecord, revisions) { return this._getChangePath(changeNum, patchRangeRecord.base, revisions); }, - _computeFileDisplayName: function(path) { + _computeFileDisplayName(path) { if (path === COMMIT_MESSAGE_PATH) { return 'Commit message'; } else if (path === MERGE_LIST_PATH) { @@ -562,20 +566,20 @@ return path; }, - _computeTruncatedFileDisplayName: function(path) { + _computeTruncatedFileDisplayName(path) { return util.truncatePath(this._computeFileDisplayName(path)); }, - _computeFileSelected: function(path, currentPath) { + _computeFileSelected(path, currentPath) { return path == currentPath; }, - _computePrefsButtonHidden: function(prefs, loggedIn) { + _computePrefsButtonHidden(prefs, loggedIn) { return !loggedIn || !prefs; }, - _computeKeyNav: function(path, selectedPath, fileList) { - var selectedIndex = fileList.indexOf(selectedPath); + _computeKeyNav(path, selectedPath, fileList) { + const selectedIndex = fileList.indexOf(selectedPath); if (fileList.indexOf(path) == selectedIndex - 1) { return '['; } @@ -585,46 +589,41 @@ return ''; }, - _handleFileTap: function(e) { - this.$.dropdown.close(); + _handleFileTap(e) { + // async is needed so that that the click event is fired before the + // dropdown closes (This was a bug for touch devices). + this.async(() => { + this.$.dropdown.close(); + }, 1); }, - _handleMobileSelectChange: function(e) { - var path = Polymer.dom(e).rootTarget.value; + _handleMobileSelectChange(e) { + const path = Polymer.dom(e).rootTarget.value; page.show(this._getDiffURL(this._changeNum, this._patchRange, path)); }, - _showDropdownTapHandler: function(e) { + _showDropdownTapHandler(e) { this.$.dropdown.open(); }, - _handlePrefsTap: function(e) { + _handlePrefsTap(e) { e.preventDefault(); - this._openPrefs(); + this.$.diffPreferences.open(); }, - _handlePrefsSave: function(e) { + _handlePrefsSave(e) { e.stopPropagation(); - var el = Polymer.dom(e).rootTarget; + const el = Polymer.dom(e).rootTarget; el.disabled = true; this.$.storage.savePreferences(this._localPrefs); - this._saveDiffPreferences().then(function(response) { + this._saveDiffPreferences().then(response => { el.disabled = false; if (!response.ok) { return response; } this.$.prefsOverlay.close(); - }.bind(this)).catch(function(err) { + }).catch(err => { el.disabled = false; - }.bind(this)); - }, - - _saveDiffPreferences: function() { - return this.$.restAPI.saveDiffPreferences(this._prefs); - }, - - _handlePrefsCancel: function(e) { - e.stopPropagation(); - this.$.prefsOverlay.close(); + }); }, /** @@ -641,7 +640,7 @@ * * @return {String} */ - _getDiffViewMode: function() { + _getDiffViewMode() { if (this.changeViewState.diffMode) { return this.changeViewState.diffMode; } else if (this._userPrefs) { @@ -652,17 +651,17 @@ } }, - _computeModeSelectHidden: function() { + _computeModeSelectHidden() { return this._isImageDiff; }, - _onLineSelected: function(e, detail) { + _onLineSelected(e, detail) { this.$.cursor.moveToLineNumber(detail.number, detail.side); history.replaceState(null, null, '#' + this.$.cursor.getAddress()); }, - _computeDownloadLink: function(changeNum, patchRange, path) { - var url = this.changeBaseURL(changeNum, patchRange.patchNum); + _computeDownloadLink(changeNum, patchRange, path) { + let url = this.changeBaseURL(changeNum, patchRange.patchNum); url += '/patch?zip&path=' + encodeURIComponent(path); return url; }, @@ -673,9 +672,9 @@ * current patch range. * @return {Promise} A promise that yields a comment map object. */ - _loadCommentMap: function() { - function filterByRange(comment) { - var patchNum = comment.patch_set + ''; + _loadCommentMap() { + const filterByRange = comment => { + const patchNum = comment.patch_set + ''; return patchNum === this._patchRange.patchNum || patchNum === this._patchRange.basePatchNum; }; @@ -684,34 +683,34 @@ this.$.restAPI.getDiffComments(this._changeNum), this._getDiffDrafts(), this.$.restAPI.getDiffRobotComments(this._changeNum), - ]).then(function(results) { - var commentMap = {}; - results.forEach(function(response) { - for (var path in response) { + ]).then(results => { + const commentMap = {}; + for (const response of results) { + for (const path in response) { if (response.hasOwnProperty(path) && - response[path].filter(filterByRange.bind(this)).length) { + response[path].filter(filterByRange).length) { commentMap[path] = true; } } - }.bind(this)); + } return commentMap; - }.bind(this)); + }); }, - _getDiffDrafts: function() { - return this._getLoggedIn().then(function(loggedIn) { + _getDiffDrafts() { + return this._getLoggedIn().then(loggedIn => { if (!loggedIn) { return Promise.resolve({}); } return this.$.restAPI.getDiffDrafts(this._changeNum); - }.bind(this)); + }); }, - _computeCommentSkips: function(commentMap, fileList, path) { - var skips = {previous: null, next: null}; + _computeCommentSkips(commentMap, fileList, path) { + const skips = {previous: null, next: null}; if (!fileList.length) { return skips; } - var pathIndex = fileList.indexOf(path); + const pathIndex = fileList.indexOf(path); // Scan backward for the previous file. - for (var i = pathIndex - 1; i >= 0; i--) { + for (let i = pathIndex - 1; i >= 0; i--) { if (commentMap[fileList[i]]) { skips.previous = fileList[i]; break; @@ -719,7 +718,7 @@ } // Scan forward for the next file. - for (i = pathIndex + 1; i < fileList.length; i++) { + for (let i = pathIndex + 1; i < fileList.length; i++) { if (commentMap[fileList[i]]) { skips.next = fileList[i]; break; @@ -728,5 +727,11 @@ return skips; }, + + _computeDiffClass(panelFloatingDisabled) { + if (panelFloatingDisabled) { + return 'noOverflow'; + } + }, }); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html index d1bcd60..0a7ed28 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -20,10 +20,10 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../bower_components/page/page.js"></script> <script src="../../../scripts/util.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-diff-view.html"> <script>void(0);</script> @@ -41,34 +41,36 @@ </test-fixture> <script> - suite('gr-diff-view tests', function() { - var element; - var sandbox; + suite('gr-diff-view tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(false); }, - getProjectConfig: function() { return Promise.resolve({}); }, - getDiffChangeDetail: function() { return Promise.resolve(null); }, - getChangeFiles: function() { return Promise.resolve({}); }, - saveFileReviewed: function() { return Promise.resolve(); }, + getLoggedIn() { return Promise.resolve(false); }, + getProjectConfig() { return Promise.resolve({}); }, + getDiffChangeDetail() { return Promise.resolve(null); }, + getChangeFiles() { return Promise.resolve({}); }, + saveFileReviewed() { return Promise.resolve(); }, + getDiffRobotComments() { return Promise.resolve(); }, + getDiffDrafts() { return Promise.resolve(); }, }); element = fixture('basic'); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('toggle left diff with a hotkey', function() { - var toggleLeftDiffStub = sandbox.stub(element.$.diff, 'toggleLeftDiff'); + test('toggle left diff with a hotkey', () => { + const toggleLeftDiffStub = sandbox.stub(element.$.diff, 'toggleLeftDiff'); MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a'); assert.isTrue(toggleLeftDiffStub.calledOnce); }); - test('keyboard shortcuts', function() { + test('keyboard shortcuts', () => { element._changeNum = '42'; element._patchRange = { basePatchNum: 'PARENT', @@ -83,7 +85,7 @@ element._path = 'glados.txt'; element.changeViewState.selectedFileIndex = 1; - var showStub = sandbox.stub(page, 'show'); + const showStub = sandbox.stub(page, 'show'); MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); assert(showStub.lastCall.calledWithExactly('/c/42/'), 'Should navigate to /c/42/'); @@ -111,13 +113,14 @@ 'Should navigate to /c/42/'); assert.equal(element.changeViewState.selectedFileIndex, 0); - var showPrefsStub = sandbox.stub(element.$.prefsOverlay, 'open', - function() { return Promise.resolve({}); }); + const showPrefsStub = + sandbox.stub(element.$.diffPreferences.$.prefsOverlay, 'open', + () => Promise.resolve()); MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); assert(showPrefsStub.calledOnce); - var scrollStub = sandbox.stub(element.$.cursor, 'moveToNextChunk'); + let scrollStub = sandbox.stub(element.$.cursor, 'moveToNextChunk'); MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n'); assert(scrollStub.calledOnce); @@ -134,7 +137,7 @@ MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p'); assert(scrollStub.calledOnce); - var computeContainerClassStub = sandbox.stub(element.$.diff, + const computeContainerClassStub = sandbox.stub(element.$.diff, '_computeContainerClass'); MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); assert(computeContainerClassStub.lastCall.calledWithExactly( @@ -145,23 +148,7 @@ false, 'SIDE_BY_SIDE', false)); }); - test('saving diff preferences', function() { - var savePrefs = sandbox.stub(element, '_handlePrefsSave'); - var cancelPrefs = sandbox.stub(element, '_handlePrefsCancel'); - element.$.diffPreferences._handleSave(); - assert(savePrefs.calledOnce); - assert(cancelPrefs.notCalled); - }); - - test('cancelling diff preferences', function() { - var savePrefs = sandbox.stub(element, '_handlePrefsSave'); - var cancelPrefs = sandbox.stub(element, '_handlePrefsCancel'); - element.$.diffPreferences._handleCancel(); - assert(cancelPrefs.calledOnce); - assert(savePrefs.notCalled); - }); - - test('keyboard shortcuts with patch range', function() { + test('keyboard shortcuts with patch range', () => { element._changeNum = '42'; element._patchRange = { basePatchNum: '5', @@ -175,7 +162,7 @@ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; element._path = 'glados.txt'; - var showStub = sandbox.stub(page, 'show'); + const showStub = sandbox.stub(page, 'show'); MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' + @@ -214,7 +201,7 @@ 'Should navigate to /c/42/5..10'); }); - test('keyboard shortcuts with old patch number', function() { + test('keyboard shortcuts with old patch number', () => { element._changeNum = '42'; element._patchRange = { basePatchNum: 'PARENT', @@ -229,7 +216,7 @@ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; element._path = 'glados.txt'; - var showStub = sandbox.stub(page, 'show'); + const showStub = sandbox.stub(page, 'show'); MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' + @@ -268,7 +255,39 @@ 'Should navigate to /c/42/1'); }); - test('go up to change via kb without change loaded', function() { + test('Diff preferences hidden when no prefs or logged out', () => { + element._loggedIn = false; + flushAsynchronousOperations(); + assert.isTrue(element.$.diffPrefsContainer.hidden); + + element._loggedIn = true; + flushAsynchronousOperations(); + assert.isTrue(element.$.diffPrefsContainer.hidden); + + element._loggedIn = false; + element._prefs = {font_size: '12'}; + flushAsynchronousOperations(); + assert.isTrue(element.$.diffPrefsContainer.hidden); + + element._loggedIn = true; + flushAsynchronousOperations(); + assert.isFalse(element.$.diffPrefsContainer.hidden); + }); + + test('prefsButton opens gr-diff-preferences', () => { + const handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap'); + const overlayOpenStub = sandbox.stub(element.$.diffPreferences, + 'open'); + const prefsButton = + Polymer.dom(element.root).querySelector('.prefsButton'); + + MockInteractions.tap(prefsButton); + + assert.isTrue(handlePrefsTapSpy.called); + assert.isTrue(overlayOpenStub.called); + }); + + test('go up to change via kb without change loaded', () => { element._changeNum = '42'; element._patchRange = { basePatchNum: 'PARENT', @@ -278,7 +297,7 @@ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; element._path = 'glados.txt'; - var showStub = sandbox.stub(page, 'show'); + const showStub = sandbox.stub(page, 'show'); MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); assert.isTrue(showStub.notCalled, 'The `a` keyboard shortcut should ' + @@ -317,7 +336,7 @@ 'Should navigate to /c/42/1'); }); - test('jump to file dropdown', function() { + test('jump to file dropdown', () => { element._changeNum = '42'; element._patchRange = { basePatchNum: 'PARENT', @@ -326,7 +345,7 @@ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; element._path = 'glados.txt'; flushAsynchronousOperations(); - var linkEls = + const linkEls = Polymer.dom(element.root).querySelectorAll('.dropdown-content > a'); assert.equal(linkEls.length, 3); assert.isFalse(linkEls[0].hasAttribute('selected')); @@ -347,7 +366,7 @@ 'Merge list'); }); - test('jump to file dropdown with patch range', function() { + test('jump to file dropdown with patch range', () => { element._changeNum = '42'; element._patchRange = { basePatchNum: '5', @@ -356,7 +375,7 @@ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; element._path = 'glados.txt'; flushAsynchronousOperations(); - var linkEls = + const linkEls = Polymer.dom(element.root).querySelectorAll('.dropdown-content > a'); assert.equal(linkEls.length, 3); assert.isFalse(linkEls[0].hasAttribute('selected')); @@ -370,7 +389,7 @@ assert.equal(linkEls[2].getAttribute('href'), '/c/42/5..10/wheatley.md'); }); - test('prev/up/next links', function() { + test('prev/up/next links', () => { element._changeNum = '42'; element._patchRange = { basePatchNum: 'PARENT', @@ -384,7 +403,7 @@ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; element._path = 'glados.txt'; flushAsynchronousOperations(); - var linkEls = Polymer.dom(element.root).querySelectorAll('.navLink'); + const linkEls = Polymer.dom(element.root).querySelectorAll('.navLink'); assert.equal(linkEls.length, 3); assert.equal(linkEls[0].getAttribute('href'), '/c/42/10/chell.go'); assert.equal(linkEls[1].getAttribute('href'), '/c/42/'); @@ -406,7 +425,7 @@ assert.equal(linkEls[2].getAttribute('href'), '/c/42/10/chell.go'); }); - test('download link', function() { + test('download link', () => { element._changeNum = '42'; element._patchRange = { basePatchNum: 'PARENT', @@ -419,7 +438,7 @@ '/changes/42/revisions/10/patch?zip&path=glados.txt'); }); - test('prev/up/next links with patch range', function() { + test('prev/up/next links with patch range', () => { element._changeNum = '42'; element._patchRange = { basePatchNum: '5', @@ -434,7 +453,7 @@ element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; element._path = 'glados.txt'; flushAsynchronousOperations(); - var linkEls = Polymer.dom(element.root).querySelectorAll('.navLink'); + const linkEls = Polymer.dom(element.root).querySelectorAll('.navLink'); assert.equal(linkEls.length, 3); assert.equal(linkEls[0].getAttribute('href'), '/c/42/5..10/chell.go'); assert.equal(linkEls[1].getAttribute('href'), '/c/42/5..10'); @@ -451,20 +470,25 @@ assert.equal(linkEls[2].getAttribute('href'), '/c/42/5..10/glados.txt'); }); - test('file review status', function(done) { - element._loggedIn = true; - element._changeNum = '42'; - element._patchRange = { - basePatchNum: '1', - patchNum: '2', - }; - element._fileList = ['/COMMIT_MSG']; - element._path = '/COMMIT_MSG'; - var saveReviewedStub = sandbox.stub(element, '_saveReviewedState', - function() { return Promise.resolve(); }); + test('file review status', done => { + stub('gr-rest-api-interface', { + getDiffComments() { return Promise.resolve({}); }, + }); + const saveReviewedStub = sandbox.stub(element, '_saveReviewedState', + () => Promise.resolve()); + sandbox.stub(element.$.diff, 'reload'); - flush(function() { - var commitMsg = Polymer.dom(element.root).querySelector( + element._loggedIn = true; + element.params = { + view: 'gr-diff-view', + changeNum: '42', + patchNum: '2', + basePatchNum: '1', + path: '/COMMIT_MSG', + }; + + flush(() => { + const commitMsg = Polymer.dom(element.root).querySelector( 'input[type="checkbox"]'); assert.isTrue(commitMsg.checked); @@ -480,9 +504,9 @@ }); }); - test('diff mode selector correctly toggles the diff', function() { - var select = element.$.modeSelect; - var diffDisplay = element.$.diff; + test('diff mode selector correctly toggles the diff', () => { + const select = element.$.modeSelect; + const diffDisplay = element.$.diff; element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'}; // The mode selected in the view state reflects the selected option. @@ -493,7 +517,7 @@ assert.equal(select.value, diffDisplay.viewMode); // We will simulate a user change of the selected mode. - var newMode = 'UNIFIED_DIFF'; + const newMode = 'UNIFIED_DIFF'; // Set the actual value of the select, and simulate the change event. select.value = newMode; element.fire('change', {}, {node: select}); @@ -504,17 +528,16 @@ assert.equal(element._getDiffViewMode(), diffDisplay.viewMode); }); - test('diff mode selector initializes from preferences', function() { - var resolvePrefs; - var prefsPromise = new Promise(function(resolve) { + test('diff mode selector initializes from preferences', () => { + let resolvePrefs; + const prefsPromise = new Promise(resolve => { resolvePrefs = resolve; }); - var getPreferencesStub = sandbox.stub(element.$.restAPI, 'getPreferences', - function() { return prefsPromise; }); + sandbox.stub(element.$.restAPI, 'getPreferences', () => prefsPromise); // Attach a new gr-diff-view so we can intercept the preferences fetch. - var view = document.createElement('gr-diff-view'); - var select = view.$.modeSelect; + const view = document.createElement('gr-diff-view'); + const select = view.$.modeSelect; fixture('blank').appendChild(view); flushAsynchronousOperations(); @@ -527,7 +550,7 @@ assert.equal(select.value, 'SIDE_BY_SIDE'); }); - test('_loadHash', function() { + test('_loadHash', () => { assert.isNotOk(element.$.cursor.initialLineNumber); // Ignores invalid hashes: @@ -550,32 +573,31 @@ assert.equal(element.$.cursor.side, 'left'); }); - test('_shortenPath with long path should add ellipsis', function() { - var path = - 'level1/level2/level3/level4/file.js'; - var shortenedPath = util.truncatePath(path); + test('_shortenPath with long path should add ellipsis', () => { + let path = 'level1/level2/level3/level4/file.js'; + let shortenedPath = util.truncatePath(path); // The expected path is truncated with an ellipsis. - var expectedPath = '\u2026/file.js'; + const expectedPath = '\u2026/file.js'; assert.equal(shortenedPath, expectedPath); - var path = 'level2/file.js'; - var shortenedPath = util.truncatePath(path); + path = 'level2/file.js'; + shortenedPath = util.truncatePath(path); assert.equal(shortenedPath, expectedPath); }); - test('_shortenPath with short path should not add ellipsis', function() { - var path = 'file.js'; - var expectedPath = 'file.js'; - var shortenedPath = util.truncatePath(path); + test('_shortenPath with short path should not add ellipsis', () => { + const path = 'file.js'; + const expectedPath = 'file.js'; + const shortenedPath = util.truncatePath(path); assert.equal(shortenedPath, expectedPath); }); - test('_onLineSelected', function() { - var replaceStateStub = sandbox.stub(history, 'replaceState'); - var moveStub = sandbox.stub(element.$.cursor, 'moveToLineNumber'); + test('_onLineSelected', () => { + const replaceStateStub = sandbox.stub(history, 'replaceState'); + const moveStub = sandbox.stub(element.$.cursor, 'moveToLineNumber'); - var e = {}; - var detail = {number: 123, side: 'right'}; + const e = {}; + const detail = {number: 123, side: 'right'}; element._onLineSelected(e, detail); @@ -586,15 +608,15 @@ assert.isTrue(replaceStateStub.called); }); - test('_getDiffURL encodes special characters', function() { - var changeNum = 123; - var patchRange = {basePatchNum: 123, patchNum: 456}; - var path = 'c++/cpp.cpp'; + test('_getDiffURL encodes special characters', () => { + const changeNum = 123; + const patchRange = {basePatchNum: 123, patchNum: 456}; + const path = 'c++/cpp.cpp'; assert.equal(element._getDiffURL(changeNum, patchRange, path), '/c/123/123..456/c%252B%252B/cpp.cpp'); }); - test('_getDiffViewMode', function() { + test('_getDiffViewMode', () => { // No user prefs or change view state set. assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); @@ -607,22 +629,20 @@ assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); }); - suite('_loadCommentMap', function() { - test('empty', function(done) { + suite('_loadCommentMap', () => { + test('empty', done => { stub('gr-rest-api-interface', { - getDiffRobotComments: function() { return Promise.resolve({}); }, - getDiffComments: function() { return Promise.resolve({}); }, + getDiffComments() { return Promise.resolve({}); }, }); - element._loadCommentMap().then(function(map) { + element._loadCommentMap().then(map => { assert.equal(Object.keys(map).length, 0); done(); }); }); - test('paths in patch range', function(done) { + test('paths in patch range', done => { stub('gr-rest-api-interface', { - getDiffRobotComments: function() { return Promise.resolve({}); }, - getDiffComments: function() { + getDiffComments() { return Promise.resolve({ 'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}], 'path-to/file/two.py': [{patch_set: 5, message: 'ipsum'}], @@ -634,17 +654,16 @@ basePatchNum: '3', patchNum: '5', }; - element._loadCommentMap().then(function(map) { + element._loadCommentMap().then(map => { assert.deepEqual(Object.keys(map), ['path/to/file/one.cpp', 'path-to/file/two.py']); done(); }); }); - test('empty for paths outside patch range', function(done) { + test('empty for paths outside patch range', done => { stub('gr-rest-api-interface', { - getDiffRobotComments: function() { return Promise.resolve({}); }, - getDiffComments: function() { + getDiffComments() { return Promise.resolve({ 'path/to/file/one.cpp': [{patch_set: 'PARENT', message: 'lorem'}], 'path-to/file/two.py': [{patch_set: 2, message: 'ipsum'}], @@ -656,35 +675,35 @@ basePatchNum: '3', patchNum: '5', }; - element._loadCommentMap().then(function(map) { + element._loadCommentMap().then(map => { assert.equal(Object.keys(map).length, 0); done(); }); }); }); - suite('_computeCommentSkips', function() { - test('empty file list', function() { - var commentMap = { + suite('_computeCommentSkips', () => { + test('empty file list', () => { + const commentMap = { 'path/one.jpg': true, 'path/three.wav': true, }; - var path = 'path/two.m4v'; - var fileList = []; - var result = element._computeCommentSkips(commentMap, fileList, path); + const path = 'path/two.m4v'; + const fileList = []; + const result = element._computeCommentSkips(commentMap, fileList, path); assert.isNull(result.previous); assert.isNull(result.next); }); - test('finds skips', function() { - var fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav']; - var path = fileList[1]; - var commentMap = {}; + test('finds skips', () => { + const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav']; + let path = fileList[1]; + const commentMap = {}; commentMap[fileList[0]] = true; commentMap[fileList[1]] = false; commentMap[fileList[2]] = true; - var result = element._computeCommentSkips(commentMap, fileList, path); + let result = element._computeCommentSkips(commentMap, fileList, path); assert.equal(result.previous, fileList[0]); assert.equal(result.next, fileList[2]);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js index 2dc495a..8d1cef6 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group.js
@@ -22,6 +22,7 @@ this.lines = []; this.adds = []; this.removes = []; + this.dueToRebase = undefined; this.lineRange = { left: {start: null, end: null}, @@ -44,7 +45,7 @@ GrDiffGroup.prototype.addLine = function(line) { this.lines.push(line); - var notDelta = (this.type === GrDiffGroup.Type.BOTH || + const notDelta = (this.type === GrDiffGroup.Type.BOTH || this.type === GrDiffGroup.Type.CONTEXT_CONTROL); if (notDelta && (line.type === GrDiffLine.Type.ADD || line.type === GrDiffLine.Type.REMOVE)) { @@ -62,7 +63,7 @@ GrDiffGroup.prototype.getSideBySidePairs = function() { if (this.type === GrDiffGroup.Type.BOTH || this.type === GrDiffGroup.Type.CONTEXT_CONTROL) { - return this.lines.map(function(line) { + return this.lines.map(line => { return { left: line, right: line, @@ -70,9 +71,9 @@ }); } - var pairs = []; - var i = 0; - var j = 0; + const pairs = []; + let i = 0; + let j = 0; while (i < this.removes.length || j < this.adds.length) { pairs.push({ left: this.removes[i] || GrDiffLine.BLANK_LINE,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html index 563825e..32405cf 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
@@ -19,17 +19,17 @@ <title>gr-diff-group</title> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="gr-diff-line.js"></script> <script src="gr-diff-group.js"></script> <script> - suite('gr-diff-group tests', function() { - - test('delta line pairs', function() { - var group = new GrDiffGroup(GrDiffGroup.Type.DELTA); - var l1 = new GrDiffLine(GrDiffLine.Type.ADD); - var l2 = new GrDiffLine(GrDiffLine.Type.ADD); - var l3 = new GrDiffLine(GrDiffLine.Type.REMOVE); + suite('gr-diff-group tests', () => { + test('delta line pairs', () => { + let group = new GrDiffGroup(GrDiffGroup.Type.DELTA); + const l1 = new GrDiffLine(GrDiffLine.Type.ADD); + const l2 = new GrDiffLine(GrDiffLine.Type.ADD); + const l3 = new GrDiffLine(GrDiffLine.Type.REMOVE); l1.afterNumber = 128; l2.afterNumber = 129; l3.beforeNumber = 64; @@ -44,7 +44,7 @@ right: {start: 128, end: 129}, }); - var pairs = group.getSideBySidePairs(); + let pairs = group.getSideBySidePairs(); assert.deepEqual(pairs, [ {left: l3, right: l1}, {left: GrDiffLine.BLANK_LINE, right: l2}, @@ -62,20 +62,20 @@ ]); }); - test('group/header line pairs', function() { - var l1 = new GrDiffLine(GrDiffLine.Type.BOTH); + test('group/header line pairs', () => { + const l1 = new GrDiffLine(GrDiffLine.Type.BOTH); l1.beforeNumber = 64; l1.afterNumber = 128; - var l2 = new GrDiffLine(GrDiffLine.Type.BOTH); + const l2 = new GrDiffLine(GrDiffLine.Type.BOTH); l2.beforeNumber = 65; l2.afterNumber = 129; - var l3 = new GrDiffLine(GrDiffLine.Type.BOTH); + const l3 = new GrDiffLine(GrDiffLine.Type.BOTH); l3.beforeNumber = 66; l3.afterNumber = 130; - var group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]); + let group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]); assert.deepEqual(group.lines, [l1, l2, l3]); assert.deepEqual(group.adds, []); @@ -86,7 +86,7 @@ right: {start: 128, end: 130}, }); - var pairs = group.getSideBySidePairs(); + let pairs = group.getSideBySidePairs(); assert.deepEqual(pairs, [ {left: l1, right: l1}, {left: l2, right: l2}, @@ -106,12 +106,12 @@ ]); }); - test('adding delta lines to non-delta group', function() { - var l1 = new GrDiffLine(GrDiffLine.Type.ADD); - var l2 = new GrDiffLine(GrDiffLine.Type.REMOVE); - var l3 = new GrDiffLine(GrDiffLine.Type.BOTH); + test('adding delta lines to non-delta group', () => { + const l1 = new GrDiffLine(GrDiffLine.Type.ADD); + const l2 = new GrDiffLine(GrDiffLine.Type.REMOVE); + const l3 = new GrDiffLine(GrDiffLine.Type.BOTH); - var group = new GrDiffGroup(GrDiffGroup.Type.BOTH); + let group = new GrDiffGroup(GrDiffGroup.Type.BOTH); assert.throws(group.addLine.bind(group, l1)); assert.throws(group.addLine.bind(group, l2)); assert.doesNotThrow(group.addLine.bind(group, l3));
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js index 2a5913c..8db0c4f 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-line.js
@@ -43,5 +43,4 @@ GrDiffLine.BLANK_LINE = new GrDiffLine(GrDiffLine.Type.BLANK); window.GrDiffLine = GrDiffLine; - })(window);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html index 54e9c6e..ebbc7e1 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -22,21 +22,27 @@ <link rel="import" href="../gr-diff-highlight/gr-diff-highlight.html"> <link rel="import" href="../gr-diff-selection/gr-diff-selection.html"> <link rel="import" href="../gr-syntax-themes/gr-theme-default.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<script src="../../../scripts/hiddenscroll.js"></script> <dom-module id="gr-diff"> <template> - <style> + <style include="shared-styles"> :host { --light-remove-highlight-color: #fee; --dark-remove-highlight-color: rgba(255, 0, 0, 0.15); --light-add-highlight-color: #efe; --dark-add-highlight-color: rgba(0, 255, 0, 0.15); - + --light-rebased-remove-highlight-color: #fff6ea; + --dark-rebased-remove-highlight-color: rgba(255, 139, 6, 0.15); + --light-rebased-add-highlight-color: #edfffa; + --dark-rebased-add-highlight-color: rgba(11, 255, 155, 0.15); } - :host.no-left .sideBySide ::content .left, - :host.no-left .sideBySide ::content .left + td, - :host.no-left .sideBySide ::content .right:not([data-value]), - :host.no-left .sideBySide ::content .right:not([data-value]) + td { + :host(.no-left) .sideBySide ::content .left, + :host(.no-left) .sideBySide ::content .left + td, + :host(.no-left) .sideBySide ::content .right:not([data-value]), + :host(.no-left) .sideBySide ::content .right:not([data-value]) + td { display: none; } .diffContainer { @@ -44,20 +50,14 @@ border-top: 1px solid #eee; display: flex; font: 12px var(--monospace-font-family); - overflow-x: auto; - will-change: transform; + } + .diffContainer.hiddenscroll { + padding-bottom: .8em; } table { border-collapse: collapse; border-right: 1px solid #ddd; table-layout: fixed; - - /* Hint GPU acceleration */ - -webkit-transform: translateZ(0); - -moz-transform: translateZ(0); - -ms-transform: translateZ(0); - -o-transform: translateZ(0); - transform: translateZ(0); } .lineNum { background-color: #eee; @@ -73,6 +73,9 @@ font-family: var(--font-family); font-style: italic; } + .diff-row { + outline: none; + } .diff-row.target-row.target-side-left .lineNum.left, .diff-row.target-row.target-side-right .lineNum.right, .diff-row.target-row.unified .lineNum { @@ -103,10 +106,11 @@ } .contextLineNum:before, .lineNum:before { + box-sizing: border-box; display: inline-block; color: #666; content: attr(data-value); - padding: 0 .75em; + padding: 0 .5em; text-align: right; width: 100%; } @@ -154,8 +158,8 @@ .contextControl td:not(.lineNum) { text-align: center; } - .displayLine .diff-row.target-row { - border-bottom: 1px solid #bbb; + .displayLine .diff-row.target-row td { + box-shadow: inset 0 -1px #bbb; } .br:after { /* Line feed */ @@ -173,8 +177,34 @@ border-radius: .4em; background-color: #FF9AD2; } + #diffHeader { + background-color: #F9F9F9; + color: #2A00FF; + font-family: var(--monospace-font-family); + font-size: var(--font-size, 12px); + padding: 0.5em 0 0.5em 4em; + } + #sizeWarning { + display: none; + margin: 1em auto; + max-width: 60em; + text-align: center; + } + #sizeWarning gr-button { + margin: 1em; + } + #sizeWarning.warn { + display: block; + } </style> <style include="gr-theme-default"></style> + <div id="diffHeader" hidden$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]"> + <template + is="dom-repeat" + items="[[_diffHeaderItems]]"> + <div>[[item]]</div> + </template> + </div> <div class$="[[_computeContainerClass(_loggedIn, viewMode, displayLine)]]" on-tap="_handleTap"> <gr-diff-selection diff="[[_diff]]"> @@ -186,16 +216,32 @@ id="diffBuilder" comments="[[_comments]]" diff="[[_diff]]" + diff-path="[[path]]" view-mode="[[viewMode]]" line-wrapping="[[lineWrapping]]" is-image-diff="[[isImageDiff]]" base-image="[[_baseImage]]" revision-image="[[_revisionImage]]"> - <table id="diffTable" class$="[[_diffTableClass]]"></table> + <table + id="diffTable" + class$="[[_diffTableClass]]" + role="presentation"></table> </gr-diff-builder> </gr-diff-highlight> </gr-diff-selection> </div> + <div id="sizeWarning" class$="[[_computeWarningClass(_showWarning)]]"> + <p> + Prevented render because "Whole file" is enabled and this diff is very + large (about [[_diffLength(_diff)]] lines). + </p> + <gr-button on-tap="_handleLimitedBypass"> + Render with limited context + </gr-button> + <gr-button on-tap="_handleFullBypass"> + Render anyway (may be slow) + </gr-button> + </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template> <script src="gr-diff-line.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js index 05a7f72..8103cb3 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -14,16 +14,20 @@ (function() { 'use strict'; - var DiffViewMode = { + const DiffViewMode = { SIDE_BY_SIDE: 'SIDE_BY_SIDE', UNIFIED: 'UNIFIED_DIFF', }; - var DiffSide = { + const DiffSide = { LEFT: 'left', RIGHT: 'right', }; + const LARGE_DIFF_THRESHOLD_LINES = 10000; + const FULL_CONTEXT = -1; + const LIMITED_CONTEXT = 10; + Polymer({ is: 'gr-diff', @@ -32,6 +36,12 @@ * @event line-selected */ + /** + * Fired if being logged in is required. + * + * @event show-auth-required + */ + properties: { changeNum: String, noAutoRender: { @@ -48,8 +58,6 @@ type: Object, observer: '_projectConfigChanged', }, - project: String, - commit: String, displayLine: { type: Boolean, value: false, @@ -61,13 +69,14 @@ }, filesWeblinks: { type: Object, - value: function() { return {}; }, + value() { return {}; }, notify: true, }, hidden: { type: Boolean, reflectToAttribute: true, }, + noRenderOnPrefsChange: Boolean, _loggedIn: { type: Boolean, value: false, @@ -83,6 +92,11 @@ observer: '_viewModeObserver', }, _diff: Object, + _diffHeaderItems: { + type: Array, + value: [], + computed: '_computeDiffHeaderItems(_diff.*)', + }, _diffTableClass: { type: String, value: '', @@ -90,6 +104,19 @@ _comments: Object, _baseImage: Object, _revisionImage: Object, + + /** + * Whether the safety check for large diffs when whole-file is set has + * been bypassed. If the value is null, then the safety has not been + * bypassed. If the value is a number, then that number represents the + * context preference to use when rendering the bypassed diff. + */ + _safetyBypass: { + type: Number, + value: null, + }, + + _showWarning: Boolean, }, listeners: { @@ -100,42 +127,44 @@ 'create-comment': '_handleCreateComment', }, - attached: function() { - this._getLoggedIn().then(function(loggedIn) { + attached() { + this._getLoggedIn().then(loggedIn => { this._loggedIn = loggedIn; - }.bind(this)); - + }); }, - ready: function() { + ready() { if (this._canRender()) { this.reload(); } }, - reload: function() { + reload() { + this.$.diffBuilder.cancel(); + this._safetyBypass = null; + this._showWarning = false; this._clearDiffContent(); - var promises = []; + const promises = []; - promises.push(this._getDiff().then(function(diff) { + promises.push(this._getDiff().then(diff => { this._diff = diff; return this._loadDiffAssets(); - }.bind(this))); + })); - promises.push(this._getDiffCommentsAndDrafts().then(function(comments) { + promises.push(this._getDiffCommentsAndDrafts().then(comments => { this._comments = comments; - }.bind(this))); + })); - return Promise.all(promises).then(function() { + return Promise.all(promises).then(() => { if (this.prefs) { return this._renderDiffTable(); } return Promise.resolve(); - }.bind(this)); + }); }, - getCursorStops: function() { + getCursorStops() { if (this.hidden && this.noAutoRender) { return []; } @@ -143,43 +172,46 @@ return Polymer.dom(this.root).querySelectorAll('.diff-row'); }, - addDraftAtLine: function(el) { + addDraftAtLine(el) { this._selectLine(el); - this._getLoggedIn().then(function(loggedIn) { - if (!loggedIn) { return; } + this._getLoggedIn().then(loggedIn => { + if (!loggedIn) { + this.fire('show-auth-required'); + return; + } - var value = el.getAttribute('data-value'); + const value = el.getAttribute('data-value'); if (value === GrDiffLine.FILE) { this._addDraft(el); return; } - var lineNum = parseInt(value, 10); + const lineNum = parseInt(value, 10); if (isNaN(lineNum)) { throw Error('Invalid line number: ' + value); } this._addDraft(el, lineNum); - }.bind(this)); + }); }, - isRangeSelected: function() { + isRangeSelected() { return this.$.highlights.isRangeSelected(); }, - toggleLeftDiff: function() { + toggleLeftDiff() { this.toggleClass('no-left'); }, - _canRender: function() { + _canRender() { return this.changeNum && this.patchRange && this.path && !this.noAutoRender; }, - _getCommentThreads: function() { + _getCommentThreads() { return Polymer.dom(this.root).querySelectorAll('gr-diff-comment-thread'); }, - _computeContainerClass: function(loggedIn, viewMode, displayLine) { - var classes = ['diffContainer']; + _computeContainerClass(loggedIn, viewMode, displayLine) { + const classes = ['diffContainer']; switch (viewMode) { case DiffViewMode.UNIFIED: classes.push('unified'); @@ -190,6 +222,9 @@ default: throw Error('Invalid view mode: ', viewMode); } + if (Gerrit.hiddenscroll) { + classes.push('hiddenscroll'); + } if (loggedIn) { classes.push('canComment'); } @@ -199,8 +234,8 @@ return classes.join(' '); }, - _handleTap: function(e) { - var el = Polymer.dom(e).rootTarget; + _handleTap(e) { + const el = Polymer.dom(e).rootTarget; if (el.classList.contains('showContext')) { this.$.diffBuilder.showContext(e.detail.groups, e.detail.section); @@ -209,58 +244,60 @@ } else if (el.tagName === 'HL' || el.classList.contains('content') || el.classList.contains('contentText')) { - var target = this.$.diffBuilder.getLineElByChild(el); + const target = this.$.diffBuilder.getLineElByChild(el); if (target) { this._selectLine(target); } } }, - _selectLine: function(el) { + _selectLine(el) { this.fire('line-selected', { side: el.classList.contains('left') ? DiffSide.LEFT : DiffSide.RIGHT, number: el.getAttribute('data-value'), + path: this.path, }); }, - _handleCreateComment: function(e) { - var range = e.detail.range; - var diffSide = e.detail.side; - var line = range.endLine; - var lineEl = this.$.diffBuilder.getLineElByNumber(line, diffSide); - var contentText = this.$.diffBuilder.getContentByLineEl(lineEl); - var contentEl = contentText.parentElement; - var patchNum = this._getPatchNumByLineAndContent(lineEl, contentEl); - var isOnParent = + _handleCreateComment(e) { + const range = e.detail.range; + const diffSide = e.detail.side; + const line = range.endLine; + const lineEl = this.$.diffBuilder.getLineElByNumber(line, diffSide); + const contentText = this.$.diffBuilder.getContentByLineEl(lineEl); + const contentEl = contentText.parentElement; + const patchNum = this._getPatchNumByLineAndContent(lineEl, contentEl); + const isOnParent = this._getIsParentCommentByLineAndContent(lineEl, contentEl); - var threadEl = this._getOrCreateThreadAtLineRange(contentEl, patchNum, + const threadEl = this._getOrCreateThreadAtLineRange(contentEl, patchNum, diffSide, isOnParent, range); threadEl.addOrEditDraft(line, range); }, - _addDraft: function(lineEl, opt_lineNum) { - var contentText = this.$.diffBuilder.getContentByLineEl(lineEl); - var contentEl = contentText.parentElement; - var patchNum = this._getPatchNumByLineAndContent(lineEl, contentEl); - var commentSide = this._getCommentSideByLineAndContent(lineEl, contentEl); - var isOnParent = + _addDraft(lineEl, opt_lineNum) { + const contentText = this.$.diffBuilder.getContentByLineEl(lineEl); + const contentEl = contentText.parentElement; + const patchNum = this._getPatchNumByLineAndContent(lineEl, contentEl); + const commentSide = + this._getCommentSideByLineAndContent(lineEl, contentEl); + const isOnParent = this._getIsParentCommentByLineAndContent(lineEl, contentEl); - var threadEl = this._getOrCreateThreadAtLineRange(contentEl, patchNum, + const threadEl = this._getOrCreateThreadAtLineRange(contentEl, patchNum, commentSide, isOnParent); threadEl.addOrEditDraft(opt_lineNum); }, - _getThreadForRange: function(threadGroupEl, rangeToCheck) { + _getThreadForRange(threadGroupEl, rangeToCheck) { return threadGroupEl.getThreadForRange(rangeToCheck); }, - _getThreadGroupForLine: function(contentEl) { + _getThreadGroupForLine(contentEl) { return contentEl.querySelector('gr-diff-comment-thread-group'); }, - _getOrCreateThreadAtLineRange: - function(contentEl, patchNum, commentSide, isOnParent, range) { - var rangeToCheck = range ? + _getOrCreateThreadAtLineRange(contentEl, patchNum, commentSide, + isOnParent, range) { + const rangeToCheck = range ? 'range-' + range.startLine + '-' + range.startChar + '-' + @@ -269,15 +306,15 @@ commentSide : 'line-' + commentSide; // Check if thread group exists. - var threadGroupEl = this._getThreadGroupForLine(contentEl); + let threadGroupEl = this._getThreadGroupForLine(contentEl); if (!threadGroupEl) { threadGroupEl = this.$.diffBuilder.createCommentThreadGroup( - this.changeNum, patchNum, this.path, isOnParent, - this.projectConfig); + this.changeNum, patchNum, this.path, isOnParent, + this.projectConfig); contentEl.appendChild(threadGroupEl); } - var threadEl = this._getThreadForRange(threadGroupEl, rangeToCheck); + let threadEl = this._getThreadForRange(threadGroupEl, rangeToCheck); if (!threadEl) { threadGroupEl.addNewThread(rangeToCheck, commentSide); @@ -288,8 +325,8 @@ return threadEl; }, - _getPatchNumByLineAndContent: function(lineEl, contentEl) { - var patchNum = this.patchRange.patchNum; + _getPatchNumByLineAndContent(lineEl, contentEl) { + let patchNum = this.patchRange.patchNum; if ((lineEl.classList.contains(DiffSide.LEFT) || contentEl.classList.contains('remove')) && this.patchRange.basePatchNum !== 'PARENT') { @@ -298,8 +335,8 @@ return patchNum; }, - _getIsParentCommentByLineAndContent: function(lineEl, contentEl) { - var isOnParent = false; + _getIsParentCommentByLineAndContent(lineEl, contentEl) { + let isOnParent = false; if ((lineEl.classList.contains(DiffSide.LEFT) || contentEl.classList.contains('remove')) && this.patchRange.basePatchNum === 'PARENT') { @@ -308,8 +345,8 @@ return isOnParent; }, - _getCommentSideByLineAndContent: function(lineEl, contentEl) { - var side = 'right'; + _getCommentSideByLineAndContent(lineEl, contentEl) { + let side = 'right'; if (lineEl.classList.contains(DiffSide.LEFT) || contentEl.classList.contains('remove')) { side = 'left'; @@ -317,32 +354,32 @@ return side; }, - _handleThreadDiscard: function(e) { - var el = Polymer.dom(e).rootTarget; + _handleThreadDiscard(e) { + const el = Polymer.dom(e).rootTarget; el.parentNode.removeThread(el.locationRange); }, - _handleCommentDiscard: function(e) { - var comment = e.detail.comment; + _handleCommentDiscard(e) { + const comment = e.detail.comment; this._removeComment(comment, e.detail.patchNum); }, - _removeComment: function(comment, opt_patchNum) { - var side = comment.__commentSide; + _removeComment(comment) { + const side = comment.__commentSide; this._removeCommentFromSide(comment, side); }, - _handleCommentSave: function(e) { - var comment = e.detail.comment; - var side = e.detail.comment.__commentSide; - var idx = this._findDraftIndex(comment, side); + _handleCommentSave(e) { + const comment = e.detail.comment; + const side = e.detail.comment.__commentSide; + const idx = this._findDraftIndex(comment, side); this.set(['_comments', side, idx], comment); }, - _handleCommentUpdate: function(e) { - var comment = e.detail.comment; - var side = e.detail.comment.__commentSide; - var idx = this._findCommentIndex(comment, side); + _handleCommentUpdate(e) { + const comment = e.detail.comment; + const side = e.detail.comment.__commentSide; + let idx = this._findCommentIndex(comment, side); if (idx === -1) { idx = this._findDraftIndex(comment, side); } @@ -353,8 +390,8 @@ } }, - _removeCommentFromSide: function(comment, side) { - var idx = this._findCommentIndex(comment, side); + _removeCommentFromSide(comment, side) { + let idx = this._findCommentIndex(comment, side); if (idx === -1) { idx = this._findDraftIndex(comment, side); } @@ -363,29 +400,29 @@ } }, - _findCommentIndex: function(comment, side) { + _findCommentIndex(comment, side) { if (!comment.id || !this._comments[side]) { return -1; } - return this._comments[side].findIndex(function(item) { + return this._comments[side].findIndex(item => { return item.id === comment.id; }); }, - _findDraftIndex: function(comment, side) { + _findDraftIndex(comment, side) { if (!comment.__draftID || !this._comments[side]) { return -1; } - return this._comments[side].findIndex(function(item) { + return this._comments[side].findIndex(item => { return item.__draftID === comment.__draftID; }); }, - _prefsObserver: function(newPrefs, oldPrefs) { + _prefsObserver(newPrefs, oldPrefs) { // Scan the preference objects one level deep to see if they differ. - var differ = !oldPrefs; + let differ = !oldPrefs; if (newPrefs && oldPrefs) { - for (var key in newPrefs) { + for (const key in newPrefs) { if (newPrefs[key] !== oldPrefs[key]) { differ = true; } @@ -397,15 +434,15 @@ } }, - _viewModeObserver: function() { + _viewModeObserver() { this._prefsChanged(this.prefs); }, - _lineWrappingObserver: function() { + _lineWrappingObserver() { this._prefsChanged(this.prefs); }, - _prefsChanged: function(prefs) { + _prefsChanged(prefs) { if (!prefs) { return; } if (prefs.line_wrapping) { this._diffTableClass = 'full-width'; @@ -417,51 +454,69 @@ this.customStyle['--content-width'] = prefs.line_length + 'ch'; } - if (!!prefs.font_size) { + if (prefs.font_size) { this.customStyle['--font-size'] = prefs.font_size + 'px'; } this.updateStyles(); - if (this._diff && this._comments) { + if (this._diff && this._comments && !this.noRenderOnPrefsChange) { this._renderDiffTable(); } }, - _renderDiffTable: function() { - return this.$.diffBuilder.render(this._comments, this.prefs); + _renderDiffTable() { + if (this.prefs.context === -1 && + this._diffLength(this._diff) >= LARGE_DIFF_THRESHOLD_LINES && + this._safetyBypass === null) { + this._showWarning = true; + return Promise.resolve(); + } + + this._showWarning = false; + return this.$.diffBuilder.render(this._comments, this._getBypassPrefs()); }, - _clearDiffContent: function() { + /** + * Get the preferences object including the safety bypass context (if any). + */ + _getBypassPrefs() { + if (this._safetyBypass !== null) { + return Object.assign({}, this.prefs, {context: this._safetyBypass}); + } + return this.prefs; + }, + + _clearDiffContent() { this.$.diffTable.innerHTML = null; }, - _handleGetDiffError: function(response) { + _handleGetDiffError(response) { // Loading the diff may respond with 409 if the file is too large. In this // case, use a toast error.. if (response.status === 409) { - this.fire('server-error', {response: response}); + this.fire('server-error', {response}); return; } - this.fire('page-error', {response: response}); + this.fire('page-error', {response}); }, - _getDiff: function() { + _getDiff() { return this.$.restAPI.getDiff( this.changeNum, this.patchRange.basePatchNum, this.patchRange.patchNum, this.path, - this._handleGetDiffError.bind(this)).then(function(diff) { + this._handleGetDiffError.bind(this)).then(diff => { this.filesWeblinks = { meta_a: diff && diff.meta_a && diff.meta_a.web_links, meta_b: diff && diff.meta_b && diff.meta_b.web_links, }; return diff; - }.bind(this)); + }); }, - _getDiffComments: function() { + _getDiffComments() { return this.$.restAPI.getDiffComments( this.changeNum, this.patchRange.basePatchNum, @@ -469,8 +524,8 @@ this.path); }, - _getDiffDrafts: function() { - return this._getLoggedIn().then(function(loggedIn) { + _getDiffDrafts() { + return this._getLoggedIn().then(loggedIn => { if (!loggedIn) { return Promise.resolve({baseComments: [], comments: []}); } @@ -479,10 +534,10 @@ this.patchRange.basePatchNum, this.patchRange.patchNum, this.path); - }.bind(this)); + }); }, - _getDiffRobotComments: function() { + _getDiffRobotComments() { return this.$.restAPI.getDiffRobotComments( this.changeNum, this.patchRange.basePatchNum, @@ -490,12 +545,12 @@ this.path); }, - _getDiffCommentsAndDrafts: function() { - var promises = []; + _getDiffCommentsAndDrafts() { + const promises = []; promises.push(this._getDiffComments()); promises.push(this._getDiffDrafts()); promises.push(this._getDiffRobotComments()); - return Promise.all(promises).then(function(results) { + return Promise.all(promises).then(results => { return Promise.resolve({ comments: results[0], drafts: results[1], @@ -504,16 +559,16 @@ }).then(this._normalizeDiffCommentsAndDrafts.bind(this)); }, - _normalizeDiffCommentsAndDrafts: function(results) { + _normalizeDiffCommentsAndDrafts(results) { function markAsDraft(d) { d.__draft = true; return d; } - var baseDrafts = results.drafts.baseComments.map(markAsDraft); - var drafts = results.drafts.comments.map(markAsDraft); + const baseDrafts = results.drafts.baseComments.map(markAsDraft); + const drafts = results.drafts.comments.map(markAsDraft); - var baseRobotComments = results.robotComments.baseComments; - var robotComments = results.robotComments.comments; + const baseRobotComments = results.robotComments.baseComments; + const robotComments = results.robotComments.comments; return Promise.resolve({ meta: { path: this.path, @@ -528,27 +583,27 @@ }); }, - _getLoggedIn: function() { + _getLoggedIn() { return this.$.restAPI.getLoggedIn(); }, - _computeIsImageDiff: function() { + _computeIsImageDiff() { if (!this._diff) { return false; } - var isA = this._diff.meta_a && - this._diff.meta_a.content_type.indexOf('image/') === 0; - var isB = this._diff.meta_b && - this._diff.meta_b.content_type.indexOf('image/') === 0; + const isA = this._diff.meta_a && + this._diff.meta_a.content_type.startsWith('image/'); + const isB = this._diff.meta_b && + this._diff.meta_b.content_type.startsWith('image/'); return this._diff.binary && (isA || isB); }, - _loadDiffAssets: function() { + _loadDiffAssets() { if (this.isImageDiff) { - return this._getImages().then(function(images) { + return this._getImages().then(images => { this._baseImage = images.baseImage; this._revisionImage = images.revisionImage; - }.bind(this)); + }); } else { this._baseImage = null; this._revisionImage = null; @@ -556,16 +611,64 @@ } }, - _getImages: function() { - return this.$.restAPI.getImagesForDiff(this.project, this.commit, - this.changeNum, this._diff, this.patchRange); + _getImages() { + return this.$.restAPI.getImagesForDiff(this.changeNum, this._diff, + this.patchRange); }, - _projectConfigChanged: function(projectConfig) { - var threadEls = this._getCommentThreads(); - for (var i = 0; i < threadEls.length; i++) { + _projectConfigChanged(projectConfig) { + const threadEls = this._getCommentThreads(); + for (let i = 0; i < threadEls.length; i++) { threadEls[i].projectConfig = projectConfig; } }, + + _computeDiffHeaderItems(diffInfoRecord) { + const diffInfo = diffInfoRecord.base; + if (!diffInfo || !diffInfo.diff_header || diffInfo.binary) { return []; } + return diffInfo.diff_header.filter(item => { + return !(item.startsWith('diff --git ') || + item.startsWith('index ') || + item.startsWith('+++ ') || + item.startsWith('--- ')); + }); + }, + + _computeDiffHeaderHidden(items) { + return items.length === 0; + }, + + /** + * The number of lines in the diff. For delta chunks that are different + * sizes on the left and the right, the longer side is used. + * @param {!Object} diff + * @return {Number} + */ + _diffLength(diff) { + return diff.content.reduce((sum, sec) => { + if (sec.hasOwnProperty('ab')) { + return sum + sec.ab.length; + } else { + return sum + Math.max( + sec.hasOwnProperty('a') ? sec.a.length : 0, + sec.hasOwnProperty('b') ? sec.b.length : 0 + ); + } + }, 0); + }, + + _handleFullBypass() { + this._safetyBypass = FULL_CONTEXT; + this._renderDiffTable(); + }, + + _handleLimitedBypass() { + this._safetyBypass = LIMITED_CONTEXT; + this._renderDiffTable(); + }, + + _computeWarningClass(showWarning) { + return showWarning ? 'warn' : ''; + }, }); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html index 34d6de21c..921d285 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html"> <link rel="import" href="gr-diff.html"> @@ -35,72 +35,101 @@ </test-fixture> <script> - suite('gr-diff tests', function() { - var element; - var sandbox; + suite('gr-diff tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - suite('not logged in', function() { - setup(function() { + test('reload cancels before network resolves', () => { + element = fixture('basic'); + const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel'); + + // Stub the network calls into requests that never resolve. + sandbox.stub(element, '_getDiff', () => new Promise(() => {})); + sandbox.stub(element, '_getDiffCommentsAndDrafts', + () => new Promise(() => {})); + + element.reload(); + assert.isTrue(cancelStub.called); + }); + + test('_diffLength', () => { + element = fixture('basic'); + const mock = document.createElement('mock-diff-response'); + assert.equal(element._diffLength(mock.diffResponse), 52); + }); + + suite('not logged in', () => { + setup(() => { stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(false); }, + getLoggedIn() { return Promise.resolve(false); }, }); element = fixture('basic'); }); - test('toggleLeftDiff', function() { + test('toggleLeftDiff', () => { element.toggleLeftDiff(); assert.isTrue(element.classList.contains('no-left')); element.toggleLeftDiff(); assert.isFalse(element.classList.contains('no-left')); }); - test('view does not start with displayLine classList', function() { + test('addDraftAtLine', done => { + sandbox.stub(element, '_selectLine'); + const loggedInErrorSpy = sandbox.spy(); + element.addEventListener('show-auth-required', loggedInErrorSpy); + element.addDraftAtLine(); + flush(() => { + assert.isTrue(loggedInErrorSpy.called); + done(); + }); + }); + + test('view does not start with displayLine classList', () => { assert.isFalse( element.$$('.diffContainer').classList.contains('displayLine')); }); - test('displayLine class added called when displayLine is true', - function() { - var spy = sandbox.spy(element, '_computeContainerClass'); + test('displayLine class added called when displayLine is true', () => { + const spy = sandbox.spy(element, '_computeContainerClass'); element.displayLine = true; assert.isTrue(spy.called); assert.isTrue( element.$$('.diffContainer').classList.contains('displayLine')); }); - test('get drafts', function(done) { + test('get drafts', done => { element.patchRange = {basePatchNum: 0, patchNum: 0}; - var getDraftsStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts'); - element._getDiffDrafts().then(function(result) { + const getDraftsStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts'); + element._getDiffDrafts().then(result => { assert.deepEqual(result, {baseComments: [], comments: []}); sinon.assert.notCalled(getDraftsStub); done(); }); }); - test('get robot comments', function(done) { + test('get robot comments', done => { element.patchRange = {basePatchNum: 0, patchNum: 0}; - var getDraftsStub = sandbox.stub(element.$.restAPI, + const getDraftsStub = sandbox.stub(element.$.restAPI, 'getDiffRobotComments'); - element._getDiffDrafts().then(function(result) { + element._getDiffDrafts().then(result => { assert.deepEqual(result, {baseComments: [], comments: []}); sinon.assert.notCalled(getDraftsStub); done(); }); }); - test('loads files weblinks', function(done) { - var diffStub = sandbox.stub(element.$.restAPI, 'getDiff').returns( + test('loads files weblinks', done => { + sandbox.stub(element.$.restAPI, 'getDiff').returns( Promise.resolve({ meta_a: { web_links: 'foo', @@ -110,7 +139,7 @@ }, })); element.patchRange = {}; - element._getDiff().then(function() { + element._getDiff().then(() => { assert.deepEqual(element.filesWeblinks, { meta_a: 'foo', meta_b: 'bar', @@ -119,7 +148,7 @@ }); }); - test('remove comment', function() { + test('remove comment', () => { element._comments = { meta: { changeNum: '42', @@ -172,7 +201,7 @@ })); element._removeComment({id: 'bc2', side: 'PARENT', - __commentSide: 'left'}); + __commentSide: 'left'}); assert.deepEqual(element._comments, { meta: { changeNum: '42', @@ -220,13 +249,12 @@ }); }); - test('thread groups', function() { - var contentEl = document.createElement('div'); - var rangeToCheck = 'line-left'; - var commentSide = 'left'; - var patchNum = 1; - var side = 'PARENT'; - var range = { + test('thread groups', () => { + const contentEl = document.createElement('div'); + const commentSide = 'left'; + const patchNum = 1; + const side = 'PARENT'; + let range = { startLine: 1, startChar: 1, endLine: 1, @@ -237,10 +265,9 @@ element.patchRange = {basePatchNum: 1, patchNum: 2}; element.path = 'file.txt'; - sandbox.stub(element.$.diffBuilder, 'createCommentThreadGroup', - function() { - var threadGroup = - document.createElement('gr-diff-comment-thread-group'); + sandbox.stub(element.$.diffBuilder, 'createCommentThreadGroup', () => { + const threadGroup = + document.createElement('gr-diff-comment-thread-group'); threadGroup.patchForNewThreads = 1; return threadGroup; }); @@ -268,116 +295,350 @@ assert.equal(contentEl.querySelectorAll( 'gr-diff-comment-thread-group').length, 1); - var threadGroup = contentEl.querySelector( + const threadGroup = contentEl.querySelector( 'gr-diff-comment-thread-group'); - var threadLength = Polymer.dom(threadGroup.root). + const threadLength = Polymer.dom(threadGroup.root). querySelectorAll('gr-diff-comment-thread').length; assert.equal(threadLength, 2); }); - test('renders image diffs', function(done) { - var mockDiff = { - meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, - meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 560}, - intraline_status: 'OK', - change_type: 'MODIFIED', - diff_header: [ - 'diff --git a/carrot.jpg b/carrot.jpg', - 'index 2adc47d..f9c2f2c 100644', - '--- a/carrot.jpg', - '+++ b/carrot.jpg', - 'Binary files differ', - ], - content: [{skip: 66}], - binary: true, - }; - var mockFile1 = { - body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAEwsA' + - 'AAAAAAAAAAAAAAAA/w==', - type: 'image/bmp', - }; - var mockFile2 = { - body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAEwsA' + - 'AAAAAAAAAAAA/////w==', - type: 'image/bmp' - }; - var mockCommit = { - commit: '9a1a1d10baece5efbba10bc4ccf808a67a50ac0a', - parents: [{ - commit: '7338aa9adfe57909f1fdaf88975cdea467d3382f', - subject: 'Added a carrot', - }], - author: { - name: 'Wyatt Allen', - email: 'wyatta@google.com', - date: '2016-05-23 21:44:51.000000000', - tz: -420, - }, - committer: { - name: 'Wyatt Allen', - email: 'wyatta@google.com', - date: '2016-05-25 00:25:41.000000000', - tz: -420, - }, - subject: 'Updated the carrot', - message: 'Updated the carrot\n\nChange-Id: Iabcd123\n', - }; - var mockComments = {baseComments: [], comments: []}; + suite('image diffs', () => { + let mockFile1; + let mockFile2; + const stubs = []; + setup(() => { + mockFile1 = { + body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' + + 'wsAAAAAAAAAAAAAAAAA/w==', + type: 'image/bmp', + }; + mockFile2 = { + body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' + + 'wsAAAAAAAAAAAAA/////w==', + type: 'image/bmp', + }; + const mockCommit = { + commit: '9a1a1d10baece5efbba10bc4ccf808a67a50ac0a', + parents: [{ + commit: '7338aa9adfe57909f1fdaf88975cdea467d3382f', + subject: 'Added a carrot', + }], + author: { + name: 'Wyatt Allen', + email: 'wyatta@google.com', + date: '2016-05-23 21:44:51.000000000', + tz: -420, + }, + committer: { + name: 'Wyatt Allen', + email: 'wyatta@google.com', + date: '2016-05-25 00:25:41.000000000', + tz: -420, + }, + subject: 'Updated the carrot', + message: 'Updated the carrot\n\nChange-Id: Iabcd123\n', + }; + const mockComments = {baseComments: [], comments: []}; - var stubs = []; - stubs.push(sandbox.stub(element, '_getDiff', - function() { return Promise.resolve(mockDiff); })); - stubs.push(sandbox.stub(element.$.restAPI, 'getCommitInfo', - function() { return Promise.resolve(mockCommit); })); - stubs.push(sandbox.stub(element.$.restAPI, - 'getCommitFileContents', - function() { return Promise.resolve(mockFile1); })); - stubs.push(sandbox.stub(element.$.restAPI, - 'getChangeFileContents', - function() { return Promise.resolve(mockFile2); })); - stubs.push(sandbox.stub(element.$.restAPI, '_getDiffComments', - function() { return Promise.resolve(mockComments); })); - stubs.push(sandbox.stub(element.$.restAPI, 'getDiffDrafts', - function() { return Promise.resolve(mockComments); })); + stubs.push(sandbox.stub(element.$.restAPI, 'getCommitInfo', + () => Promise.resolve(mockCommit))); + stubs.push(sandbox.stub(element.$.restAPI, + 'getChangeFileContents', + (changeId, patchNum, path, opt_parentIndex) => { + return Promise.resolve(opt_parentIndex === 1 ? mockFile1 : + mockFile2); + })); + stubs.push(sandbox.stub(element.$.restAPI, '_getDiffComments', + () => Promise.resolve(mockComments))); + stubs.push(sandbox.stub(element.$.restAPI, 'getDiffDrafts', + () => Promise.resolve(mockComments))); - element.patchRange = {basePatchNum: 'PARENT', patchNum: 1}; + element.patchRange = {basePatchNum: 'PARENT', patchNum: 1}; + }); - var rendered = function() { - // Recognizes that it should be an image diff. - assert.isTrue(element.isImageDiff); - assert.instanceOf(element.$.diffBuilder._builder, GrDiffBuilderImage); + test('renders image diffs with same file name', done => { + const mockDiff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, + meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, + intraline_status: 'OK', + change_type: 'MODIFIED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot.jpg', + 'index 2adc47d..f9c2f2c 100644', + '--- a/carrot.jpg', + '+++ b/carrot.jpg', + 'Binary files differ', + ], + content: [{skip: 66}], + binary: true, + }; + stubs.push(sandbox.stub(element, '_getDiff', + () => Promise.resolve(mockDiff))); - // Left image rendered with the parent commit's version of the file. - var leftInmage = element.$.diffTable.querySelector('td.left img'); - assert.isOk(leftInmage); - assert.equal(leftInmage.getAttribute('src'), - 'data:image/bmp;base64, ' + mockFile1.body); + const rendered = () => { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diffBuilder._builder, GrDiffBuilderImage); - // Right image rendered with this change's revision of the image. - var rightInmage = element.$.diffTable.querySelector('td.right img'); - assert.isOk(rightInmage); - assert.equal(rightInmage.getAttribute('src'), - 'data:image/bmp;base64, ' + mockFile2.body); + // Left image rendered with the parent commit's version of the file. + const leftImage = element.$.diffTable.querySelector('td.left img'); + const leftLabel = + element.$.diffTable.querySelector('td.left label'); + const leftLabelContent = leftLabel.querySelector('.label'); + const leftLabelName = leftLabel.querySelector('.name'); - // Cleanup. - element.removeEventListener('render', rendered); + const rightImage = + element.$.diffTable.querySelector('td.right img'); + const rightLabel = element.$.diffTable.querySelector( + 'td.right label'); + const rightLabelContent = rightLabel.querySelector('.label'); + const rightLabelName = rightLabel.querySelector('.name'); - done(); - }; + assert.isNotOk(rightLabelName); + assert.isNotOk(leftLabelName); - element.addEventListener('render', rendered); + let leftLoaded = false; + let rightLoaded = false; - element.$.restAPI.getDiffPreferences().then(function(prefs) { - element.prefs = prefs; - element.reload(); + leftImage.addEventListener('load', () => { + assert.isOk(leftImage); + assert.equal(leftImage.getAttribute('src'), + 'data:image/bmp;base64, ' + mockFile1.body); + assert.equal(leftLabelContent.textContent, '1⨉1 image/bmp'); + leftLoaded = true; + if (rightLoaded) { + element.removeEventListener('render', rendered); + done(); + } + }); + + rightImage.addEventListener('load', () => { + assert.isOk(rightImage); + assert.equal(rightImage.getAttribute('src'), + 'data:image/bmp;base64, ' + mockFile2.body); + assert.equal(rightLabelContent.textContent, '1⨉1 image/bmp'); + + rightLoaded = true; + if (leftLoaded) { + element.removeEventListener('render', rendered); + done(); + } + }); + }; + + element.addEventListener('render', rendered); + + element.$.restAPI.getDiffPreferences().then(prefs => { + element.prefs = prefs; + element.reload(); + }); + }); + + test('renders image diffs with a different file name', done => { + const mockDiff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, + meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg', + lines: 560}, + intraline_status: 'OK', + change_type: 'MODIFIED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot2.jpg', + 'index 2adc47d..f9c2f2c 100644', + '--- a/carrot.jpg', + '+++ b/carrot2.jpg', + 'Binary files differ', + ], + content: [{skip: 66}], + binary: true, + }; + stubs.push(sandbox.stub(element, '_getDiff', + () => Promise.resolve(mockDiff))); + + const rendered = () => { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diffBuilder._builder, GrDiffBuilderImage); + + // Left image rendered with the parent commit's version of the file. + const leftImage = element.$.diffTable.querySelector('td.left img'); + const leftLabel = + element.$.diffTable.querySelector('td.left label'); + const leftLabelContent = leftLabel.querySelector('.label'); + const leftLabelName = leftLabel.querySelector('.name'); + + const rightImage = + element.$.diffTable.querySelector('td.right img'); + const rightLabel = element.$.diffTable.querySelector( + 'td.right label'); + const rightLabelContent = rightLabel.querySelector('.label'); + const rightLabelName = rightLabel.querySelector('.name'); + + assert.isOk(rightLabelName); + assert.isOk(leftLabelName); + assert.equal(leftLabelName.textContent, mockDiff.meta_a.name); + assert.equal(rightLabelName.textContent, mockDiff.meta_b.name); + + let leftLoaded = false; + let rightLoaded = false; + + leftImage.addEventListener('load', () => { + assert.isOk(leftImage); + assert.equal(leftImage.getAttribute('src'), + 'data:image/bmp;base64, ' + mockFile1.body); + assert.equal(leftLabelContent.textContent, '1⨉1 image/bmp'); + leftLoaded = true; + if (rightLoaded) { + element.removeEventListener('render', rendered); + done(); + } + }); + + rightImage.addEventListener('load', () => { + assert.isOk(rightImage); + assert.equal(rightImage.getAttribute('src'), + 'data:image/bmp;base64, ' + mockFile2.body); + assert.equal(rightLabelContent.textContent, '1⨉1 image/bmp'); + + rightLoaded = true; + if (leftLoaded) { + element.removeEventListener('render', rendered); + done(); + } + }); + }; + + element.addEventListener('render', rendered); + + element.$.restAPI.getDiffPreferences().then(prefs => { + element.prefs = prefs; + element.reload(); + }); + }); + + test('renders added image', done => { + const mockDiff = { + meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, + intraline_status: 'OK', + change_type: 'ADDED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot.jpg', + 'index 0000000..f9c2f2c 100644', + '--- /dev/null', + '+++ b/carrot.jpg', + 'Binary files differ', + ], + content: [{skip: 66}], + binary: true, + }; + stubs.push(sandbox.stub(element, '_getDiff', + () => Promise.resolve(mockDiff))); + + element.addEventListener('render', () => { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diffBuilder._builder, GrDiffBuilderImage); + + const leftImage = element.$.diffTable.querySelector('td.left img'); + const rightImage = element.$.diffTable.querySelector('td.right img'); + + assert.isNotOk(leftImage); + assert.isOk(rightImage); + done(); + }); + + element.$.restAPI.getDiffPreferences().then(prefs => { + element.prefs = prefs; + element.reload(); + }); + }); + + test('renders removed image', done => { + const mockDiff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, + intraline_status: 'OK', + change_type: 'DELETED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot.jpg', + 'index f9c2f2c..0000000 100644', + '--- a/carrot.jpg', + '+++ /dev/null', + 'Binary files differ', + ], + content: [{skip: 66}], + binary: true, + }; + stubs.push(sandbox.stub(element, '_getDiff', + () => Promise.resolve(mockDiff))); + + element.addEventListener('render', () => { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diffBuilder._builder, GrDiffBuilderImage); + + const leftImage = element.$.diffTable.querySelector('td.left img'); + const rightImage = element.$.diffTable.querySelector('td.right img'); + + assert.isOk(leftImage); + assert.isNotOk(rightImage); + done(); + }); + + element.$.restAPI.getDiffPreferences().then(prefs => { + element.prefs = prefs; + element.reload(); + }); + }); + + test('does not render disallowed image type', done => { + const mockDiff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil', + lines: 560}, + intraline_status: 'OK', + change_type: 'DELETED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot.jpg', + 'index f9c2f2c..0000000 100644', + '--- a/carrot.jpg', + '+++ /dev/null', + 'Binary files differ', + ], + content: [{skip: 66}], + binary: true, + }; + mockFile1.type = 'image/jpeg-evil'; + + stubs.push(sandbox.stub(element, '_getDiff', + () => Promise.resolve(mockDiff))); + + element.addEventListener('render', () => { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diffBuilder._builder, GrDiffBuilderImage); + const leftImage = element.$.diffTable.querySelector('td.left img'); + assert.isNotOk(leftImage); + done(); + }); + + element.$.restAPI.getDiffPreferences().then(prefs => { + element.prefs = prefs; + element.reload(); + }); }); }); - test('_handleTap lineNum', function(done) { - var addDraftStub = sinon.stub(element, 'addDraftAtLine'); - var el = document.createElement('div'); + test('_handleTap lineNum', done => { + const addDraftStub = sandbox.stub(element, 'addDraftAtLine'); + const el = document.createElement('div'); el.className = 'lineNum'; - el.addEventListener('click', function(e) { + el.addEventListener('click', e => { element._handleTap(e); assert.isTrue(addDraftStub.called); assert.equal(addDraftStub.lastCall.args[0], el); @@ -386,11 +647,12 @@ el.click(); }); - test('_handleTap context', function(done) { - var showContextStub = sinon.stub(element.$.diffBuilder, 'showContext'); - var el = document.createElement('div'); + test('_handleTap context', done => { + const showContextStub = + sandbox.stub(element.$.diffBuilder, 'showContext'); + const el = document.createElement('div'); el.className = 'showContext'; - el.addEventListener('click', function(e) { + el.addEventListener('click', e => { element._handleTap(e); assert.isTrue(showContextStub.called); done(); @@ -398,16 +660,15 @@ el.click(); }); - test('_handleTap content', function(done) { - var content = document.createElement('div'); - var lineEl = document.createElement('div'); + test('_handleTap content', done => { + const content = document.createElement('div'); + const lineEl = document.createElement('div'); - var selectStub = sandbox.stub(element, '_selectLine'); - var getLineStub = sandbox.stub(element.$.diffBuilder, - 'getLineElByChild', function() { return lineEl; }); + const selectStub = sandbox.stub(element, '_selectLine'); + sandbox.stub(element.$.diffBuilder, 'getLineElByChild', () => lineEl); content.className = 'content'; - content.addEventListener('click', function(e) { + content.addEventListener('click', e => { element._handleTap(e); assert.isTrue(selectStub.called); assert.equal(selectStub.lastCall.args[0], lineEl); @@ -416,9 +677,9 @@ content.click(); }); - test('_getDiff handles null diff responses', function(done) { + test('_getDiff handles null diff responses', done => { stub('gr-rest-api-interface', { - getDiff: function() { return Promise.resolve(null); }, + getDiff() { return Promise.resolve(null); }, }); element.changeNum = 123; element.patchRange = {basePatchNum: 1, patchNum: 2}; @@ -426,10 +687,9 @@ element._getDiff().then(done); }); - suite('getCursorStops', function() { - - var setupDiff = function() { - var mock = document.createElement('mock-diff-response'); + suite('getCursorStops', () => { + const setupDiff = function() { + const mock = document.createElement('mock-diff-response'); element._diff = mock.diffResponse; element._comments = { left: [], @@ -456,48 +716,53 @@ flushAsynchronousOperations(); }; - test('getCursorStops returns [] when hidden and noAutoRender are true', - function() { + test('getCursorStops returns [] when hidden and noAutoRender', () => { element.noAutoRender = true; setupDiff(); element.hidden = true; assert.equal(element.getCursorStops().length, 0); }); - test('getCursorStops', function() { + test('getCursorStops', () => { setupDiff(); assert.equal(element.getCursorStops().length, 50); }); }); + + test('adds .hiddenscroll', () => { + Gerrit.hiddenscroll = true; + element.displayLine = true; + assert.include(element.$$('.diffContainer').className, 'hiddenscroll'); + }); }); - suite('logged in', function() { - setup(function() { + suite('logged in', () => { + setup(() => { stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(true); }, - getPreferences: function() { + getLoggedIn() { return Promise.resolve(true); }, + getPreferences() { return Promise.resolve({time_format: 'HHMM_12'}); }, }); element = fixture('basic'); }); - test('get drafts', function(done) { + test('get drafts', done => { element.patchRange = {basePatchNum: 0, patchNum: 0}; - var draftsResponse = { + const draftsResponse = { baseComments: [{id: 'foo'}], comments: [{id: 'bar'}], }; - var getDraftsStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts', - function() { return Promise.resolve(draftsResponse); }); - element._getDiffDrafts().then(function(result) { + sandbox.stub(element.$.restAPI, 'getDiffDrafts', + () => Promise.resolve(draftsResponse)); + element._getDiffDrafts().then(result => { assert.deepEqual(result, draftsResponse); done(); }); }); - test('get comments and drafts', function(done) { - var comments = { + test('get comments and drafts', done => { + const comments = { baseComments: [ {id: 'bc1', __commentSide: 'left'}, {id: 'bc2', __commentSide: 'left'}, @@ -507,10 +772,10 @@ {id: 'c2', __commentSide: 'right'}, ], }; - var diffCommentsStub = sandbox.stub(element, '_getDiffComments', - function() { return Promise.resolve(comments); }); + sandbox.stub(element, '_getDiffComments', + () => Promise.resolve(comments)); - var drafts = { + const drafts = { baseComments: [ {id: 'bd1', __commentSide: 'left'}, {id: 'bd2', __commentSide: 'left'}, @@ -521,10 +786,9 @@ ], }; - var diffDraftsStub = sandbox.stub(element, '_getDiffDrafts', - function() { return Promise.resolve(drafts); }); + sandbox.stub(element, '_getDiffDrafts', () => Promise.resolve(drafts)); - var robotComments = { + const robotComments = { baseComments: [ {id: 'br1', __commentSide: 'left'}, {id: 'br2', __commentSide: 'left'}, @@ -535,9 +799,8 @@ ], }; - var diffRobotCommentStub = sandbox.stub(element, - '_getDiffRobotComments', function() { - return Promise.resolve(robotComments); }); + sandbox.stub(element, + '_getDiffRobotComments', () => Promise.resolve(robotComments)); element.changeNum = '42'; element.patchRange = { @@ -547,7 +810,7 @@ element.path = '/path/to/foo'; element.projectConfig = {foo: 'bar'}; - element._getDiffCommentsAndDrafts().then(function(result) { + element._getDiffCommentsAndDrafts().then(result => { assert.deepEqual(result, { meta: { changeNum: '42', @@ -580,9 +843,31 @@ }); }); - suite('handle comment-update', function() { + test('addDraftAtLine', done => { + const fakeLineEl = {getAttribute: sandbox.stub().returns(42)}; + sandbox.stub(element, '_selectLine'); + sandbox.stub(element, '_addDraft'); + const loggedInErrorSpy = sandbox.spy(); + element.addEventListener('show-auth-required', loggedInErrorSpy); + element.addDraftAtLine(fakeLineEl); + flush(() => { + assert.isFalse(loggedInErrorSpy.called); + assert.isTrue(element._addDraft.calledWithExactly(fakeLineEl, 42)); + done(); + }); + }); - setup(function() { + suite('change in preferences', () => { + setup(() => { + element._diff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, + meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, + diff_header: [], + intraline_status: 'OK', + change_type: 'MODIFIED', + content: [{skip: 66}], + }; element._comments = { meta: { changeNum: '42', @@ -608,24 +893,70 @@ }; }); - test('creating a draft', function() { - var comment = {__draft: true, __draftID: 'tempID', side: 'PARENT', - __commentSide: 'left'}; - element.fire('comment-update', {comment: comment}); + test('change in preferences re-renders diff', () => { + sandbox.stub(element, '_renderDiffTable'); + element.prefs = {}; + element.prefs = {time_format: 'HHMM_12'}; + assert.isTrue(element._renderDiffTable.called); + }); + + test('change in preferences does not re-renders diff with ' + + 'noRenderOnPrefsChange', () => { + sandbox.stub(element, '_renderDiffTable'); + element.noRenderOnPrefsChange = true; + element.prefs = {}; + element.prefs = {time_format: 'HHMM_12'}; + assert.isFalse(element._renderDiffTable.called); + }); + }); + + suite('handle comment-update', () => { + setup(() => { + element._comments = { + meta: { + changeNum: '42', + patchRange: { + basePatchNum: 'PARENT', + patchNum: 3, + }, + path: '/path/to/foo', + projectConfig: {foo: 'bar'}, + }, + left: [ + {id: 'bc1', side: 'PARENT', __commentSide: 'left'}, + {id: 'bc2', side: 'PARENT', __commentSide: 'left'}, + {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'}, + {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'}, + ], + right: [ + {id: 'c1', __commentSide: 'right'}, + {id: 'c2', __commentSide: 'right'}, + {id: 'd1', __draft: true, __commentSide: 'right'}, + {id: 'd2', __draft: true, __commentSide: 'right'}, + ], + }; + }); + + test('creating a draft', () => { + const comment = {__draft: true, __draftID: 'tempID', side: 'PARENT', + __commentSide: 'left'}; + element.fire('comment-update', {comment}); assert.include(element._comments.left, comment); }); - test('saving a draft', function() { - var draftID = 'tempID'; - var id = 'savedID'; - element._comments.left.push( - {__draft: true, __draftID: draftID, side: 'PARENT', - __commentSide: 'left'}); - element.fire('comment-update', {comment: - {id: id, __draft: true, __draftID: draftID, side: 'PARENT', - __commentSide: 'left'}, - }); - var drafts = element._comments.left.filter(function(item) { + test('saving a draft', () => { + const draftID = 'tempID'; + const id = 'savedID'; + const comment = { + __draft: true, + __draftID: draftID, + side: 'PARENT', + __commentSide: 'left', + }; + element._comments.left.push(comment); + comment.id = id; + element.fire('comment-update', {comment}); + const drafts = element._comments.left.filter(item => { return item.__draftID === draftID; }); assert.equal(drafts.length, 1); @@ -633,5 +964,82 @@ }); }); }); + + suite('diff header', () => { + setup(() => { + element._diff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, + meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, + diff_header: [], + intraline_status: 'OK', + change_type: 'MODIFIED', + content: [{skip: 66}], + }; + }); + + test('hidden', () => { + assert.equal(element._diffHeaderItems.length, 0); + element.push('_diff.diff_header', 'diff --git a/test.jpg b/test.jpg'); + assert.equal(element._diffHeaderItems.length, 0); + element.push('_diff.diff_header', 'index 2adc47d..f9c2f2c 100644'); + assert.equal(element._diffHeaderItems.length, 0); + element.push('_diff.diff_header', '--- a/test.jpg'); + assert.equal(element._diffHeaderItems.length, 0); + element.push('_diff.diff_header', '+++ b/test.jpg'); + assert.equal(element._diffHeaderItems.length, 0); + element.push('_diff.diff_header', 'test'); + assert.equal(element._diffHeaderItems.length, 1); + flushAsynchronousOperations(); + + assert.equal(element.$.diffHeader.textContent.trim(), 'test'); + element.set('_diff.binary', true); + assert.equal(element._diffHeaderItems.length, 0); + }); + }); + + suite('safety and bypass', () => { + let renderStub; + + setup(() => { + element = fixture('basic'); + renderStub = sandbox.stub(element.$.diffBuilder, 'render', + () => Promise.resolve()); + const mock = document.createElement('mock-diff-response'); + element._diff = mock.diffResponse; + element._comments = {left: [], right: []}; + element.noRenderOnPrefsChange = true; + }); + + test('lage render w/ context = 10', () => { + element.prefs = {context: 10}; + sandbox.stub(element, '_diffLength', () => 10000); + return element._renderDiffTable().then(() => { + assert.isTrue(renderStub.called); + assert.isFalse(element._showWarning); + }); + }); + + test('lage render w/ whole file and bypass', () => { + element.prefs = {context: -1}; + element._safetyBypass = 10; + sandbox.stub(element, '_diffLength', () => 10000); + return element._renderDiffTable().then(() => { + assert.isTrue(renderStub.called); + assert.isFalse(element._showWarning); + }); + }); + + test('lage render w/ whole file and no bypass', () => { + element.prefs = {context: -1}; + sandbox.stub(element, '_diffLength', () => 10000); + return element._renderDiffTable().then(() => { + assert.isFalse(renderStub.called); + assert.isTrue(element._showWarning); + }); + }); + }); }); + + a11ySuite('basic'); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html index 66e6ee7..b74cbe2 100644 --- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.html
@@ -17,10 +17,11 @@ <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-select/gr-select.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-patch-range-select"> <template> - <style> + <style include="shared-styles"> :host { display: block; }
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js index 58d29bd..e9437bd 100644 --- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -15,7 +15,7 @@ 'use strict'; // Maximum length for patch set descriptions. - var PATCH_DESC_MAX_LENGTH = 500; + const PATCH_DESC_MAX_LENGTH = 500; Polymer({ is: 'gr-patch-range-select', @@ -36,15 +36,15 @@ behaviors: [Gerrit.PatchSetBehavior], - _updateSelected: function() { + _updateSelected() { this._rightSelected = this.patchRange.patchNum; this._leftSelected = this.patchRange.basePatchNum; }, - _handlePatchChange: function(e) { - var leftPatch = this._leftSelected; - var rightPatch = this._rightSelected; - var rangeStr = rightPatch; + _handlePatchChange(e) { + const leftPatch = this._leftSelected; + const rightPatch = this._rightSelected; + let rangeStr = rightPatch; if (leftPatch != 'PARENT') { rangeStr = leftPatch + '..' + rangeStr; } @@ -52,11 +52,11 @@ e.target.blur(); }, - _computeLeftDisabled: function(patchNum, patchRange) { + _computeLeftDisabled(patchNum, patchRange) { return parseInt(patchNum, 10) >= parseInt(patchRange.patchNum, 10); }, - _computeRightDisabled: function(patchNum, patchRange) { + _computeRightDisabled(patchNum, patchRange) { if (patchRange.basePatchNum == 'PARENT') { return false; } return parseInt(patchNum, 10) <= parseInt(patchRange.basePatchNum, 10); }, @@ -66,16 +66,16 @@ // are loaded, the correct value will get selected. I attempted to // debounce these, but because they are detecting two different // events, sometimes the timing was off and one ended up missing. - _synchronizeSelectionRight: function() { + _synchronizeSelectionRight() { this.$.rightPatchSelect.value = this._rightSelected; }, - _synchronizeSelectionLeft: function() { + _synchronizeSelectionLeft() { this.$.leftPatchSelect.value = this._leftSelected; }, - _computePatchSetDescription: function(revisions, patchNum) { - var rev = this.getRevisionByPatchNum(revisions, patchNum); + _computePatchSetDescription(revisions, patchNum) { + const rev = this.getRevisionByPatchNum(revisions, patchNum); return (rev && rev.description) ? rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; },
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html index 00d73bf..162936c 100644 --- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../bower_components/page/page.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-patch-range-select.html"> <script>void(0);</script> @@ -34,24 +34,24 @@ </test-fixture> <script> - suite('gr-patch-range-select tests', function() { - var element; + suite('gr-patch-range-select tests', () => { + let element; - setup(function() { + setup(() => { element = fixture('basic'); }); - test('enabled/disabled options', function() { - var patchRange = { + test('enabled/disabled options', () => { + const patchRange = { basePatchNum: 'PARENT', patchNum: '3', }; - ['1', '2', '3'].forEach(function(patchNum) { + for (const patchNum of ['1', '2', '3']) { assert.isFalse(element._computeRightDisabled(patchNum, patchRange)); - }); - ['PARENT', '1', '2'].forEach(function(patchNum) { + } + for (const patchNum of ['PARENT', '1', '2']) { assert.isFalse(element._computeLeftDisabled(patchNum, patchRange)); - }); + } assert.isTrue(element._computeLeftDisabled('3', patchRange)); patchRange.basePatchNum = '2'; @@ -61,11 +61,11 @@ assert.isFalse(element._computeRightDisabled('3', patchRange)); }); - test('navigation', function(done) { - var showStub = sinon.stub(page, 'show'); - var leftSelectEl = element.$.leftPatchSelect; - var rightSelectEl = element.$.rightPatchSelect; - var blurSpy = sinon.spy(leftSelectEl, 'blur'); + test('navigation', done => { + const showStub = sinon.stub(page, 'show'); + const leftSelectEl = element.$.leftPatchSelect; + const rightSelectEl = element.$.rightPatchSelect; + const blurSpy = sinon.spy(leftSelectEl, 'blur'); element.changeNum = '42'; element.path = 'path/to/file.txt'; element.availablePatches = ['1', '2', '3']; @@ -75,8 +75,8 @@ }; flushAsynchronousOperations(); - var numEvents = 0; - leftSelectEl.addEventListener('change', function(e) { + let numEvents = 0; + leftSelectEl.addEventListener('change', e => { numEvents++; if (numEvents == 1) { assert(showStub.lastCall.calledWithExactly( @@ -98,23 +98,23 @@ element.fire('change', {}, {node: leftSelectEl}); }); - test('filesWeblinks', function() { + test('filesWeblinks', () => { element.filesWeblinks = { meta_a: [ { name: 'foo', url: 'f.oo', - } + }, ], meta_b: [ { name: 'bar', url: 'ba.r', - } + }, ], }; flushAsynchronousOperations(); - var domApi = Polymer.dom(element.root); + const domApi = Polymer.dom(element.root); assert.equal( domApi.querySelector('a[href="f.oo"]').textContent, 'foo'); assert.equal(
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js index 1425a79..aa55414 100644 --- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js +++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -14,13 +14,13 @@ (function() { 'use strict'; - var HOVER_PATH_PATTERN = /^comments\.(left|right)\.\#(\d+)\.__hovering$/; - var SPLICE_PATH_PATTERN = /^comments\.(left|right)\.splices$/; + const HOVER_PATH_PATTERN = /^comments\.(left|right)\.\#(\d+)\.__hovering$/; + const SPLICE_PATH_PATTERN = /^comments\.(left|right)\.splices$/; - var RANGE_HIGHLIGHT = 'range'; - var HOVER_HIGHLIGHT = 'rangeHighlight'; + const RANGE_HIGHLIGHT = 'range'; + const HOVER_HIGHLIGHT = 'rangeHighlight'; - var NORMALIZE_RANGE_EVENT = 'normalize-range'; + const NORMALIZE_RANGE_EVENT = 'normalize-range'; Polymer({ is: 'gr-ranged-comment-layer', @@ -29,11 +29,11 @@ comments: Object, _listeners: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _commentMap: { type: Object, - value: function() { return {left: [], right: []}; }, + value() { return {left: [], right: []}; }, }, }, @@ -47,8 +47,8 @@ * annotation to. * @param {GrDiffLine} line The line object. */ - annotate: function(el, line) { - var ranges = []; + annotate(el, line) { + let ranges = []; if (line.type === GrDiffLine.Type.REMOVE || ( line.type === GrDiffLine.Type.BOTH && el.getAttribute('data-side') !== 'right')) { @@ -60,11 +60,11 @@ ranges = ranges.concat(this._getRangesForLine(line, 'right')); } - ranges.forEach(function(range) { + for (const range of ranges) { GrAnnotation.annotateElement(el, range.start, range.end - range.start, range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT); - }); + } }, /** @@ -73,7 +73,7 @@ * Should accept as arguments the line numbers for the start and end of * the update and the side as a string. */ - addListener: function(fn) { + addListener(fn) { this._listeners.push(fn); }, @@ -83,10 +83,10 @@ * @param {Number} end The line where the update ends. * @param {String} side The side of the update. ('left' or 'right') */ - _notifyUpdateRange: function(start, end, side) { - this._listeners.forEach(function(listener) { + _notifyUpdateRange(start, end, side) { + for (const listener of this._listeners) { listener(start, end, side); - }); + } }, /** @@ -94,7 +94,7 @@ * emitting appropriate update notifications. * @param {Object} record The change record. */ - _handleCommentChange: function(record) { + _handleCommentChange(record) { if (!record.path) { return; } // If the entire set of comments was changed. @@ -105,11 +105,13 @@ } // If the change only changed the `hovering` property of a comment. - var match = record.path.match(HOVER_PATH_PATTERN); + let match = record.path.match(HOVER_PATH_PATTERN); + let side; + if (match) { - var side = match[1]; - var index = match[2]; - var comment = this.comments[side][index]; + side = match[1]; + const index = match[2]; + const comment = this.comments[side][index]; if (comment && comment.range) { this._commentMap[side] = this._computeCommentMap(this.comments[side]); this._notifyUpdateRange( @@ -121,7 +123,7 @@ // If comments were spliced in or out. match = record.path.match(SPLICE_PATH_PATTERN); if (match) { - var side = match[1]; + side = match[1]; this._commentMap[side] = this._computeCommentMap(this.comments[side]); this._handleCommentSplice(record.value, side); } @@ -134,44 +136,45 @@ * @param {Array<Object>} commentList The list of comments. * @return {Object} The sparse list. */ - _computeCommentMap: function(commentList) { - var result = {}; - commentList.forEach(function(comment) { - if (!comment.range) { return; } - var range = comment.range; - for (var line = range.start_line; line <= range.end_line; line++) { + _computeCommentMap(commentList) { + const result = {}; + for (const comment of commentList) { + if (!comment.range) { continue; } + const range = comment.range; + for (let line = range.start_line; line <= range.end_line; line++) { if (!result[line]) { result[line] = []; } result[line].push({ - comment: comment, + comment, start: line === range.start_line ? range.start_character : 0, end: line === range.end_line ? range.end_character : -1, }); } - }); + } return result; }, /** * Translate a splice record into range update notifications. */ - _handleCommentSplice: function(record, side) { + _handleCommentSplice(record, side) { if (!record || !record.indexSplices) { return; } - record.indexSplices.forEach(function(splice) { - var ranges = splice.removed.length ? - splice.removed.map(function(c) { return c.range; }) : - [splice.object[splice.index].range]; - ranges.forEach(function(range) { - if (!range) { return; } + + for (const splice of record.indexSplices) { + const ranges = splice.removed.length ? + splice.removed.map(c => { return c.range; }) : + [splice.object[splice.index].range]; + for (const range of ranges) { + if (!range) { continue; } this._notifyUpdateRange(range.start_line, range.end_line, side); - }.bind(this)); - }.bind(this)); + } + } }, - _getRangesForLine: function(line, side) { - var lineNum = side === 'left' ? line.beforeNumber : line.afterNumber; - var ranges = this.get(['_commentMap', side, lineNum]) || []; + _getRangesForLine(line, side) { + const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber; + const ranges = this.get(['_commentMap', side, lineNum]) || []; return ranges - .map(function(range) { + .map(range => { range = { start: range.start, end: range.end === -1 ? line.text.length : range.end, @@ -189,11 +192,9 @@ } return range; - }.bind(this)) - .sort(function(a, b) { - // Sort the ranges so that hovering highlights are on top. - return a.hovering && !b.hovering ? 1 : 0; - }); + }) + // Sort the ranges so that hovering highlights are on top. + .sort((a, b) => a.hovering && !b.hovering ? 1 : 0); }, }); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html index 20fba4d..afdf630 100644 --- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html +++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../gr-diff/gr-diff-line.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-ranged-comment-layer.html"> <script>void(0);</script> @@ -34,12 +34,12 @@ </test-fixture> <script> - suite('gr-ranged-comment-layer', function() { - var element; - var sandbox; + suite('gr-ranged-comment-layer', () => { + let element; + let sandbox; - setup(function() { - var initialComments = { + setup(() => { + const initialComments = { left: [ { id: '12345', @@ -97,17 +97,17 @@ element.comments = initialComments; }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - suite('annotate', function() { - var sandbox; - var el; - var line; - var annotateElementStub; + suite('annotate', () => { + let sandbox; + let el; + let line; + let annotateElementStub; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); el = document.createElement('div'); @@ -116,11 +116,11 @@ line.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,'; }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('type=Remove no-comment', function() { + test('type=Remove no-comment', () => { line.type = GrDiffLine.Type.REMOVE; line.beforeNumber = 40; @@ -129,58 +129,58 @@ assert.isFalse(annotateElementStub.called); }); - test('type=Remove has-comment', function() { + test('type=Remove has-comment', () => { line.type = GrDiffLine.Type.REMOVE; line.beforeNumber = 36; - var expectedStart = 6; - var expectedLength = line.text.length - expectedStart; + const expectedStart = 6; + const expectedLength = line.text.length - expectedStart; element.annotate(el, line); assert.isTrue(annotateElementStub.called); - var lastCall = annotateElementStub.lastCall; + const lastCall = annotateElementStub.lastCall; assert.equal(lastCall.args[0], el); assert.equal(lastCall.args[1], expectedStart); assert.equal(lastCall.args[2], expectedLength); assert.equal(lastCall.args[3], 'range'); }); - test('type=Remove has-comment hovering', function() { + test('type=Remove has-comment hovering', () => { line.type = GrDiffLine.Type.REMOVE; line.beforeNumber = 36; element.set(['comments', 'left', 0, '__hovering'], true); - var expectedStart = 6; - var expectedLength = line.text.length - expectedStart; + const expectedStart = 6; + const expectedLength = line.text.length - expectedStart; element.annotate(el, line); assert.isTrue(annotateElementStub.called); - var lastCall = annotateElementStub.lastCall; + const lastCall = annotateElementStub.lastCall; assert.equal(lastCall.args[0], el); assert.equal(lastCall.args[1], expectedStart); assert.equal(lastCall.args[2], expectedLength); assert.equal(lastCall.args[3], 'rangeHighlight'); }); - test('type=Both has-comment', function() { + test('type=Both has-comment', () => { line.type = GrDiffLine.Type.BOTH; line.beforeNumber = 36; - var expectedStart = 6; - var expectedLength = line.text.length - expectedStart; + const expectedStart = 6; + const expectedLength = line.text.length - expectedStart; element.annotate(el, line); assert.isTrue(annotateElementStub.called); - var lastCall = annotateElementStub.lastCall; + const lastCall = annotateElementStub.lastCall; assert.equal(lastCall.args[0], el); assert.equal(lastCall.args[1], expectedStart); assert.equal(lastCall.args[2], expectedLength); assert.equal(lastCall.args[3], 'range'); }); - test('type=Both has-comment off side', function() { + test('type=Both has-comment off side', () => { line.type = GrDiffLine.Type.BOTH; line.beforeNumber = 36; el.setAttribute('data-side', 'right'); @@ -190,18 +190,18 @@ assert.isFalse(annotateElementStub.called); }); - test('type=Add has-comment', function() { + test('type=Add has-comment', () => { line.type = GrDiffLine.Type.ADD; line.afterNumber = 12; el.setAttribute('data-side', 'right'); - var expectedStart = 0; - var expectedLength = 22; + const expectedStart = 0; + const expectedLength = 22; element.annotate(el, line); assert.isTrue(annotateElementStub.called); - var lastCall = annotateElementStub.lastCall; + const lastCall = annotateElementStub.lastCall; assert.equal(lastCall.args[0], el); assert.equal(lastCall.args[1], expectedStart); assert.equal(lastCall.args[2], expectedLength); @@ -209,9 +209,9 @@ }); }); - test('_handleCommentChange overwrite', function() { - var handlerSpy = sandbox.spy(element, '_handleCommentChange'); - var mapSpy = sandbox.spy(element, '_computeCommentMap'); + test('_handleCommentChange overwrite', () => { + const handlerSpy = sandbox.spy(element, '_handleCommentChange'); + const mapSpy = sandbox.spy(element, '_computeCommentMap'); element.set('comments', {left: [], right: []}); @@ -222,10 +222,10 @@ assert.equal(Object.keys(element._commentMap.right).length, 0); }); - test('_handleCommentChange hovering', function() { - var handlerSpy = sandbox.spy(element, '_handleCommentChange'); - var mapSpy = sandbox.spy(element, '_computeCommentMap'); - var notifyStub = sinon.stub(); + test('_handleCommentChange hovering', () => { + const handlerSpy = sandbox.spy(element, '_handleCommentChange'); + const mapSpy = sandbox.spy(element, '_computeCommentMap'); + const notifyStub = sinon.stub(); element.addListener(notifyStub); element.set(['comments', 'right', 0, '__hovering'], true); @@ -234,16 +234,16 @@ assert.isTrue(mapSpy.called); assert.isTrue(notifyStub.called); - var lastCall = notifyStub.lastCall; + const lastCall = notifyStub.lastCall; assert.equal(lastCall.args[0], 10); assert.equal(lastCall.args[1], 12); assert.equal(lastCall.args[2], 'right'); }); - test('_handleCommentChange splice out', function() { - var handlerSpy = sandbox.spy(element, '_handleCommentChange'); - var mapSpy = sandbox.spy(element, '_computeCommentMap'); - var notifyStub = sinon.stub(); + test('_handleCommentChange splice out', () => { + const handlerSpy = sandbox.spy(element, '_handleCommentChange'); + const mapSpy = sandbox.spy(element, '_computeCommentMap'); + const notifyStub = sinon.stub(); element.addListener(notifyStub); element.splice('comments.right', 0, 1); @@ -252,16 +252,16 @@ assert.isTrue(mapSpy.called); assert.isTrue(notifyStub.called); - var lastCall = notifyStub.lastCall; + const lastCall = notifyStub.lastCall; assert.equal(lastCall.args[0], 10); assert.equal(lastCall.args[1], 12); assert.equal(lastCall.args[2], 'right'); }); - test('_handleCommentChange splice in', function() { - var handlerSpy = sandbox.spy(element, '_handleCommentChange'); - var mapSpy = sandbox.spy(element, '_computeCommentMap'); - var notifyStub = sinon.stub(); + test('_handleCommentChange splice in', () => { + const handlerSpy = sandbox.spy(element, '_handleCommentChange'); + const mapSpy = sandbox.spy(element, '_computeCommentMap'); + const notifyStub = sinon.stub(); element.addListener(notifyStub); element.splice('comments.left', element.comments.left.length, 0, { @@ -280,16 +280,16 @@ assert.isTrue(mapSpy.called); assert.isTrue(notifyStub.called); - var lastCall = notifyStub.lastCall; + const lastCall = notifyStub.lastCall; assert.equal(lastCall.args[0], 250); assert.equal(lastCall.args[1], 275); assert.equal(lastCall.args[2], 'left'); }); - test('_computeCommentMap creates maps correctly', function() { + test('_computeCommentMap creates maps correctly', () => { // There is only one ranged comment on the left, but it spans ll.36-39. - var leftKeys = []; - for (var i = 36; i <= 39; i++) { leftKeys.push('' + i); } + const leftKeys = []; + for (let i = 36; i <= 39; i++) { leftKeys.push('' + i); } assert.deepEqual(Object.keys(element._commentMap.left).sort(), leftKeys.sort()); @@ -311,8 +311,8 @@ // The right has two ranged comments, one spanning ll.10-12 and the other // on line 100. - var rightKeys = []; - for (i = 10; i <= 12; i++) { rightKeys.push('' + i); } + const rightKeys = []; + for (let i = 10; i <= 12; i++) { rightKeys.push('' + i); } rightKeys.push('55', '100'); assert.deepEqual(Object.keys(element._commentMap.right).sort(), rightKeys.sort()); @@ -334,14 +334,14 @@ assert.equal(element._commentMap.right[100][0].end, 15); }); - test('_getRangesForLine normalizes invalid ranges', function() { - var line = { + test('_getRangesForLine normalizes invalid ranges', () => { + const line = { afterNumber: 55, - text: '_getRangesForLine normalizes invalid ranges' + text: '_getRangesForLine normalizes invalid ranges', }; - var ranges = element._getRangesForLine(line, 'right'); + const ranges = element._getRangesForLine(line, 'right'); assert.equal(ranges.length, 1); - var range = ranges[0]; + const range = ranges[0]; assert.isTrue(range.start < range.end, 'start and end are normalized'); assert.equal(range.end, line.text.length); });
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html index 5f74f1f..db1cc19 100644 --- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html +++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.html
@@ -16,10 +16,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-selection-action-box"> <template> - <style> + <style include="shared-styles"> :host { --gr-arrow-size: .65em;
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js index 0f7f2f2..c228235 100644 --- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js +++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -26,7 +26,7 @@ properties: { keyEventTarget: { type: Object, - value: function() { return document.body; }, + value() { return document.body; }, }, range: { type: Object, @@ -48,27 +48,28 @@ ], listeners: { - 'mousedown': '_handleMouseDown', // See https://crbug.com/gerrit/4767 + mousedown: '_handleMouseDown', // See https://crbug.com/gerrit/4767 }, keyBindings: { - 'c': '_handleCKey', + c: '_handleCKey', }, - placeAbove: function(el) { - var rect = this._getTargetBoundingRect(el); - var boxRect = this.getBoundingClientRect(); - var parentRect = this.parentElement.getBoundingClientRect(); + placeAbove(el) { + Polymer.dom.flush(); + const rect = this._getTargetBoundingRect(el); + const boxRect = this.getBoundingClientRect(); + const parentRect = this.parentElement.getBoundingClientRect(); this.style.top = - rect.top - parentRect.top - boxRect.height - 4 + 'px'; + rect.top - parentRect.top - boxRect.height - 6 + 'px'; this.style.left = rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px'; }, - _getTargetBoundingRect: function(el) { - var rect; + _getTargetBoundingRect(el) { + let rect; if (el instanceof Text) { - var range = document.createRange(); + const range = document.createRange(); range.selectNode(el); rect = range.getBoundingClientRect(); range.detach(); @@ -78,7 +79,7 @@ return rect; }, - _handleCKey: function(e) { + _handleCKey(e) { if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { return; } @@ -86,13 +87,14 @@ this._fireCreateComment(); }, - _handleMouseDown: function(e) { + _handleMouseDown(e) { + if (e.button !== 0) { return; } // 0 = main button e.preventDefault(); e.stopPropagation(); this._fireCreateComment(); }, - _fireCreateComment: function() { + _fireCreateComment() { this.fire('create-comment', {side: this.side, range: this.range}); }, });
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html index e9ac0a5..8c70772 100644 --- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html +++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-selection-action-box.html"> <script>void(0);</script> @@ -36,33 +35,57 @@ </test-fixture> <script> - suite('gr-selection-action-box', function() { - var container; - var element; + suite('gr-selection-action-box', () => { + let container; + let element; - setup(function() { + setup(() => { container = fixture('basic'); element = container.querySelector('gr-selection-action-box'); sinon.stub(element, 'fire'); }); - teardown(function() { + teardown(() => { element.fire.restore(); }); - test('ignores regular keys', function() { + test('ignores regular keys', () => { MockInteractions.pressAndReleaseKeyOn(document.body, 27, null, 'esc'); assert.isFalse(element.fire.called); }); - test('reacts to hotkey', function() { + test('reacts to hotkey', () => { MockInteractions.pressAndReleaseKeyOn(document.body, 67, null, 'c'); assert.isTrue(element.fire.called); }); - test('event fired contains playload', function() { - var side = 'left'; - var range = { + suite('mousedown reacts only to main button', () => { + let e; + + setup(() => { + e = { + button: 0, + preventDefault: sinon.stub(), + stopPropagation: sinon.stub(), + }; + sinon.stub(element, '_fireCreateComment'); + }); + + test('event handled if main button', () => { + element._handleMouseDown(e); + assert.isTrue(e.preventDefault.called); + }); + + test('event ignored if not main button', () => { + e.button = 1; + element._handleMouseDown(e); + assert.isFalse(e.preventDefault.called); + }); + }); + + test('event fired contains playload', () => { + const side = 'left'; + const range = { startLine: 1, startChar: 11, endLine: 2, @@ -74,15 +97,15 @@ assert(element.fire.calledWithExactly( 'create-comment', { - side: side, - range: range, + side, + range, })); }); - suite('placeAbove', function() { - var target; + suite('placeAbove', () => { + let target; - setup(function() { + setup(() => { target = container.querySelector('.target'); sinon.stub(container, 'getBoundingClientRect').returns( {top: 1, bottom: 2, left: 3, right: 4, width: 50, height: 6}); @@ -92,25 +115,25 @@ {width: 10, height: 10}); }); - teardown(function() { + teardown(() => { element.getBoundingClientRect.restore(); container.getBoundingClientRect.restore(); element._getTargetBoundingRect.restore(); }); - test('placeAbove for Element argument', function() { + test('placeAbove for Element argument', () => { element.placeAbove(target); - assert.equal(element.style.top, '27px'); + assert.equal(element.style.top, '25px'); assert.equal(element.style.left, '72px'); }); - test('placeAbove for Text Node argument', function() { + test('placeAbove for Text Node argument', () => { element.placeAbove(target.firstChild); - assert.equal(element.style.top, '27px'); + assert.equal(element.style.top, '25px'); assert.equal(element.style.left, '72px'); }); - test('uses document.createRange', function() { + test('uses document.createRange', () => { sinon.spy(document, 'createRange'); element._getTargetBoundingRect.restore(); sinon.spy(element, '_getTargetBoundingRect');
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js index 0914846..3ab8a5f 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js +++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -14,7 +14,7 @@ (function() { 'use strict'; - var LANGUAGE_MAP = { + const LANGUAGE_MAP = { 'application/dart': 'dart', 'application/json': 'json', 'application/typescript': 'typescript', @@ -31,6 +31,7 @@ 'text/x-go': 'go', 'text/x-haskell': 'haskell', 'text/x-java': 'java', + 'text/x-kotlin': 'kotlin', 'text/x-lua': 'lua', 'text/x-markdown': 'markdown', 'text/x-objectivec': 'objectivec', @@ -46,9 +47,9 @@ 'text/x-swift': 'swift', 'text/x-yaml': 'yaml', }; - var ASYNC_DELAY = 10; + const ASYNC_DELAY = 10; - var CLASS_WHITELIST = { + const CLASS_WHITELIST = { 'gr-diff gr-syntax gr-syntax-literal': true, 'gr-diff gr-syntax gr-syntax-keyword': true, 'gr-diff gr-syntax gr-syntax-selector-tag': true, @@ -77,11 +78,11 @@ 'gr-diff gr-syntax gr-syntax-selector-class': true, }; - var CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</; - var CPP_WCHAR_PATTERN = /L\'.\'/g; - var JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g; - var GO_BACKSLASH_LITERAL = '\'\\\\\''; - var GLOBAL_LT_PATTERN = /</g; + const CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</; + const CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g; + const JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g; + const GO_BACKSLASH_LITERAL = '\'\\\\\''; + const GLOBAL_LT_PATTERN = /</g; Polymer({ is: 'gr-syntax-layer', @@ -97,23 +98,23 @@ }, _baseRanges: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _revisionRanges: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _baseLanguage: String, _revisionLanguage: String, _listeners: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _processHandle: Number, _hljs: Object, }, - addListener: function(fn) { + addListener(fn) { this.push('_listeners', fn); }, @@ -123,11 +124,11 @@ * @param {!HTMLElement} el * @param {!GrDiffLine} line */ - annotate: function(el, line) { + annotate(el, line) { if (!this.enabled) { return; } // Determine the side. - var side; + let side; if (line.type === GrDiffLine.Type.REMOVE || ( line.type === GrDiffLine.Type.BOTH && el.getAttribute('data-side') !== 'right')) { @@ -138,7 +139,7 @@ } // Find the relevant syntax ranges, if any. - var ranges = []; + let ranges = []; if (side === 'left' && this._baseRanges.length >= line.beforeNumber) { ranges = this._baseRanges[line.beforeNumber - 1] || []; } else if (side === 'right' && @@ -147,10 +148,10 @@ } // Apply the ranges to the element. - ranges.forEach(function(range) { + for (const range of ranges) { GrAnnotation.annotateElement( el, range.start, range.length, range.className); - }); + } }, /** @@ -158,7 +159,7 @@ * as syntax info comes online. * @return {Promise} */ - process: function() { + process() { // Discard existing ranges. this._baseRanges = []; this._revisionRanges = []; @@ -179,7 +180,7 @@ return Promise.resolve(); } - var state = { + const state = { sectionIndex: 0, lineIndex: 0, baseContext: undefined, @@ -188,9 +189,9 @@ lastNotify: {left: 1, right: 1}, }; - return this._loadHLJS().then(function() { - return new Promise(function(resolve) { - var nextStep = function() { + return this._loadHLJS().then(() => { + return new Promise(resolve => { + const nextStep = () => { this._processHandle = null; this._processNextLine(state); @@ -219,21 +220,21 @@ }; this._processHandle = this.async(nextStep, 1); - }.bind(this)); - }.bind(this)); + }); + }); }, /** * Cancel any asynchronous syntax processing jobs. */ - cancel: function() { + cancel() { if (this._processHandle) { this.cancelAsync(this._processHandle); this._processHandle = null; } }, - _diffChanged: function() { + _diffChanged() { this.cancel(); this._baseRanges = []; this._revisionRanges = []; @@ -246,17 +247,16 @@ * @param {string} str The string of HTML. * @return {!Array<!Object>} The list of ranges. */ - _rangesFromString: function(str) { - var div = document.createElement('div'); + _rangesFromString(str) { + const div = document.createElement('div'); div.innerHTML = str; return this._rangesFromElement(div, 0); }, - _rangesFromElement: function(elem, offset) { - var result = []; - for (var i = 0; i < elem.childNodes.length; i++) { - var node = elem.childNodes[i]; - var nodeLength = GrAnnotation.getLength(node); + _rangesFromElement(elem, offset) { + let result = []; + for (const node of elem.childNodes) { + const nodeLength = GrAnnotation.getLength(node); // Note: HLJS may emit a span with class undefined when it thinks there // may be a syntax error. if (node.tagName === 'SPAN' && node.className !== 'undefined') { @@ -281,11 +281,11 @@ * lines). * @param {!Object} state The processing state for the layer. */ - _processNextLine: function(state) { - var baseLine; - var revisionLine; + _processNextLine(state) { + let baseLine; + let revisionLine; - var section = this.diff.content[state.sectionIndex]; + const section = this.diff.content[state.sectionIndex]; if (section.ab) { baseLine = section.ab[state.lineIndex]; revisionLine = section.ab[state.lineIndex]; @@ -303,7 +303,7 @@ } // To store the result of the syntax highlighter. - var result; + let result; if (this._baseLanguage && baseLine !== undefined) { baseLine = this._workaround(this._baseLanguage, baseLine); @@ -341,7 +341,7 @@ * @param {!string} line The line of code to potentially rewrite. * @return {string} A potentially-rewritten line of code. */ - _workaround: function(language, line) { + _workaround(language, line) { if (language === 'cpp') { /** * Prevent confusing < and << operators for the start of a meta string @@ -360,7 +360,7 @@ * {#see https://github.com/isagalaev/highlight.js/issues/1412} */ if (CPP_WCHAR_PATTERN.test(line)) { - line = line.replace(CPP_WCHAR_PATTERN, 'L"."'); + line = line.replace(CPP_WCHAR_PATTERN, 'L"$1."'); } return line; @@ -382,7 +382,7 @@ * {@see Issue 5007} * {#see https://github.com/isagalaev/highlight.js/issues/1411} */ - if (language === 'go' && line.indexOf(GO_BACKSLASH_LITERAL) !== -1) { + if (language === 'go' && line.includes(GO_BACKSLASH_LITERAL)) { return line.replace(GO_BACKSLASH_LITERAL, '"\\\\"'); } @@ -394,8 +394,8 @@ * @param {!Object} state * @return {boolean} */ - _isSectionDone: function(state) { - var section = this.diff.content[state.sectionIndex]; + _isSectionDone(state) { + const section = this.diff.content[state.sectionIndex]; if (section.ab) { return state.lineIndex >= section.ab.length; } else { @@ -409,33 +409,33 @@ * that have not yet been notified. * @param {!Object} state */ - _notify: function(state) { + _notify(state) { if (state.lineNums.left - state.lastNotify.left) { this._notifyRange( - state.lastNotify.left, - state.lineNums.left, - 'left'); + state.lastNotify.left, + state.lineNums.left, + 'left'); state.lastNotify.left = state.lineNums.left; } if (state.lineNums.right - state.lastNotify.right) { this._notifyRange( - state.lastNotify.right, - state.lineNums.right, - 'right'); + state.lastNotify.right, + state.lineNums.right, + 'right'); state.lastNotify.right = state.lineNums.right; } }, - _notifyRange: function(start, end, side) { - this._listeners.forEach(function(fn) { + _notifyRange(start, end, side) { + for (const fn of this._listeners) { fn(start, end, side); - }); + } }, - _loadHLJS: function() { - return this.$.libLoader.get().then(function(hljs) { + _loadHLJS() { + return this.$.libLoader.get().then(hljs => { this._hljs = hljs; - }.bind(this)); + }); }, }); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html index eaa8d29..98a6f8e 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html +++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -20,7 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html"> <link rel="import" href="gr-syntax-layer.html"> @@ -33,17 +33,17 @@ </test-fixture> <script> - suite('gr-syntax-layer tests', function() { - var sandbox; - var diff; - var element; + suite('gr-syntax-layer tests', () => { + let sandbox; + let diff; + let element; function getMockHLJS() { - var html = '<span class="gr-diff gr-syntax gr-syntax-string">' + + const html = '<span class="gr-diff gr-syntax gr-syntax-string">' + 'ipsum</span>'; return { - configure: function() {}, - highlight: function(lang, line, ignore, state) { + configure() {}, + highlight(lang, line, ignore, state) { return { value: line.replace(/ipsum/, html), top: state === undefined ? 1 : state + 1, @@ -52,23 +52,23 @@ }; } - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); element = fixture('basic'); - var mock = document.createElement('mock-diff-response'); + const mock = document.createElement('mock-diff-response'); diff = mock.diffResponse; element.diff = diff; }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('annotate without range does nothing', function() { - var annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement'); - var el = document.createElement('div'); + test('annotate without range does nothing', () => { + const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement'); + const el = document.createElement('div'); el.textContent = 'Etiam dui, blandit wisi.'; - var line = new GrDiffLine(GrDiffLine.Type.REMOVE); + const line = new GrDiffLine(GrDiffLine.Type.REMOVE); line.beforeNumber = 12; element.annotate(el, line); @@ -76,21 +76,21 @@ assert.isFalse(annotationSpy.called); }); - test('annotate with range applies it', function() { - var str = 'Etiam dui, blandit wisi.'; - var start = 6; - var length = 3; - var className = 'foobar'; + test('annotate with range applies it', () => { + const str = 'Etiam dui, blandit wisi.'; + const start = 6; + const length = 3; + const className = 'foobar'; - var annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement'); - var el = document.createElement('div'); + const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement'); + const el = document.createElement('div'); el.textContent = str; - var line = new GrDiffLine(GrDiffLine.Type.REMOVE); + const line = new GrDiffLine(GrDiffLine.Type.REMOVE); line.beforeNumber = 12; element._baseRanges[11] = [{ - start: start, - length: length, - className: className, + start, + length, + className, }]; element.annotate(el, line); @@ -103,21 +103,21 @@ assert.isOk(el.querySelector('hl.' + className)); }); - test('annotate with range but disabled does nothing', function() { - var str = 'Etiam dui, blandit wisi.'; - var start = 6; - var length = 3; - var className = 'foobar'; + test('annotate with range but disabled does nothing', () => { + const str = 'Etiam dui, blandit wisi.'; + const start = 6; + const length = 3; + const className = 'foobar'; - var annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement'); - var el = document.createElement('div'); + const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement'); + const el = document.createElement('div'); el.textContent = str; - var line = new GrDiffLine(GrDiffLine.Type.REMOVE); + const line = new GrDiffLine(GrDiffLine.Type.REMOVE); line.beforeNumber = 12; element._baseRanges[11] = [{ - start: start, - length: length, - className: className, + start, + length, + className, }]; element.enabled = false; @@ -126,17 +126,17 @@ assert.isFalse(annotationSpy.called); }); - test('process on empty diff does nothing', function(done) { + test('process on empty diff does nothing', done => { element.diff = { meta_a: {content_type: 'application/json'}, meta_b: {content_type: 'application/json'}, content: [], }; - var processNextSpy = sandbox.spy(element, '_processNextLine'); + const processNextSpy = sandbox.spy(element, '_processNextLine'); - var processPromise = element.process(); + const processPromise = element.process(); - processPromise.then(function() { + processPromise.then(() => { assert.isFalse(processNextSpy.called); assert.equal(element._baseRanges.length, 0); assert.equal(element._revisionRanges.length, 0); @@ -144,17 +144,17 @@ }); }); - test('process for unsupported languages does nothing', function(done) { + test('process for unsupported languages does nothing', done => { element.diff = { meta_a: {content_type: 'text/x+objective-cobol-plus-plus'}, meta_b: {content_type: 'application/not-a-real-language'}, content: [], }; - var processNextSpy = sandbox.spy(element, '_processNextLine'); + const processNextSpy = sandbox.spy(element, '_processNextLine'); - var processPromise = element.process(); + const processPromise = element.process(); - processPromise.then(function() { + processPromise.then(() => { assert.isFalse(processNextSpy.called); assert.equal(element._baseRanges.length, 0); assert.equal(element._revisionRanges.length, 0); @@ -162,14 +162,14 @@ }); }); - test('process while disabled does nothing', function(done) { - var processNextSpy = sandbox.spy(element, '_processNextLine'); + test('process while disabled does nothing', done => { + const processNextSpy = sandbox.spy(element, '_processNextLine'); element.enabled = false; - var loadHLJSSpy = sandbox.spy(element, '_loadHLJS'); + const loadHLJSSpy = sandbox.spy(element, '_loadHLJS'); - var processPromise = element.process(); + const processPromise = element.process(); - processPromise.then(function() { + processPromise.then(() => { assert.isFalse(processNextSpy.called); assert.equal(element._baseRanges.length, 0); assert.equal(element._revisionRanges.length, 0); @@ -178,20 +178,20 @@ }); }); - test('process highlight ipsum', function(done) { + test('process highlight ipsum', done => { element.diff.meta_a.content_type = 'application/json'; element.diff.meta_b.content_type = 'application/json'; - var mockHLJS = getMockHLJS(); - var highlightSpy = sinon.spy(mockHLJS, 'highlight'); + const mockHLJS = getMockHLJS(); + const highlightSpy = sinon.spy(mockHLJS, 'highlight'); sandbox.stub(element.$.libLoader, 'get', - function() { return Promise.resolve(mockHLJS); }); - var processNextSpy = sandbox.spy(element, '_processNextLine'); - var processPromise = element.process(); + () => { return Promise.resolve(mockHLJS); }); + const processNextSpy = sandbox.spy(element, '_processNextLine'); + const processPromise = element.process(); - processPromise.then(function() { - var linesA = diff.meta_a.lines; - var linesB = diff.meta_b.lines; + processPromise.then(() => { + const linesA = diff.meta_a.lines; + const linesB = diff.meta_b.lines; assert.isTrue(processNextSpy.called); assert.equal(element._baseRanges.length, linesA); @@ -200,37 +200,39 @@ assert.equal(highlightSpy.callCount, linesA + linesB); // The first line of both sides have a range. - [element._baseRanges[0], element._revisionRanges[0]] - .forEach(function(range) { - assert.equal(range.length, 1); - assert.equal(range[0].className, - 'gr-diff gr-syntax gr-syntax-string'); - assert.equal(range[0].start, 'lorem '.length); - assert.equal(range[0].length, 'ipsum'.length); - }); + let ranges = [element._baseRanges[0], element._revisionRanges[0]]; + for (const range of ranges) { + assert.equal(range.length, 1); + assert.equal(range[0].className, + 'gr-diff gr-syntax gr-syntax-string'); + assert.equal(range[0].start, 'lorem '.length); + assert.equal(range[0].length, 'ipsum'.length); + } // There are no ranges from ll.1-12 on the left and ll.1-11 on the // right. - element._baseRanges.slice(1, 12) - .concat(element._revisionRanges.slice(1, 11)) - .forEach(function(range) { - assert.equal(range.length, 0); - }); + ranges = element._baseRanges.slice(1, 12) + .concat(element._revisionRanges.slice(1, 11)); + + for (const range of ranges) { + assert.equal(range.length, 0); + } // There should be another pair of ranges on l.13 for the left and // l.12 for the right. - [element._baseRanges[13], element._revisionRanges[12]] - .forEach(function(range) { - assert.equal(range.length, 1); - assert.equal(range[0].className, - 'gr-diff gr-syntax gr-syntax-string'); - assert.equal(range[0].start, 32); - assert.equal(range[0].length, 'ipsum'.length); - }); + ranges = [element._baseRanges[13], element._revisionRanges[12]]; + + for (const range of ranges) { + assert.equal(range.length, 1); + assert.equal(range[0].className, + 'gr-diff gr-syntax gr-syntax-string'); + assert.equal(range[0].start, 32); + assert.equal(range[0].length, 'ipsum'.length); + } // The next group should have a similar instance on either side. - var range = element._baseRanges[15]; + let range = element._baseRanges[15]; assert.equal(range.length, 1); assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string'); assert.equal(range[0].start, 34); @@ -246,38 +248,38 @@ }); }); - test('_diffChanged calls cancel', function() { - var cancelSpy = sandbox.spy(element, '_diffChanged'); + test('_diffChanged calls cancel', () => { + const cancelSpy = sandbox.spy(element, '_diffChanged'); element.diff = {content: []}; assert.isTrue(cancelSpy.called); }); - test('_rangesFromElement no ranges', function() { - var elem = document.createElement('span'); + test('_rangesFromElement no ranges', () => { + const elem = document.createElement('span'); elem.textContent = 'Etiam dui, blandit wisi.'; - var offset = 100; + const offset = 100; - var result = element._rangesFromElement(elem, offset); + const result = element._rangesFromElement(elem, offset); assert.equal(result.length, 0); }); - test('_rangesFromElement single range', function() { - var str0 = 'Etiam '; - var str1 = 'dui, blandit'; - var str2 = ' wisi.'; - var className = 'gr-diff gr-syntax gr-syntax-string'; - var offset = 100; + test('_rangesFromElement single range', () => { + const str0 = 'Etiam '; + const str1 = 'dui, blandit'; + const str2 = ' wisi.'; + const className = 'gr-diff gr-syntax gr-syntax-string'; + const offset = 100; - var elem = document.createElement('span'); + const elem = document.createElement('span'); elem.appendChild(document.createTextNode(str0)); - var span = document.createElement('span'); + const span = document.createElement('span'); span.textContent = str1; span.className = className; elem.appendChild(span); elem.appendChild(document.createTextNode(str2)); - var result = element._rangesFromElement(elem, offset); + const result = element._rangesFromElement(elem, offset); assert.equal(result.length, 1); assert.equal(result[0].start, str0.length + offset); @@ -285,37 +287,37 @@ assert.equal(result[0].className, className); }); - test('_rangesFromElement non-whitelist', function() { - var str0 = 'Etiam '; - var str1 = 'dui, blandit'; - var str2 = ' wisi.'; - var className = 'not-in-the-whitelist'; - var offset = 100; + test('_rangesFromElement non-whitelist', () => { + const str0 = 'Etiam '; + const str1 = 'dui, blandit'; + const str2 = ' wisi.'; + const className = 'not-in-the-whitelist'; + const offset = 100; - var elem = document.createElement('span'); + const elem = document.createElement('span'); elem.appendChild(document.createTextNode(str0)); - var span = document.createElement('span'); + const span = document.createElement('span'); span.textContent = str1; span.className = className; elem.appendChild(span); elem.appendChild(document.createTextNode(str2)); - var result = element._rangesFromElement(elem, offset); + const result = element._rangesFromElement(elem, offset); assert.equal(result.length, 0); }); - test('_rangesFromElement milti range', function() { - var str0 = 'Etiam '; - var str1 = 'dui,'; - var str2 = ' blandit'; - var str3 = ' wisi.'; - var className = 'gr-diff gr-syntax gr-syntax-string'; - var offset = 100; + test('_rangesFromElement milti range', () => { + const str0 = 'Etiam '; + const str1 = 'dui,'; + const str2 = ' blandit'; + const str3 = ' wisi.'; + const className = 'gr-diff gr-syntax gr-syntax-string'; + const offset = 100; - var elem = document.createElement('span'); + const elem = document.createElement('span'); elem.appendChild(document.createTextNode(str0)); - var span = document.createElement('span'); + let span = document.createElement('span'); span.textContent = str1; span.className = className; elem.appendChild(span); @@ -325,7 +327,7 @@ span.className = className; elem.appendChild(span); - var result = element._rangesFromElement(elem, offset); + const result = element._rangesFromElement(elem, offset); assert.equal(result.length, 2); @@ -339,27 +341,27 @@ assert.equal(result[1].className, className); }); - test('_rangesFromElement nested range', function() { - var str0 = 'Etiam '; - var str1 = 'dui,'; - var str2 = ' blandit'; - var str3 = ' wisi.'; - var className = 'gr-diff gr-syntax gr-syntax-string'; - var offset = 100; + test('_rangesFromElement nested range', () => { + const str0 = 'Etiam '; + const str1 = 'dui,'; + const str2 = ' blandit'; + const str3 = ' wisi.'; + const className = 'gr-diff gr-syntax gr-syntax-string'; + const offset = 100; - var elem = document.createElement('span'); + const elem = document.createElement('span'); elem.appendChild(document.createTextNode(str0)); - var span1 = document.createElement('span'); + const span1 = document.createElement('span'); span1.textContent = str1; span1.className = className; elem.appendChild(span1); - var span2 = document.createElement('span'); + const span2 = document.createElement('span'); span2.textContent = str2; span2.className = className; span1.appendChild(span2); elem.appendChild(document.createTextNode(str3)); - var result = element._rangesFromElement(elem, offset); + const result = element._rangesFromElement(elem, offset); assert.equal(result.length, 2); @@ -372,17 +374,17 @@ assert.equal(result[1].className, className); }); - test('_rangesFromString whitelist allows recursion', function() { - var str = [ - '<span class="non-whtelisted-class">', - '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>', - '</span>'].join(''); - var result = element._rangesFromString(str); + test('_rangesFromString whitelist allows recursion', () => { + const str = [ + '<span class="non-whtelisted-class">', + '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>', + '</span>'].join(''); + const result = element._rangesFromString(str); assert.notEqual(result.length, 0); }); - test('_isSectionDone', function() { - var state = {sectionIndex: 0, lineIndex: 0}; + test('_isSectionDone', () => { + let state = {sectionIndex: 0, lineIndex: 0}; assert.isFalse(element._isSectionDone(state)); state = {sectionIndex: 0, lineIndex: 2}; @@ -407,9 +409,9 @@ assert.isTrue(element._isSectionDone(state)); }); - test('workaround CPP LT directive', function() { + test('workaround CPP LT directive', () => { // Does nothing to regular line. - var line = 'int main(int argc, char** argv) { return 0; }'; + let line = 'int main(int argc, char** argv) { return 0; }'; assert.equal(element._workaround('cpp', line), line); // Does nothing to include directive. @@ -418,7 +420,7 @@ // Converts left-shift operator in #define. line = '#define GiB (1ull << 30)'; - var expected = '#define GiB (1ull || 30)'; + let expected = '#define GiB (1ull || 30)'; assert.equal(element._workaround('cpp', line), expected); // Converts less-than operator in #if. @@ -427,9 +429,9 @@ assert.equal(element._workaround('cpp', line), expected); }); - test('workaround Java param-annotation', function() { + test('workaround Java param-annotation', () => { // Does nothing to regular line. - var line = 'public static void foo(int bar) { }'; + let line = 'public static void foo(int bar) { }'; assert.equal(element._workaround('java', line), line); // Does nothing to regular annotation. @@ -438,14 +440,14 @@ // Converts parameterized annotation. line = 'public static void foo(@SuppressWarnings("unused") int bar) { }'; - var expected = 'public static void foo(@SuppressWarnings "unused" ' + + const expected = 'public static void foo(@SuppressWarnings "unused" ' + ' int bar) { }'; assert.equal(element._workaround('java', line), expected); }); - test('workaround CPP whcar_t character literals', function() { + test('workaround CPP whcar_t character literals', () => { // Does nothing to regular line. - var line = 'int main(int argc, char** argv) { return 0; }'; + let line = 'int main(int argc, char** argv) { return 0; }'; assert.equal(element._workaround('cpp', line), line); // Does nothing to wchar_t string. @@ -454,13 +456,18 @@ // Converts wchar_t character literal to string. line = 'wchar_t myChar = L\'#\''; - var expected = 'wchar_t myChar = L"."'; + let expected = 'wchar_t myChar = L"."'; + assert.equal(element._workaround('cpp', line), expected); + + // Converts wchar_t character literal with escape sequence to string. + line = 'wchar_t myChar = L\'\\"\''; + expected = 'wchar_t myChar = L"\\."'; assert.equal(element._workaround('cpp', line), expected); }); - test('workaround go backslash character literals', function() { + test('workaround go backslash character literals', () => { // Does nothing to regular line. - var line = 'func foo(in []byte) (lit []byte, n int, err error) {'; + let line = 'func foo(in []byte) (lit []byte, n int, err error) {'; assert.equal(element._workaround('go', line), line); // Does nothing to string with backslash literal @@ -469,7 +476,7 @@ // Converts backslash literal character to a string. line = 'c := \'\\\\\''; - var expected = 'c := "\\\\"'; + const expected = 'c := "\\\\"'; assert.equal(element._workaround('go', line), expected); }); });
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js index 520f24d..75b00e8 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js +++ b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js
@@ -14,8 +14,8 @@ (function() { 'use strict'; - var HLJS_PATH = 'bower_components/highlightjs/highlight.min.js'; - var LIB_ROOT_PATTERN = /(.+\/)elements\/gr-app\.html/; + const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js'; + const LIB_ROOT_PATTERN = /(.+\/)elements\/gr-app\.html/; Polymer({ is: 'gr-syntax-lib-loader', @@ -30,11 +30,11 @@ loading: false, callbacks: [], }, - } + }, }, - get: function() { - return new Promise(function(resolve) { + get() { + return new Promise(resolve => { // If the lib is totally loaded, resolve immediately. if (this._state.loaded) { resolve(this._getHighlightLib()); @@ -48,27 +48,29 @@ } this._state.callbacks.push(resolve); - }.bind(this)); + }); }, - _onLibLoaded: function() { - var lib = this._getHighlightLib(); + _onLibLoaded() { + const lib = this._getHighlightLib(); this._state.loaded = true; this._state.loading = false; - this._state.callbacks.forEach(function(cb) { cb(lib); }); + for (const cb of this._state.callbacks) { + cb(lib); + } this._state.callbacks = []; }, - _getHighlightLib: function() { + _getHighlightLib() { return window.hljs; }, - _configureHighlightLib: function() { + _configureHighlightLib() { this._getHighlightLib().configure( {classPrefix: 'gr-diff gr-syntax gr-syntax-'}); }, - _getLibRoot: function() { + _getLibRoot() { if (this._cachedLibRoot) { return this._cachedLibRoot; } return this._cachedLibRoot = document.head @@ -78,16 +80,16 @@ }, _cachedLibRoot: null, - _loadHLJS: function() { - return new Promise(function(resolve) { - var script = document.createElement('script'); + _loadHLJS() { + return new Promise(resolve => { + const script = document.createElement('script'); script.src = this._getLibRoot() + HLJS_PATH; script.onload = function() { this._configureHighlightLib(); resolve(); }.bind(this); Polymer.dom(document.head).appendChild(script); - }.bind(this)); + }); }, }); })();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html index 985fc6d..b46910a 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html +++ b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html
@@ -20,7 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-syntax-lib-loader.html"> <script>void(0);</script> @@ -32,26 +32,24 @@ </test-fixture> <script> - suite('gr-syntax-lib-loader tests', function() { - var element; - var resolveLoad; - var loadStub; + suite('gr-syntax-lib-loader tests', () => { + let element; + let resolveLoad; + let loadStub; - setup(function() { + setup(() => { element = fixture('basic'); - loadStub = sinon.stub(element, '_loadHLJS', function() { - return new Promise(function(resolve) { - resolveLoad = resolve; - }); - }); + loadStub = sinon.stub(element, '_loadHLJS', () => + new Promise(resolve => resolveLoad = resolve) + ); // Assert preconditions: assert.isFalse(element._state.loaded); assert.isFalse(element._state.loading); }); - teardown(function() { + teardown(() => { if (window.hljs) { delete window.hljs; } @@ -63,8 +61,8 @@ element._state.callbacks = []; }); - test('only load once', function(done) { - var firstCallHandler = sinon.stub(); + test('only load once', done => { + const firstCallHandler = sinon.stub(); element.get().then(firstCallHandler); // It should now be in the loading state. @@ -73,7 +71,7 @@ assert.isFalse(element._state.loaded); assert.isFalse(firstCallHandler.called); - var secondCallHandler = sinon.stub(); + const secondCallHandler = sinon.stub(); element.get().then(secondCallHandler); // No change in state. @@ -84,7 +82,7 @@ // Now load the library. resolveLoad(); - flush(function() { + flush(() => { // The state should be loaded and both handlers called. assert.isFalse(element._state.loading); assert.isTrue(element._state.loaded);
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html index fabc347..31a276f 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html +++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
@@ -29,8 +29,7 @@ color: #FF1717; } .gr-syntax-keyword { - color: #7F0055; - font-weight: bold; + color: #9E0069; line-height: 1em; } .gr-syntax-number, @@ -80,7 +79,7 @@ font-style: italic; } .gr-syntax-strong { - font-weight: bold; + font-weight: 700; } </style> </template>
diff --git a/polygerrit-ui/app/elements/gr-app-it_test.html b/polygerrit-ui/app/elements/gr-app-it_test.html new file mode 100644 index 0000000..7b134c1 --- /dev/null +++ b/polygerrit-ui/app/elements/gr-app-it_test.html
@@ -0,0 +1,92 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-app-it_test</title> + +<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../test/common-test-setup.html"/> +<link rel="import" href="gr-app.html"> + +<script>void(0);</script> + +<test-fixture id="element"> + <template> + <gr-app id="app"></gr-app> + </template> +</test-fixture> + +<script> + suite('gr-app integration tests', () => { + let sandbox; + let element; + + setup(done => { + sandbox = sinon.sandbox.create(); + stub('gr-reporting', { + appStarted: sandbox.stub(), + }); + stub('gr-account-dropdown', { + _getTopContent: sinon.stub(), + }); + stub('gr-rest-api-interface', { + getAccount() { return Promise.resolve(null); }, + getAccountCapabilities() { return Promise.resolve({}); }, + getConfig() { + return Promise.resolve({ + gerrit: {web_uis: ['GWT', 'POLYGERRIT']}, + plugin: { + js_resource_paths: [], + html_resource_paths: [ + new URL('test/plugin.html', window.location.href).toString(), + ], + }, + }); + }, + getVersion() { return Promise.resolve(42); }, + getLoggedIn() { return Promise.resolve(false); }, + }); + element = fixture('element'); + + const importSpy = sandbox.spy(element.$.externalStyle, '_import'); + Gerrit.awaitPluginsLoaded().then(() => { + Promise.all(importSpy.returnValues).then(() => { + flush(done); + }); + }); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('applies --primary-text-color', () => { + assert.equal( + element.getComputedStyleValue('--primary-text-color'), '#F00BAA'); + }); + + test('applies --header-background-color', () => { + assert.equal(element.getComputedStyleValue('--header-background-color'), + '#F01BAA'); + }); + test('applies --footer-background-color', () => { + assert.equal(element.getComputedStyleValue('--footer-background-color'), + '#F02BAA'); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html index c92610bf..754ca69d 100644 --- a/polygerrit-ui/app/elements/gr-app.html +++ b/polygerrit-ui/app/elements/gr-app.html
@@ -14,53 +14,69 @@ limitations under the License. --> -<link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../bower_components/polymer/polymer.html"> -<link rel="import" href="../styles/app-theme.html"> +<link rel="import" href="../bower_components/polymer-resin/standalone/polymer-resin.html"> +<script> + security.polymer_resin.install({ + allowedIdentifierPrefixes: [''], + reportHandler: security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER, + }); +</script> <link rel="import" href="./admin/gr-admin-view/gr-admin-view.html"> - -<link rel="import" href="./core/gr-error-manager/gr-error-manager.html"> -<link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html"> -<link rel="import" href="./core/gr-main-header/gr-main-header.html"> -<link rel="import" href="./core/gr-router/gr-router.html"> -<link rel="import" href="./core/gr-reporting/gr-reporting.html"> - <link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html"> <link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html"> <link rel="import" href="./change/gr-change-view/gr-change-view.html"> +<link rel="import" href="./core/gr-error-manager/gr-error-manager.html"> +<link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html"> +<link rel="import" href="./core/gr-main-header/gr-main-header.html"> +<link rel="import" href="./core/gr-reporting/gr-reporting.html"> +<link rel="import" href="./core/gr-router/gr-router.html"> <link rel="import" href="./diff/gr-diff-view/gr-diff-view.html"> +<link rel="import" href="./plugins/gr-external-style/gr-external-style.html"> +<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html"> <link rel="import" href="./settings/gr-cla-view/gr-cla-view.html"> <link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html"> <link rel="import" href="./settings/gr-settings-view/gr-settings-view.html"> - +<link rel="import" href="./shared/gr-fixed-panel/gr-fixed-panel.html"> <link rel="import" href="./shared/gr-overlay/gr-overlay.html"> <link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html"> +<link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> +<link rel="import" href="../styles/app-theme.html"> +<link rel="import" href="../styles/shared-styles.html"> <script src="../scripts/util.js"></script> <dom-module id="gr-app"> <template> - <style> + <style include="shared-styles"> :host { display: flex; - min-height: 100vh; + min-height: 100%; flex-direction: column; } + gr-fixed-panel { + /** + * This one should be greater that the z-index in gr-diff-view + * because gr-main-header contains overlay. + */ + z-index: 10; + } gr-main-header, footer { color: var(--primary-text-color); } gr-main-header { - background-color: var(--header-background-color, #eee); + background-color: var(--header-background-color); padding: 0 var(--default-horizontal-margin); } footer { - background-color: var(--footer-background-color, #eee); + background-color: var(--footer-background-color); display: flex; justify-content: space-between; padding: .5rem var(--default-horizontal-margin); + z-index: 100; } main { flex: 1; @@ -68,7 +84,7 @@ } .errorView { align-items: center; - display: flex; + display: none; flex-direction: column; justify-content: center; position: absolute; @@ -77,6 +93,9 @@ bottom: 0; left: 0; } + .errorView.show { + display: flex; + } .errorEmoji { font-size: 2.6em; } @@ -94,8 +113,10 @@ color: #b71c1c; } </style> - <gr-main-header id="mainHeader" search-query="{{params.query}}"> - </gr-main-header> + <gr-fixed-panel id="header"> + <gr-main-header id="mainHeader" search-query="{{params.query}}"> + </gr-main-header> + </gr-fixed-panel> <main> <template is="dom-if" if="[[_showChangeListView]]" restamp="true"> <gr-change-list-view @@ -112,7 +133,6 @@ <template is="dom-if" if="[[_showChangeView]]" restamp="true"> <gr-change-view params="[[params]]" - server-config="[[_serverConfig]]" view-state="{{_viewState.changeView}}" back-page="[[_lastSearchPage]]"></gr-change-view> </template> @@ -128,18 +148,19 @@ </gr-settings-view> </template> <template is="dom-if" if="[[_showAdminView]]" restamp="true"> - <gr-admin-view path="[[_path]]"></gr-admin-view> + <gr-admin-view path="[[_path]]" + params=[[params]]></gr-admin-view> </template> <template is="dom-if" if="[[_showCLAView]]" restamp="true"> <gr-cla-view path="[[_path]]"></gr-cla-view> </template> - <div id="errorView" class="errorView" hidden> + <div id="errorView" class="errorView"> <div class="errorEmoji">[[_lastError.emoji]]</div> <div class="errorText">[[_lastError.text]]</div> <div class="errorMoreInfo">[[_lastError.moreInfo]]</div> </div> </main> - <footer role="contentinfo"> + <footer r="contentinfo"> <div> Powered by <a href="https://www.gerritcodereview.com/" rel="noopener" target="_blank">Gerrit Code Review</a> @@ -171,6 +192,10 @@ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-reporting id="reporting"></gr-reporting> <gr-router id="router"></gr-router> + <gr-plugin-host id="plugins" + config="[[_serverConfig.plugin]]"> + </gr-plugin-host> + <gr-external-style id="externalStyle" name="app-theme"></gr-external-style> </template> <script src="gr-app.js" crossorigin="anonymous"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js index d820bc7..d4e1bb1 100644 --- a/polygerrit-ui/app/elements/gr-app.js +++ b/polygerrit-ui/app/elements/gr-app.js
@@ -34,7 +34,7 @@ params: Object, keyEventTarget: { type: Object, - value: function() { return document.body; }, + value() { return document.body; }, }, _account: { @@ -48,6 +48,8 @@ _showChangeView: Boolean, _showDiffView: Boolean, _showSettingsView: Boolean, + _showAdminView: Boolean, + _showCLAView: Boolean, _viewState: Object, _lastError: Object, _lastSearchPage: String, @@ -62,7 +64,6 @@ observers: [ '_viewChanged(params.view)', - '_loadPlugins(_serverConfig.plugin.js_resource_paths)', ], behaviors: [ @@ -74,18 +75,27 @@ '?': '_showKeyboardShortcuts', }, - ready: function() { + created() { + // If shadow dom cookie is set, reload the page using shadow dom. + if (util.getCookie('USE_SHADOW_DOM') === 'true') { + if (!window.location.href.includes('?dom=shadow')) { + window.location.href = window.location.href + '?dom=shadow'; + } + } + }, + + ready() { this.$.router.start(); - this.$.restAPI.getAccount().then(function(account) { + this.$.restAPI.getAccount().then(account => { this._account = account; - }.bind(this)); - this.$.restAPI.getConfig().then(function(config) { + }); + this.$.restAPI.getConfig().then(config => { this._serverConfig = config; - }.bind(this)); - this.$.restAPI.getVersion().then(function(version) { + }); + this.$.restAPI.getVersion().then(version => { this._version = version; - }.bind(this)); + }); this.$.reporting.appStarted(); this._viewState = { @@ -95,6 +105,8 @@ selectedFileIndex: 0, showReplyDialog: false, diffMode: null, + numFilesShown: null, + scrollTop: 0, }, changeListView: { query: null, @@ -107,18 +119,18 @@ }; }, - _accountChanged: function(account) { + _accountChanged(account) { if (!account) { return; } // Preferences are cached when a user is logged in; warm them. this.$.restAPI.getPreferences(); this.$.restAPI.getDiffPreferences(); this.$.errorManager.knownAccountId = - this._account && this._account._account_id || null; + this._account && this._account._account_id || null; }, - _viewChanged: function(view) { - this.$.errorView.hidden = true; + _viewChanged(view) { + this.$.errorView.classList.remove('show'); this.set('_showChangeListView', view === 'gr-change-list-view'); this.set('_showDashboardView', view === 'gr-dashboard-view'); this.set('_showChangeView', view === 'gr-change-view'); @@ -129,82 +141,72 @@ if (this.params.justRegistered) { this.$.registration.open(); } + this.$.header.unfloat(); }, - _loadPlugins: function(plugins) { - Gerrit._setPluginsCount(plugins.length); - for (var i = 0; i < plugins.length; i++) { - var scriptEl = document.createElement('script'); - scriptEl.defer = true; - scriptEl.src = '/' + plugins[i]; - scriptEl.onerror = Gerrit._pluginInstalled; - document.body.appendChild(scriptEl); - } - }, - - _loginTapHandler: function(e) { + _loginTapHandler(e) { e.preventDefault(); page.show('/login/' + encodeURIComponent( window.location.pathname + window.location.hash)); }, // Argument used for binding update only. - _computeLoggedIn: function(account) { + _computeLoggedIn(account) { return !!(account && Object.keys(account).length > 0); }, - _computeShowGwtUiLink: function(config) { - return config.gerrit.web_uis && - config.gerrit.web_uis.indexOf('GWT') !== -1; + _computeShowGwtUiLink(config) { + return config.gerrit.web_uis && config.gerrit.web_uis.includes('GWT'); }, - _handlePageError: function(e) { - [ + _handlePageError(e) { + const props = [ '_showChangeListView', '_showDashboardView', '_showChangeView', '_showDiffView', '_showSettingsView', - ].forEach(function(showProp) { + ]; + for (const showProp of props) { this.set(showProp, false); - }.bind(this)); + } - this.$.errorView.hidden = false; - var response = e.detail.response; - var err = {text: [response.status, response.statusText].join(' ')}; + this.$.errorView.classList.add('show'); + const response = e.detail.response; + const err = {text: [response.status, response.statusText].join(' ')}; if (response.status === 404) { err.emoji = '¯\\_(ツ)_/¯'; this._lastError = err; } else { err.emoji = 'o_O'; - response.text().then(function(text) { + response.text().then(text => { err.moreInfo = text; this._lastError = err; - }.bind(this)); + }); } }, - _handleLocationChange: function(e) { - var hash = e.detail.hash.substring(1); - var pathname = e.detail.pathname; - if (pathname.indexOf('/c/') === 0 && parseInt(hash, 10) > 0) { + _handleLocationChange(e) { + const hash = e.detail.hash.substring(1); + let pathname = e.detail.pathname; + if (pathname.startsWith('/c/') && parseInt(hash, 10) > 0) { pathname += '@' + hash; } this.set('_path', pathname); this._handleSearchPageChange(); }, - _handleSearchPageChange: function() { + _handleSearchPageChange() { if (!this.params) { return; } - var viewsToCheck = ['gr-change-list-view', 'gr-dashboard-view']; - if (viewsToCheck.indexOf(this.params.view) !== -1) { + const viewsToCheck = ['gr-change-list-view', 'gr-dashboard-view']; + if (viewsToCheck.includes(this.params.view)) { this.set('_lastSearchPage', location.pathname); } }, - _handleTitleChange: function(e) { + _handleTitleChange(e) { if (e.detail.title) { document.title = e.detail.title + ' · Gerrit Code Review'; } else { @@ -212,23 +214,23 @@ } }, - _showKeyboardShortcuts: function(e) { + _showKeyboardShortcuts(e) { if (this.shouldSuppressKeyboardShortcut(e)) { return; } this.$.keyboardShortcuts.open(); }, - _handleKeyboardShortcutDialogClose: function() { + _handleKeyboardShortcutDialogClose() { this.$.keyboardShortcuts.close(); }, - _handleAccountDetailUpdate: function(e) { + _handleAccountDetailUpdate(e) { this.$.mainHeader.reload(); if (this.params.view === 'gr-settings-view') { this.$$('gr-settings-view').reloadAccountDetail(); } }, - _handleRegistrationDialogClose: function(e) { + _handleRegistrationDialogClose(e) { this.params.justRegistered = false; this.$.registration.close(); },
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html index 28251fe..905c5c4 100644 --- a/polygerrit-ui/app/elements/gr-app_test.html +++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -18,9 +18,9 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-app</title> -<script src="../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../test/common-test-setup.html"/> <link rel="import" href="gr-app.html"> <script>void(0);</script> @@ -32,11 +32,11 @@ </test-fixture> <script> - suite('gr-app tests', function() { - var sandbox; - var element; + suite('gr-app tests', () => { + let sandbox; + let element; - setup(function(done) { + setup(done => { sandbox = sinon.sandbox.create(); stub('gr-reporting', { appStarted: sandbox.stub(), @@ -45,43 +45,43 @@ _getTopContent: sinon.stub(), }); stub('gr-rest-api-interface', { - getAccount: function() { return Promise.resolve({}); }, - getAccountCapabilities: function() { return Promise.resolve({}); }, - getConfig: function() { + getAccount() { return Promise.resolve({}); }, + getAccountCapabilities() { return Promise.resolve({}); }, + getConfig() { return Promise.resolve({ gerrit: {web_uis: ['GWT', 'POLYGERRIT']}, plugin: {js_resource_paths: []}, }); }, - getVersion: function() { return Promise.resolve(42); }, + getPreferences() { return Promise.resolve({my: []}); }, + getVersion() { return Promise.resolve(42); }, + probePath() { return Promise.resolve(42); }, }); element = fixture('basic'); flush(done); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('reporting', function() { + test('reporting', () => { assert.isTrue(element.$.reporting.appStarted.calledOnce); }); - test('location change updates gwt footer', function(done) { + test('location change updates gwt footer', done => { element._path = '/test/path'; - flush(function() { - var gwtLink = element.$$('#gwtLink'); - assert.equal( - gwtLink.href, - 'http://' + location.host + element.getBaseUrl() + '/?polygerrit=0#/test/path' - ); + flush(() => { + const gwtLink = element.$$('#gwtLink'); + assert.equal(gwtLink.href, 'http://' + location.host + + element.getBaseUrl() + '/?polygerrit=0#/test/path'); done(); }); }); - test('_handleLocationChange handles hashes', function(done) { - var curLocation = { + test('_handleLocationChange handles hashes', done => { + const curLocation = { pathname: '/c/1/1/testfile.txt', hash: '#2', host: location.host, @@ -89,21 +89,23 @@ sandbox.stub(element, '_handleSearchPageChange'); element._handleLocationChange({detail: curLocation}); - flush(function() { - var gwtLink = element.$$('#gwtLink'); + flush(() => { + const gwtLink = element.$$('#gwtLink'); assert.equal( - gwtLink.href, - 'http://' + location.host + element.getBaseUrl() + + gwtLink.href, + 'http://' + location.host + element.getBaseUrl() + '/?polygerrit=0#/c/1/1/testfile.txt@2' ); done(); }); }); - test('sets plugins count', function() { - sandbox.stub(Gerrit, '_setPluginsCount'); - element._loadPlugins([]); - assert.isTrue(Gerrit._setPluginsCount.calledWithExactly(0)); + test('passes config to gr-plugin-host', done => { + element.$.restAPI.getConfig.lastCall.returnValue.then(config => { + const pluginConfig = config.plugin; + assert.deepEqual(element.$.plugins.config, pluginConfig); + done(); + }); }); }); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html new file mode 100644 index 0000000..49424a1 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.html
@@ -0,0 +1,25 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> + +<dom-module id="gr-endpoint-decorator"> + <template> + <content></content> + </template> + <script src="gr-endpoint-decorator.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js new file mode 100644 index 0000000..cb108ff --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -0,0 +1,49 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-endpoint-decorator', + + properties: { + name: String, + }, + + _import(url) { + return new Promise((resolve, reject) => { + this.importHref(url, resolve, reject); + }); + }, + + _initPluginDomHook(name, plugin) { + const el = document.createElement(name); + el.plugin = plugin; + el.content = this.getContentChildren()[0]; + return Polymer.dom(this.root).appendChild(el); + }, + + ready() { + Gerrit.awaitPluginsLoaded().then(() => Promise.all( + Gerrit._getPluginsForEndpoint(this.name).map( + pluginUrl => this._import(pluginUrl))) + ).then(() => { + const modulesData = Gerrit._getEndpointDetails(this.name); + for (const {moduleName, plugin} of modulesData) { + this._initPluginDomHook(moduleName, plugin); + } + }); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html new file mode 100644 index 0000000..52ece65 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -0,0 +1,70 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-endpoint-decorator</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-endpoint-decorator.html"> + +<test-fixture id="basic"> + <template> + <gr-endpoint-decorator name="foo"></gr-endpoint-decorator> + </template> +</test-fixture> + +<script> + suite('gr-endpoint-decorator', () => { + let sandbox; + let element; + let plugin; + + setup(done => { + sandbox = sinon.sandbox.create(); + + // NB: Order is important. + Gerrit.install(p => { + plugin = p; + plugin.registerCustomComponent('foo', 'some-module'); + }, '0.1', 'http://some/plugin/url.html'); + + sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve()); + + element = fixture('basic'); + sandbox.stub(element, '_initPluginDomHook'); + sandbox.stub(element, 'importHref', (url, resolve) => { resolve(); }); + + flush(done); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('imports plugin-provided module', () => { + assert.isTrue( + element.importHref.calledWith(new URL('http://some/plugin/url.html'))); + }); + + test('inits plugin-provided dom hook', () => { + assert.isTrue( + element._initPluginDomHook.calledWith('some-module', plugin)); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html new file mode 100644 index 0000000..623d304 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.html
@@ -0,0 +1,25 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> + +<dom-module id="gr-external-style"> + <template> + <content></content> + </template> + <script src="gr-external-style.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js new file mode 100644 index 0000000..369c025 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -0,0 +1,48 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-external-style', + + properties: { + name: String, + }, + + _import(url) { + return new Promise((resolve, reject) => { + this.importHref(url, resolve, reject); + }); + }, + + _applyStyle(name) { + const s = document.createElement('style', 'custom-style'); + s.setAttribute('include', name); + Polymer.dom(this.root).appendChild(s); + }, + + ready() { + Gerrit.awaitPluginsLoaded().then(() => Promise.all( + Gerrit._getPluginsForEndpoint(this.name).map( + pluginUrl => this._import(pluginUrl))) + ).then(() => { + const moduleNames = Gerrit._getModulesForEndoint(this.name); + for (const name of moduleNames) { + this._applyStyle(name); + } + }); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html new file mode 100644 index 0000000..bc24c2b --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
@@ -0,0 +1,69 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-external-style</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-external-style.html"> + +<test-fixture id="basic"> + <template> + <gr-external-style name="foo"></gr-external-style> + </template> +</test-fixture> + +<script> + suite('gr-external-style integration tests', () => { + let sandbox; + let element; + + setup(done => { + sandbox = sinon.sandbox.create(); + + // NB: Order is important. + let plugin; + Gerrit.install(p => { + plugin = p; + plugin.registerStyleModule('foo', 'some-module'); + }, '0.1', 'http://some/plugin/url.html'); + + sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve()); + + element = fixture('basic'); + sandbox.stub(element, '_applyStyle'); + sandbox.stub(element, 'importHref', (url, resolve) => { resolve(); }); + + flush(done); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('imports plugin-provided module', () => { + assert.isTrue(element.importHref.calledWith( + new URL('http://some/plugin/url.html'))); + }); + + test('applies plugin-provided styles', () => { + assert.isTrue(element._applyStyle.calledWith('some-module')); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html new file mode 100644 index 0000000..ad4e2b0 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html
@@ -0,0 +1,23 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> +<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> + +<dom-module id="gr-plugin-host"> + <script src="gr-plugin-host.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js new file mode 100644 index 0000000..efad106 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -0,0 +1,68 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-plugin-host', + + properties: { + config: { + type: Object, + observer: '_configChanged', + }, + }, + + behaviors: [ + Gerrit.BaseUrlBehavior, + ], + + _configChanged(config) { + const jsPlugins = config.js_resource_paths || []; + const htmlPlugins = config.html_resource_paths || []; + Gerrit._setPluginsCount(jsPlugins.length + htmlPlugins.length); + this._loadJsPlugins(jsPlugins); + this._importHtmlPlugins(htmlPlugins); + }, + + _importHtmlPlugins(plugins) { + for (const url of plugins) { + this.importHref( + this._urlFor(url), Gerrit._pluginInstalled, Gerrit._pluginInstalled, + true); + } + }, + + _loadJsPlugins(plugins) { + for (const url of plugins) { + this._createScriptTag(this._urlFor(url)); + } + }, + + _createScriptTag(url) { + const el = document.createElement('script'); + el.defer = true; + el.src = url; + el.onerror = Gerrit._pluginInstalled; + return document.body.appendChild(el); + }, + + _urlFor(pathOrUrl) { + if (pathOrUrl.startsWith('http')) { + return pathOrUrl; + } + return this.getBaseUrl() + '/' + pathOrUrl; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html new file mode 100644 index 0000000..ead0781 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -0,0 +1,135 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-plugin-host</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-plugin-host.html"> + +<test-fixture id="basic"> + <template> + <gr-plugin-host></gr-plugin-host> + </template> +</test-fixture> + +<script> + suite('gr-diff tests', () => { + let element; + let sandbox; + + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + sandbox.stub(document.body, 'appendChild'); + sandbox.stub(element, 'importHref'); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('counts plugins', () => { + sandbox.stub(Gerrit, '_setPluginsCount'); + element.config = { + html_resource_paths: ['foo/bar', 'baz'], + js_resource_paths: ['42'], + }; + assert.isTrue(Gerrit._setPluginsCount.calledWith(3)); + }); + + test('imports relative html plugins from config', () => { + element.config = { + html_resource_paths: ['foo/bar', 'baz'], + }; + assert.isTrue(element.importHref.calledWith( + '/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true)); + assert.isTrue(element.importHref.calledWith( + '/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled, true)); + }); + + test('imports relative html plugins from config with a base url', () => { + sandbox.stub(element, 'getBaseUrl').returns('/the-base'); + element.config = { + html_resource_paths: ['foo/bar', 'baz'], + }; + assert.isTrue(element.importHref.calledWith( + '/the-base/foo/bar', Gerrit._pluginInstalled, Gerrit._pluginInstalled, + true)); + assert.isTrue(element.importHref.calledWith( + '/the-base/baz', Gerrit._pluginInstalled, Gerrit._pluginInstalled, + true)); + }); + + test('imports absolute html plugins from config', () => { + element.config = { + html_resource_paths: [ + 'http://example.com/foo/bar', + 'https://example.com/baz', + ], + }; + assert.isTrue(element.importHref.calledWith( + 'http://example.com/foo/bar', Gerrit._pluginInstalled, + Gerrit._pluginInstalled, true)); + assert.isTrue(element.importHref.calledWith( + 'https://example.com/baz', Gerrit._pluginInstalled, + Gerrit._pluginInstalled, true)); + }); + + test('adds js plugins from config to the body', () => { + element.config = { + js_resource_paths: ['foo/bar', 'baz'], + }; + assert.isTrue(document.body.appendChild.calledTwice); + }); + + test('imports relative js plugins from config', () => { + sandbox.stub(element, '_createScriptTag'); + element.config = { + js_resource_paths: ['foo/bar', 'baz'], + }; + assert.isTrue(element._createScriptTag.calledWith('/foo/bar')); + assert.isTrue(element._createScriptTag.calledWith('/baz')); + }); + + test('imports relative html plugins from config with a base url', () => { + sandbox.stub(element, '_createScriptTag'); + sandbox.stub(element, 'getBaseUrl').returns('/the-base'); + element.config = { + js_resource_paths: ['foo/bar', 'baz'], + }; + assert.isTrue(element._createScriptTag.calledWith('/the-base/foo/bar')); + assert.isTrue(element._createScriptTag.calledWith('/the-base/baz')); + }); + + test('imports absolute html plugins from config', () => { + sandbox.stub(element, '_createScriptTag'); + element.config = { + js_resource_paths: [ + 'http://example.com/foo/bar', + 'https://example.com/baz', + ], + }; + assert.isTrue(element._createScriptTag.calledWith( + 'http://example.com/foo/bar')); + assert.isTrue(element._createScriptTag.calledWith( + 'https://example.com/baz')); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html index 0c61998..494b9e8 100644 --- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html +++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.html
@@ -19,12 +19,15 @@ <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/gr-settings-styles.html"> +<link rel="import" href="../../../styles/gr-form-styles.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + <dom-module id="gr-account-info"> <template> - <style include="gr-settings-styles"></style> - <div class="gr-settings-styles"> + <style include="shared-styles"></style> + <style include="gr-form-styles"></style> + <div class="gr-form-styles"> <section> <span class="title">ID</span> <span class="value">[[_account._account_id]]</span>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js index 91bc628..010b136 100644 --- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js +++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -60,25 +60,25 @@ '_statusChanged(_account.status)', ], - loadData: function() { - var promises = []; + loadData() { + const promises = []; this._loading = true; - promises.push(this.$.restAPI.getConfig().then(function(config) { + promises.push(this.$.restAPI.getConfig().then(config => { this._serverConfig = config; - }.bind(this))); + })); - promises.push(this.$.restAPI.getAccount().then(function(account) { + promises.push(this.$.restAPI.getAccount().then(account => { this._account = account; - }.bind(this))); + })); - return Promise.all(promises).then(function() { + return Promise.all(promises).then(() => { this._loading = false; - }.bind(this)); + }); }, - save: function() { + save() { if (!this.hasUnsavedChanges) { return Promise.resolve(); } @@ -88,45 +88,45 @@ // Must be done in sequence to avoid race conditions (@see Issue 5721) return this._maybeSetName() .then(this._maybeSetStatus.bind(this)) - .then(function() { + .then(() => { this._hasNameChange = false; this._hasStatusChange = false; this._saving = false; this.fire('account-detail-update'); - }.bind(this)); + }); }, - _maybeSetName: function() { + _maybeSetName() { return this._hasNameChange && this.mutable ? this.$.restAPI.setAccountName(this._account.name) : Promise.resolve(); }, - _maybeSetStatus: function() { + _maybeSetStatus() { return this._hasStatusChange ? this.$.restAPI.setAccountStatus(this._account.status) : Promise.resolve(); }, - _computeHasUnsavedChanges: function(name, status) { + _computeHasUnsavedChanges(name, status) { return name || status; }, - _computeMutable: function(config) { - return config.auth.editable_account_fields.indexOf('FULL_NAME') !== -1; + _computeMutable(config) { + return config.auth.editable_account_fields.includes('FULL_NAME'); }, - _statusChanged: function() { + _statusChanged() { if (this._loading) { return; } this._hasStatusChange = true; }, - _nameChanged: function() { + _nameChanged() { if (this._loading) { return; } this._hasNameChange = true; }, - _handleKeydown: function(e) { + _handleKeydown(e) { if (e.keyCode === 13) { // Enter e.stopPropagation(); this.save();
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html index cf35450..84fbc09 100644 --- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html +++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-account-info.html"> <script>void(0);</script> @@ -33,16 +32,16 @@ </test-fixture> <script> - suite('gr-account-info tests', function() { - var element; - var account; - var config; - var sandbox; + suite('gr-account-info tests', () => { + let element; + let account; + let config; + let sandbox; function valueOf(title) { - var sections = Polymer.dom(element.root).querySelectorAll('section'); - var titleEl; - for (var i = 0; i < sections.length; i++) { + const sections = Polymer.dom(element.root).querySelectorAll('section'); + let titleEl; + for (let i = 0; i < sections.length; i++) { titleEl = sections[i].querySelector('.title'); if (titleEl.textContent === title) { return sections[i].querySelector('.value'); @@ -50,7 +49,7 @@ } } - setup(function(done) { + setup(done => { sandbox = sinon.sandbox.create(); account = { _account_id: 123, @@ -62,22 +61,22 @@ config = {auth: {editable_account_fields: []}}; stub('gr-rest-api-interface', { - getAccount: function() { return Promise.resolve(account); }, - getConfig: function() { return Promise.resolve(config); }, - getPreferences: function() { + getAccount() { return Promise.resolve(account); }, + getConfig() { return Promise.resolve(config); }, + getPreferences() { return Promise.resolve({time_format: 'HHMM_12'}); }, }); element = fixture('basic'); // Allow the element to render. - element.loadData().then(function() { flush(done); }); + element.loadData().then(() => { flush(done); }); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('basic account info render', function() { + test('basic account info render', () => { assert.isFalse(element._loading); assert.equal(valueOf('ID').textContent, account._account_id); @@ -85,10 +84,10 @@ assert.equal(valueOf('Username').textContent, account.username); }); - test('user name render (immutable)', function() { - var section = element.$.nameSection; - var displaySpan = section.querySelectorAll('.value')[0]; - var inputSpan = section.querySelectorAll('.value')[1]; + test('user name render (immutable)', () => { + const section = element.$.nameSection; + const displaySpan = section.querySelectorAll('.value')[0]; + const inputSpan = section.querySelectorAll('.value')[1]; assert.isFalse(element.mutable); assert.isFalse(displaySpan.hasAttribute('hidden')); @@ -96,13 +95,13 @@ assert.isTrue(inputSpan.hasAttribute('hidden')); }); - test('user name render (mutable)', function() { + test('user name render (mutable)', () => { element.set('_serverConfig', {auth: {editable_account_fields: ['FULL_NAME']}}); - var section = element.$.nameSection; - var displaySpan = section.querySelectorAll('.value')[0]; - var inputSpan = section.querySelectorAll('.value')[1]; + const section = element.$.nameSection; + const displaySpan = section.querySelectorAll('.value')[0]; + const inputSpan = section.querySelectorAll('.value')[1]; assert.isTrue(element.mutable); assert.isTrue(displaySpan.hasAttribute('hidden')); @@ -110,25 +109,25 @@ assert.isFalse(inputSpan.hasAttribute('hidden')); }); - suite('account info edit', function() { - var nameChangedSpy; - var statusChangedSpy; - var nameStub; - var statusStub; + suite('account info edit', () => { + let nameChangedSpy; + let statusChangedSpy; + let nameStub; + let statusStub; - setup(function() { + setup(() => { nameChangedSpy = sandbox.spy(element, '_nameChanged'); statusChangedSpy = sandbox.spy(element, '_statusChanged'); element.set('_serverConfig', {auth: {editable_account_fields: ['FULL_NAME']}}); - nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', - function(name) { return Promise.resolve(); }); + nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', name => + Promise.resolve()); statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus', - function(status) { return Promise.resolve(); }); + status => Promise.resolve()); }); - test('name', function(done) { + test('name', done => { assert.isTrue(element.mutable); assert.isFalse(element.hasUnsavedChanges); @@ -142,13 +141,13 @@ assert.isTrue(nameStub.called); assert.isFalse(statusStub.called); - nameStub.lastCall.returnValue.then(function() { + nameStub.lastCall.returnValue.then(() => { assert.equal(nameStub.lastCall.args[0], 'new name'); done(); }); }); - test('status', function(done) { + test('status', done => { assert.isTrue(element.mutable); assert.isFalse(element.hasUnsavedChanges); @@ -158,10 +157,10 @@ assert.isTrue(statusChangedSpy.called); assert.isTrue(element.hasUnsavedChanges); - element.save().then(function() { + element.save().then(() => { assert.isTrue(statusStub.called); assert.isFalse(nameStub.called); - statusStub.lastCall.returnValue.then(function() { + statusStub.lastCall.returnValue.then(() => { assert.equal(statusStub.lastCall.args[0], 'new status'); done(); }); @@ -169,25 +168,25 @@ }); }); - suite('edit name and status', function() { - var nameChangedSpy; - var statusChangedSpy; - var nameStub; - var statusStub; + suite('edit name and status', () => { + let nameChangedSpy; + let statusChangedSpy; + let nameStub; + let statusStub; - setup(function() { + setup(() => { nameChangedSpy = sandbox.spy(element, '_nameChanged'); statusChangedSpy = sandbox.spy(element, '_statusChanged'); element.set('_serverConfig', {auth: {editable_account_fields: ['FULL_NAME']}}); - nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', - function(name) { return Promise.resolve(); }); + nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', name => + Promise.resolve()); statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus', - function(status) { return Promise.resolve(); }); + status => Promise.resolve()); }); - test('set name and status', function(done) { + test('set name and status', done => { assert.isTrue(element.mutable); assert.isFalse(element.hasUnsavedChanges); @@ -201,7 +200,7 @@ assert.isTrue(element.hasUnsavedChanges); - element.save().then(function() { + element.save().then(() => { assert.isTrue(statusStub.called); assert.isTrue(nameStub.called); @@ -214,23 +213,23 @@ }); }); - suite('set status but read name', function() { - var statusChangedSpy; - var statusStub; + suite('set status but read name', () => { + let statusChangedSpy; + let statusStub; - setup(function() { + setup(() => { statusChangedSpy = sandbox.spy(element, '_statusChanged'); element.set('_serverConfig', {auth: {editable_account_fields: []}}); statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus', - function(status) { return Promise.resolve(); }); + status => Promise.resolve()); }); - test('read full name but set status', function(done) { - var section = element.$.nameSection; - var displaySpan = section.querySelectorAll('.value')[0]; - var inputSpan = section.querySelectorAll('.value')[1]; + test('read full name but set status', done => { + const section = element.$.nameSection; + const displaySpan = section.querySelectorAll('.value')[0]; + const inputSpan = section.querySelectorAll('.value')[1]; assert.isFalse(element.mutable); @@ -246,9 +245,9 @@ assert.isTrue(element.hasUnsavedChanges); - element.save().then(function() { + element.save().then(() => { assert.isTrue(statusStub.called); - statusStub.lastCall.returnValue.then(function() { + statusStub.lastCall.returnValue.then(() => { assert.equal(statusStub.lastCall.args[0], 'new status'); done(); });
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html new file mode 100644 index 0000000..e4324de --- /dev/null +++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.html
@@ -0,0 +1,60 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../styles/gr-form-styles.html"> +<link rel="import" href="../../../styles/shared-styles.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> + +<dom-module id="gr-agreements-list"> + <template> + <style include="shared-styles"> + #agreements .nameColumn { + min-width: 11em; + width: auto; + } + #agreements .descriptionColumn { + width: auto; + } + </style> + <style include="gr-form-styles"></style> + <div class="gr-form-styles"> + <table id="agreements"> + <thead> + <tr> + <th class="nameColumn">Name</th> + <th class="descriptionColumn">Description</th> + </tr> + </thead> + <tbody> + <template is="dom-repeat" items="[[_agreements]]"> + <tr> + <td class="nameColumn"> + <a href$="[[getUrlBase(item.url)]]">[[item.name]]</a> + </td> + <td class="descriptionColumn">[[item.description]]</td> + </tr> + </template> + </tbody> + </table> + <!-- TODO: Renable this when supported in polygerrit --> + <!-- <a href$="[[getUrl()]]">New Contributor Agreement</a> --> + </div> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-agreements-list.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js new file mode 100644 index 0000000..4e25523 --- /dev/null +++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
@@ -0,0 +1,46 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-agreements-list', + + properties: { + _agreements: Array, + }, + + behaviors: [ + Gerrit.BaseUrlBehavior, + ], + + attached() { + this.loadData(); + }, + + loadData() { + return this.$.restAPI.getAccountAgreements().then(agreements => { + this._agreements = agreements; + }); + }, + + getUrl() { + return this.getBaseUrl() + '/settings/new-agreement'; + }, + + getUrlBase(item) { + return this.getBaseUrl() + '/' + item; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html new file mode 100644 index 0000000..13b8952 --- /dev/null +++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
@@ -0,0 +1,67 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-settings-view</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-agreements-list.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-agreements-list></gr-agreements-list> + </template> +</test-fixture> + +<script> + suite('gr-agreements-list tests', () => { + let element; + let agreements; + + setup(done => { + agreements = [{ + url: 'some url', + description: 'Agreements 1 description', + name: 'Agreements 1', + }]; + + stub('gr-rest-api-interface', { + getAccountGroups() { return Promise.resolve(agreements); }, + }); + + element = fixture('basic'); + + element.loadData().then(() => { flush(done); }); + }); + + test('renders', () => { + const rows = Polymer.dom(element.root).querySelectorAll('tbody tr'); + + assert.equal(rows.length, 3); + + const nameCells = rows.map(row => + row.querySelectorAll('td')[0].textContent + ); + + assert.equal(nameCells[0], 'Agreements 1'); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html index e2488f4..c93ed0c 100644 --- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.html
@@ -19,34 +19,33 @@ <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<link rel="import" href="../../../styles/gr-settings-styles.html"> +<link rel="import" href="../../../styles/shared-styles.html"> +<link rel="import" href="../../../styles/gr-form-styles.html"> <dom-module id="gr-change-table-editor"> <template> - <style> - table { - margin-top: 1em; + <style include="shared-styles"></style> + <style include="gr-form-styles"> + #changeCols { + width: auto; } - th.nameHeader { - width: 11em; + #changeCols .visibleHeader { + text-align: center; } - td.checkboxContainer { - border: 1px solid #fff; + .checkboxContainer { cursor: pointer; text-align: center; } - td.checkboxContainer:hover { - border: 1px solid #ddd; + .checkboxContainer:hover { + outline: 1px solid #ddd; } </style> - <style include="gr-settings-styles"></style> - <div class="gr-settings-styles"> - <table> + <div class="gr-form-styles"> + <table id="changeCols"> <thead> <tr> <th class="nameHeader">Column</th> - <th>Visible</th> + <th class="visibleHeader">Visible</th> </tr> </thead> <tbody>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js index 6a83a46..50a1146 100644 --- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -28,13 +28,13 @@ Gerrit.ChangeTableBehavior, ], - _getButtonText: function(isShown) { + _getButtonText(isShown) { return isShown ? 'Hide' : 'Show'; }, - _updateDisplayedColumns: function(displayedColumns, name, checked) { + _updateDisplayedColumns(displayedColumns, name, checked) { if (!checked) { - return displayedColumns.filter(function(column) { + return displayedColumns.filter(column => { return name.toLowerCase() !== column.toLowerCase(); }); } else { @@ -45,8 +45,8 @@ /** * Handles tap on either the checkbox itself or the surrounding table cell. */ - _handleTargetTap: function(e) { - var checkbox = Polymer.dom(e.target).querySelector('input'); + _handleTargetTap(e) { + let checkbox = Polymer.dom(e.target).querySelector('input'); if (checkbox) { checkbox.click(); } else {
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html index d4443ac..61093b9 100644 --- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-change-table-editor.html"> <script>void(0);</script> @@ -33,12 +32,12 @@ </test-fixture> <script> - suite('gr-change-table-editor tests', function() { - var element; - var columns; - var sandbox; + suite('gr-change-table-editor tests', () => { + let element; + let columns; + let sandbox; - setup(function() { + setup(() => { element = fixture('basic'); sandbox = sinon.sandbox.create(); @@ -55,25 +54,25 @@ flushAsynchronousOperations(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('renders', function() { - var rows = element.$$('tbody').querySelectorAll('tr'); - var tds; + test('renders', () => { + const rows = element.$$('tbody').querySelectorAll('tr'); + let tds; assert.equal(rows.length, element.columnNames.length); - for (var i = 0; i < columns.length; i++) { + for (let i = 0; i < columns.length; i++) { tds = rows[i].querySelectorAll('td'); assert.equal(tds[0].textContent, columns[i]); } }); - test('hide item', function() { - var checkbox = element.$$('table input'); - var isChecked = checkbox.checked; - var displayedLength = element.displayedColumns.length; + test('hide item', () => { + const checkbox = element.$$('table input'); + const isChecked = checkbox.checked; + const displayedLength = element.displayedColumns.length; assert.isTrue(isChecked); MockInteractions.tap(checkbox); @@ -83,7 +82,7 @@ displayedLength - 1); }); - test('show item', function() { + test('show item', () => { element.set('displayedColumns', [ 'Status', 'Owner', @@ -92,9 +91,9 @@ 'Updated', ]); flushAsynchronousOperations(); - var checkbox = element.$$('table input'); - var isChecked = checkbox.checked; - var displayedLength = element.displayedColumns.length; + const checkbox = element.$$('table input'); + const isChecked = checkbox.checked; + const displayedLength = element.displayedColumns.length; assert.isFalse(isChecked); assert.equal(element.$$('table').style.display, ''); @@ -105,11 +104,11 @@ displayedLength + 1); }); - test('_handleTargetTap', function() { - var checkbox = element.$$('table input'); - var originalDisplayedColumns = element.displayedColumns; - var td = element.$$('table .checkboxContainer'); - var displayedColumnStub = + test('_handleTargetTap', () => { + const checkbox = element.$$('table input'); + let originalDisplayedColumns = element.displayedColumns; + const td = element.$$('table .checkboxContainer'); + const displayedColumnStub = sandbox.stub(element, '_updateDisplayedColumns'); MockInteractions.tap(checkbox); @@ -126,9 +125,9 @@ checkbox.checked)); }); - test('_updateDisplayedColumns', function() { - var name = 'Subject'; - var checked = false; + test('_updateDisplayedColumns', () => { + let name = 'Subject'; + let checked = false; assert.deepEqual(element._updateDisplayedColumns(columns, name, checked), [ 'Status',
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html index 5339c5e..99a0392 100644 --- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.html
@@ -18,38 +18,41 @@ <link rel="import" href="../../../bower_components/iron-input/iron-input.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-email-editor"> <template> - <style> + <style include="shared-styles"></style> + <style include="gr-form-styles"> th { color: #666; text-align: left; } - th.emailHeader { - width: 32.5em; + #emailTable .emailColumn { + min-width: 32.5em; + width: auto; } - th.preferredHeader { + #emailTable .preferredHeader { text-align: center; width: 6em; } - tbody tr:nth-child(even) { - background-color: #f4f4f4; - } - td.preferredControl { + #emailTable .preferredControl { cursor: pointer; + height: auto; text-align: center; } - td.preferredControl:hover { - border: 1px solid #ddd; + #emailTable .preferredControl .preferredRadio { + height: auto; + } + .preferredControl:hover { + outline: 1px solid #d1d2d3; } </style> - <style include="gr-settings-styles"></style> - <div class="gr-settings-styles"> - <table> + <div class="gr-form-styles"> + <table id="emailTable"> <thead> <tr> - <th class="emailHeader">Email</th> + <th class="emailColumn">Email</th> <th class="preferredHeader">Preferred</th> <th></th> </tr> @@ -57,10 +60,11 @@ <tbody> <template is="dom-repeat" items="[[_emails]]"> <tr> - <td>[[item.email]]</td> + <td class="emailColumn">[[item.email]]</td> <td class="preferredControl" on-tap="_handlePreferredControlTap"> <input is="iron-input" + class="preferredRadio" type="radio" on-change="_handlePreferredChange" name="preferred"
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js index 90dd119c..8490c1e 100644 --- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
@@ -27,7 +27,7 @@ _emails: Array, _emailsToRemove: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _newPreferred: { type: String, @@ -35,18 +35,17 @@ }, }, - loadData: function() { - return this.$.restAPI.getAccountEmails().then(function(emails) { + loadData() { + return this.$.restAPI.getAccountEmails().then(emails => { this._emails = emails; - }.bind(this)); + }); }, - save: function() { - var promises = []; + save() { + const promises = []; - for (var i = 0; i < this._emailsToRemove.length; i++) { - promises.push(this.$.restAPI.deleteAccountEmail( - this._emailsToRemove[i].email)); + for (const emailObj of this._emailsToRemove) { + promises.push(this.$.restAPI.deleteAccountEmail(emailObj.email)); } if (this._newPreferred) { @@ -54,30 +53,30 @@ this._newPreferred)); } - return Promise.all(promises).then(function() { + return Promise.all(promises).then(() => { this._emailsToRemove = []; this._newPreferred = null; this.hasUnsavedChanges = false; - }.bind(this)); + }); }, - _handleDeleteButton: function(e) { - var index = parseInt(e.target.getAttribute('data-index')); - var email = this._emails[index]; + _handleDeleteButton(e) { + const index = parseInt(e.target.getAttribute('data-index')); + const email = this._emails[index]; this.push('_emailsToRemove', email); this.splice('_emails', index, 1); this.hasUnsavedChanges = true; }, - _handlePreferredControlTap: function(e) { + _handlePreferredControlTap(e) { if (e.target.classList.contains('preferredControl')) { e.target.firstElementChild.click(); } }, - _handlePreferredChange: function(e) { - var preferred = e.target.value; - for (var i = 0; i < this._emails.length; i++) { + _handlePreferredChange(e) { + const preferred = e.target.value; + for (let i = 0; i < this._emails.length; i++) { if (preferred === this._emails[i].email) { this.set(['_emails', i, 'preferred'], true); this._newPreferred = preferred;
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html index b949643..fdbafdb 100644 --- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-email-editor.html"> <script>void(0);</script> @@ -33,18 +32,18 @@ </test-fixture> <script> - suite('gr-email-editor tests', function() { - var element; + suite('gr-email-editor tests', () => { + let element; - setup(function(done) { - var emails = [ + setup(done => { + const emails = [ {email: 'email@one.com'}, {email: 'email@two.com', preferred: true}, {email: 'email@three.com'}, ]; stub('gr-rest-api-interface', { - getAccountEmails: function() { return Promise.resolve(emails); }, + getAccountEmails() { return Promise.resolve(emails); }, }); element = fixture('basic'); @@ -52,8 +51,8 @@ element.loadData().then(done); }); - test('renders', function() { - var rows = element.$$('table').querySelectorAll('tbody tr'); + test('renders', () => { + const rows = element.$$('table').querySelectorAll('tbody tr'); assert.equal(rows.length, 3); @@ -69,9 +68,9 @@ assert.isFalse(element.hasUnsavedChanges); }); - test('edit preferred', function() { - var preferredChangedSpy = sinon.spy(element, '_handlePreferredChange'); - var radios = element.$$('table').querySelectorAll('input[type=radio]'); + test('edit preferred', () => { + const preferredChangedSpy = sinon.spy(element, '_handlePreferredChange'); + const radios = element.$$('table').querySelectorAll('input[type=radio]'); assert.isFalse(element.hasUnsavedChanges); assert.isNotOk(element._newPreferred); @@ -92,8 +91,8 @@ assert.isTrue(preferredChangedSpy.called); }); - test('delete email', function() { - var buttons = element.$$('table').querySelectorAll('gr-button'); + test('delete email', () => { + const buttons = element.$$('table').querySelectorAll('gr-button'); assert.isFalse(element.hasUnsavedChanges); assert.isNotOk(element._newPreferred); @@ -110,11 +109,12 @@ assert.equal(element._emailsToRemove[0].email, 'email@three.com'); }); - test('save changes', function(done) { - var deleteEmailStub = sinon.stub(element.$.restAPI, 'deleteAccountEmail'); - var setPreferredStub = sinon.stub(element.$.restAPI, + test('save changes', done => { + const deleteEmailStub = + sinon.stub(element.$.restAPI, 'deleteAccountEmail'); + const setPreferredStub = sinon.stub(element.$.restAPI, 'setPreferredAccountEmail'); - var rows = element.$$('table').querySelectorAll('tbody tr'); + const rows = element.$$('table').querySelectorAll('tbody tr'); assert.isFalse(element.hasUnsavedChanges); assert.isNotOk(element._newPreferred); @@ -132,7 +132,7 @@ assert.equal(element._emails.length, 2); // Save the changes. - element.save().then(function() { + element.save().then(() => { assert.equal(deleteEmailStub.callCount, 1); assert.equal(deleteEmailStub.getCall(0).args[0], 'email@one.com');
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html index 303d836..bcae0bf 100644 --- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html +++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.html
@@ -16,36 +16,38 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<link rel="import" href="../../../styles/gr-settings-styles.html"> +<link rel="import" href="../../../styles/shared-styles.html"> +<link rel="import" href="../../../styles/gr-form-styles.html"> <dom-module id="gr-group-list"> <template> - <style> - .nameHeader { - width: 15em; - } - .descriptionHeader { - width: 21.5em; - } - .visibleCell { - text-align: center; - } - </style> - <style include="gr-settings-styles"></style> - <div class="gr-settings-styles"> - <table> + <style include="shared-styles"></style> + <style include="gr-form-styles"> + #groups .nameColumn { + min-width: 11em; + width: auto; + } + .descriptionHeader { + min-width: 21.5em; + } + .visibleCell { + text-align: center; + width: 6em; + } + </style> + <div class="gr-form-styles"> + <table id="groups"> <thead> <tr> <th class="nameHeader">Name</th> <th class="descriptionHeader">Description</th> - <th>Visible to all</th> + <th class="visibleCell">Visible to all</th> </tr> </thead> <tbody> <template is="dom-repeat" items="[[_groups]]"> <tr> - <td>[[item.name]]</td> + <td class="nameColumn">[[item.name]]</td> <td>[[item.description]]</td> <td class="visibleCell">[[_computeVisibleToAll(item)]]</td> </tr>
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js index d14c755..d26482d 100644 --- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js +++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
@@ -21,15 +21,15 @@ _groups: Array, }, - loadData: function() { - return this.$.restAPI.getAccountGroups().then(function(groups) { - this._groups = groups.sort(function(a, b) { + loadData() { + return this.$.restAPI.getAccountGroups().then(groups => { + this._groups = groups.sort((a, b) => { return a.name.localeCompare(b.name); }); - }.bind(this)); + }); }, - _computeVisibleToAll: function(group) { + _computeVisibleToAll(group) { return group.options.visible_to_all ? 'Yes' : 'No'; }, });
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html index 2abf797..3c4eff2 100644 --- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html +++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
@@ -20,7 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-group-list.html"> <script>void(0);</script> @@ -32,11 +32,11 @@ </test-fixture> <script> - suite('gr-group-list tests', function() { - var element; - var groups; + suite('gr-group-list tests', () => { + let element; + let groups; - setup(function(done) { + setup(done => { groups = [{ url: 'some url', options: {}, @@ -46,39 +46,40 @@ owner_id: '123', id: 'abc', name: 'Group 1', - },{ + }, { options: {visible_to_all: true}, id: '456', name: 'Group 2', - },{ + }, { options: {}, id: '789', name: 'Group 3', }]; stub('gr-rest-api-interface', { - getAccountGroups: function() { return Promise.resolve(groups); }, + getAccountGroups() { return Promise.resolve(groups); }, }); element = fixture('basic'); - element.loadData().then(function() { flush(done); }); + element.loadData().then(() => { flush(done); }); }); - test('renders', function() { - var rows = Polymer.dom(element.root).querySelectorAll('tbody tr'); + test('renders', () => { + const rows = Polymer.dom(element.root).querySelectorAll('tbody tr'); assert.equal(rows.length, 3); - var nameCells = rows.map( - function(row) { return row.querySelectorAll('td')[0].textContent; }); + const nameCells = rows.map(row => + row.querySelectorAll('td')[0].textContent + ); assert.equal(nameCells[0], 'Group 1'); assert.equal(nameCells[1], 'Group 2'); assert.equal(nameCells[2], 'Group 3'); }); - test('_computeVisibleToAll', function() { + test('_computeVisibleToAll', () => { assert.equal(element._computeVisibleToAll(groups[0]), 'No'); assert.equal(element._computeVisibleToAll(groups[1]), 'Yes'); });
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html index e01ab94..515fe4f 100644 --- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html +++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.html
@@ -15,14 +15,15 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/gr-settings-styles.html"> +<link rel="import" href="../../../styles/gr-form-styles.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-http-password"> <template> - <style> + <style include="shared-styles"> .password { font-family: var(--monospace-font-family); } @@ -46,8 +47,8 @@ right: 2em; } </style> - <style include="gr-settings-styles"></style> - <div class="gr-settings-styles"> + <style include="gr-form-styles"></style> + <div class="gr-form-styles"> <div hidden$="[[_passwordUrl]]"> <section> <span class="title">Username</span> @@ -58,7 +59,7 @@ on-tap="_handleGenerateTap">Generate new password</gr-button> </div> <span hidden$="[[!_passwordUrl]]"> - <a href="[[_passwordUrl]]" target="_blank" rel="noopener"> + <a href$="[[_passwordUrl]]" target="_blank" rel="noopener"> Obtain password</a> (opens in a new tab) </span> @@ -67,7 +68,7 @@ id="generatedPasswordOverlay" on-iron-overlay-closed="_generatedPasswordOverlayClosed" with-backdrop> - <div class="gr-settings-styles"> + <div class="gr-form-styles"> <section id="generatedPasswordDisplay"> <span class="title">New Password:</span> <span class="value">[[_generatedPassword]]</span>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js index f4894e9..d9c19e2 100644 --- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js +++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
@@ -23,33 +23,33 @@ _passwordUrl: String, }, - loadData: function() { - var promises = []; + loadData() { + const promises = []; - promises.push(this.$.restAPI.getAccount().then(function(account) { + promises.push(this.$.restAPI.getAccount().then(account => { this._username = account.username; - }.bind(this))); + })); - promises.push(this.$.restAPI.getConfig().then(function(info) { + promises.push(this.$.restAPI.getConfig().then(info => { this._passwordUrl = info.auth.http_password_url || null; - }.bind(this))); + })); return Promise.all(promises); }, - _handleGenerateTap: function() { + _handleGenerateTap() { this._generatedPassword = 'Generating...'; this.$.generatedPasswordOverlay.open(); - this.$.restAPI.generateAccountHttpPassword().then(function(newPassword) { + this.$.restAPI.generateAccountHttpPassword().then(newPassword => { this._generatedPassword = newPassword; - }.bind(this)); + }); }, - _closeOverlay: function() { + _closeOverlay() { this.$.generatedPasswordOverlay.close(); }, - _generatedPasswordOverlayClosed: function() { + _generatedPasswordOverlayClosed() { this._generatedPassword = null; }, });
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html index 787c2c4..bbe1555 100644 --- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html +++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-http-password.html"> <script>void(0);</script> @@ -33,33 +32,31 @@ </test-fixture> <script> - suite('gr-http-password tests', function() { - var element; - var account; - var password; - var config; + suite('gr-http-password tests', () => { + let element; + let account; + let config; - setup(function(done) { + setup(done => { account = {username: 'user name'}; config = {auth: {}}; - password = 'the password'; stub('gr-rest-api-interface', { - getAccount: function() { return Promise.resolve(account); }, - getConfig: function() { return Promise.resolve(config); }, + getAccount() { return Promise.resolve(account); }, + getConfig() { return Promise.resolve(config); }, }); element = fixture('basic'); - element.loadData().then(function() { flush(done); }); + element.loadData().then(() => { flush(done); }); }); - test('generate password', function() { - var button = element.$.generateButton; - var nextPassword = 'the new password'; - var generateResolve; - var generateStub = sinon.stub(element.$.restAPI, - 'generateAccountHttpPassword', function() { - return new Promise(function(resolve) { + test('generate password', () => { + const button = element.$.generateButton; + const nextPassword = 'the new password'; + let generateResolve; + const generateStub = sinon.stub(element.$.restAPI, + 'generateAccountHttpPassword', () => { + return new Promise(resolve => { generateResolve = resolve; }); }); @@ -73,18 +70,18 @@ generateResolve(nextPassword); - generateStub.lastCall.returnValue.then(function() { + generateStub.lastCall.returnValue.then(() => { assert.equal(element._generatedPassword, nextPassword); }); }); - test('without http_password_url', function() { + test('without http_password_url', () => { assert.isNull(element._passwordUrl); }); - test('with http_password_url', function(done) { + test('with http_password_url', done => { config.auth.http_password_url = 'http://example.com/'; - element.loadData().then(function() { + element.loadData().then(() => { assert.isNotNull(element._passwordUrl); assert.equal(element._passwordUrl, config.auth.http_password_url); done();
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html index e603e8c..fa3428a 100644 --- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.html
@@ -19,31 +19,32 @@ <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<link rel="import" href="../../../styles/gr-settings-styles.html"> +<link rel="import" href="../../../styles/shared-styles.html"> +<link rel="import" href="../../../styles/gr-form-styles.html"> <dom-module id="gr-menu-editor"> <template> - <style> - th.nameHeader { - width: 11em; + <style include="shared-styles"> + .buttonColumn { + width: 2em; } - tbody tr:first-of-type td .move-up-button, - tbody tr:last-of-type td .move-down-button { + .moveUpButton, + .moveDownButton { + width: 100% + } + tbody tr:first-of-type td .moveUpButton, + tbody tr:last-of-type td .moveDownButton { display: none; } td.urlCell { word-break: break-word; } - .newTitleInput { - width: 10em; - } .newUrlInput { - width: 23em; + min-width: 23em; } </style> - <style include="gr-settings-styles"></style> - <div class="gr-settings-styles"> + <style include="gr-form-styles"></style> + <div class="gr-form-styles"> <table> <thead> <tr> @@ -56,17 +57,17 @@ <tr> <td>[[item.name]]</td> <td class="urlCell">[[item.url]]</td> - <td> + <td class="buttonColumn"> <gr-button data-index="[[index]]" on-tap="_handleMoveUpButton" - class="move-up-button">↑</gr-button> + class="moveUpButton">↑</gr-button> </td> - <td> + <td class="buttonColumn"> <gr-button data-index="[[index]]" on-tap="_handleMoveDownButton" - class="move-down-button">↓</gr-button> + class="moveDownButton">↓</gr-button> </td> <td> <gr-button @@ -81,7 +82,6 @@ <tr> <th> <input - class="newTitleInput" is="iron-input" placeholder="New Title" on-keydown="_handleInputKeydown"
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js index d3a2e2d..543c86d 100644 --- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
@@ -23,28 +23,28 @@ _newUrl: String, }, - _handleMoveUpButton: function(e) { - var index = e.target.dataIndex; + _handleMoveUpButton(e) { + const index = e.target.dataIndex; if (index === 0) { return; } - var row = this.menuItems[index]; - var prev = this.menuItems[index - 1]; + const row = this.menuItems[index]; + const prev = this.menuItems[index - 1]; this.splice('menuItems', index - 1, 2, row, prev); }, - _handleMoveDownButton: function(e) { - var index = e.target.dataIndex; + _handleMoveDownButton(e) { + const index = e.target.dataIndex; if (index === this.menuItems.length - 1) { return; } - var row = this.menuItems[index]; - var next = this.menuItems[index + 1]; + const row = this.menuItems[index]; + const next = this.menuItems[index + 1]; this.splice('menuItems', index, 2, next, row); }, - _handleDeleteButton: function(e) { - var index = e.target.dataIndex; + _handleDeleteButton(e) { + const index = e.target.dataIndex; this.splice('menuItems', index, 1); }, - _handleAddButton: function() { + _handleAddButton() { if (this._computeAddDisabled(this._newName, this._newUrl)) { return; } this.splice('menuItems', this.menuItems.length, 0, { @@ -57,11 +57,11 @@ this._newUrl = ''; }, - _computeAddDisabled: function(newName, newUrl) { + _computeAddDisabled(newName, newUrl) { return !newName.length || !newUrl.length; }, - _handleInputKeydown: function(e) { + _handleInputKeydown(e) { if (e.keyCode === 13) { e.stopPropagation(); this._handleAddButton();
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html index a7078093..f16ba6c 100644 --- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-menu-editor.html"> <script>void(0);</script> @@ -33,14 +32,14 @@ </test-fixture> <script> - suite('gr-settings-view tests', function() { - var element; - var menu; + suite('gr-settings-view tests', () => { + let element; + let menu; function assertMenuNamesEqual(element, expected) { - var names = element.menuItems.map(function(i) { return i.name; }); + const names = element.menuItems.map(i => { return i.name; }); assert.equal(names.length, expected.length); - for (var i = 0; i < names.length; i++) { + for (let i = 0; i < names.length; i++) { assert.equal(names[i], expected[i]); } } @@ -48,13 +47,13 @@ // Click the up/down button (according to direction) for the index'th row. // The index of the first row is 0, corresponding to the array. function move(element, index, direction) { - var selector = - 'tr:nth-child(' + (index + 1) + ') .move-' + direction + '-button'; - var button = element.$$('tbody').querySelector(selector); + const selector = + 'tr:nth-child(' + (index + 1) + ') .move' + direction + 'Button'; + const button = element.$$('tbody').querySelector(selector); MockInteractions.tap(button); } - setup(function() { + setup(() => { element = fixture('basic'); menu = [ {url: '/first/url', name: 'first name', target: '_blank'}, @@ -65,12 +64,12 @@ Polymer.dom.flush(); }); - test('renders', function() { - var rows = element.$$('tbody').querySelectorAll('tr'); - var tds; + test('renders', () => { + const rows = element.$$('tbody').querySelectorAll('tr'); + let tds; assert.equal(rows.length, menu.length); - for (var i = 0; i < menu.length; i++) { + for (let i = 0; i < menu.length; i++) { tds = rows[i].querySelectorAll('td'); assert.equal(tds[0].textContent, menu[i].name); assert.equal(tds[1].textContent, menu[i].url); @@ -80,23 +79,23 @@ element._newUrl)); }); - test('_computeAddDisabled', function() { + test('_computeAddDisabled', () => { assert.isTrue(element._computeAddDisabled('', '')); assert.isTrue(element._computeAddDisabled('name', '')); assert.isTrue(element._computeAddDisabled('', 'url')); assert.isFalse(element._computeAddDisabled('name', 'url')); }); - test('add a new menu item', function() { - var newName = 'new name'; - var newUrl = 'new url'; + test('add a new menu item', () => { + const newName = 'new name'; + const newUrl = 'new url'; element._newName = newName; element._newUrl = newUrl; assert.isFalse(element._computeAddDisabled(element._newName, element._newUrl)); - var originalMenuLength = element.menuItems.length; + const originalMenuLength = element.menuItems.length; element._handleAddButton(); @@ -106,38 +105,38 @@ assert.equal(element.menuItems[element.menuItems.length - 1].url, newUrl); }); - test('move items down', function() { + test('move items down', () => { assertMenuNamesEqual(element, ['first name', 'second name', 'third name']); // Move the middle item down - move(element, 1, 'down'); + move(element, 1, 'Down'); assertMenuNamesEqual(element, ['first name', 'third name', 'second name']); // Moving the bottom item down is a no-op. - move(element, 2, 'down'); + move(element, 2, 'Down'); assertMenuNamesEqual(element, ['first name', 'third name', 'second name']); }); - test('move items up', function() { + test('move items up', () => { assertMenuNamesEqual(element, ['first name', 'second name', 'third name']); // Move the last item up twice to be the first. - move(element, 2, 'up'); - move(element, 1, 'up'); + move(element, 2, 'Up'); + move(element, 1, 'Up'); assertMenuNamesEqual(element, ['third name', 'first name', 'second name']); // Moving the top item up is a no-op. - move(element, 0, 'up'); + move(element, 0, 'Up'); assertMenuNamesEqual(element, ['third name', 'first name', 'second name']); }); - test('remove item', function() { + test('remove item', () => { assertMenuNamesEqual(element, ['first name', 'second name', 'third name']); @@ -148,7 +147,7 @@ assertMenuNamesEqual(element, ['first name', 'third name']); // Delete remaining items. - for (var i = 0; i < 2; i++) { + for (let i = 0; i < 2; i++) { MockInteractions.tap( element.$$('tbody').querySelector('tr:first-child .remove-button')); }
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html index ee358d5..eeac221 100644 --- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html +++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.html
@@ -15,14 +15,15 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/gr-settings-styles.html"> +<link rel="import" href="../../../styles/gr-form-styles.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-registration-dialog"> <template> - <style include="gr-settings-styles"></style> - <style> + <style include="gr-form-styles"></style> + <style include="shared-styles"> :host { display: block; } @@ -47,7 +48,7 @@ justify-content: space-between; } </style> - <main class="gr-settings-styles"> + <main class="gr-form-styles"> <header>Please confirm your contact information</header> <main> <p>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js index 9acdba9..319143a 100644 --- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js +++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -38,39 +38,39 @@ role: 'dialog', }, - attached: function() { - this.$.restAPI.getAccount().then(function(account) { + attached() { + this.$.restAPI.getAccount().then(account => { this._account = account; - }.bind(this)); + }); }, - _handleNameKeydown: function(e) { + _handleNameKeydown(e) { if (e.keyCode === 13) { // Enter e.stopPropagation(); this._save(); } }, - _save: function() { + _save() { this._saving = true; - var promises = [ + const promises = [ this.$.restAPI.setAccountName(this.$.name.value), this.$.restAPI.setPreferredAccountEmail(this.$.email.value), ]; - return Promise.all(promises).then(function() { + return Promise.all(promises).then(() => { this._saving = false; this.fire('account-detail-update'); - }.bind(this)); + }); }, - _handleSave: function(e) { + _handleSave(e) { e.preventDefault(); - this._save().then(function() { + this._save().then(() => { this.fire('close'); - }.bind(this)); + }); }, - _handleClose: function(e) { + _handleClose(e) { e.preventDefault(); this._saving = true; // disable buttons indefinitely this.fire('close');
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html index ee5a206..b3ea3fa 100644 --- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html +++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-registration-dialog.html"> <script>void(0);</script> @@ -39,12 +38,12 @@ </test-fixture> <script> - suite('gr-registration-dialog tests', function() { - var element; - var account; - var _listeners; + suite('gr-registration-dialog tests', () => { + let element; + let account; + let _listeners; - setup(function(done) { + setup(done => { _listeners = {}; account = { @@ -57,16 +56,16 @@ }; stub('gr-rest-api-interface', { - getAccount: function() { - // Once the account is resolved, we can let the test proceed. + getAccount() { + // Once the account is resolved, we can let the test proceed. flush(done); return Promise.resolve(account); }, - setAccountName: function(name) { + setAccountName(name) { account.name = name; return Promise.resolve(); }, - setPreferredAccountEmail: function(email) { + setPreferredAccountEmail(email) { account.email = email; return Promise.resolve(); }, @@ -75,8 +74,8 @@ element = fixture('basic'); }); - teardown(function() { - for (var eventType in _listeners) { + teardown(() => { + for (const eventType in _listeners) { if (_listeners.hasOwnProperty(eventType)) { element.removeEventListener(eventType, _listeners[eventType]); } @@ -84,14 +83,14 @@ }); function listen(eventType) { - return new Promise(function(resolve) { + return new Promise(resolve => { _listeners[eventType] = function() { resolve(); }; element.addEventListener(eventType, _listeners[eventType]); }); } function save(opt_action) { - var promise = listen('account-detail-update'); + const promise = listen('account-detail-update'); if (opt_action) { opt_action(); } else { @@ -101,7 +100,7 @@ } function close(opt_action) { - var promise = listen('close'); + const promise = listen('close'); if (opt_action) { opt_action(); } else { @@ -110,18 +109,18 @@ return promise; } - test('fires the close event on close', function(done) { + test('fires the close event on close', done => { close().then(done); }); - test('fires the close event on save', function(done) { - close(function() { + test('fires the close event on save', done => { + close(() => { MockInteractions.tap(element.$.saveButton); }).then(done); }); - test('saves name and preferred email', function(done) { - flush(function() { + test('saves name and preferred email', done => { + flush(() => { element.$.name.value = 'new name'; element.$.email.value = 'email3'; @@ -130,18 +129,18 @@ assert.equal(account.email, 'email'); // Save and verify new values are committed. - save().then(function() { + save().then(() => { assert.equal(account.name, 'new name'); assert.equal(account.email, 'email3'); }).then(done); }); }); - test('pressing enter saves name', function(done) { + test('pressing enter saves name', done => { element.$.name.value = 'entered name'; - save(function() { + save(() => { MockInteractions.pressAndReleaseKeyOn(element.$.name, 13); // 'enter' - }).then(function() { + }).then(() => { assert.equal(account.name, 'entered name'); }).then(done); });
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html index f485dd6..ca2feb1 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -16,85 +16,39 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../styles/shared-styles.html"> +<link rel="import" href="../../../styles/gr-menu-page-styles.html"> +<link rel="import" href="../../../styles/gr-form-styles.html"> +<link rel="import" href="../../settings/gr-change-table-editor/gr-change-table-editor.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> +<link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../shared/gr-select/gr-select.html"> <link rel="import" href="../gr-account-info/gr-account-info.html"> +<link rel="import" href="../gr-agreements-list/gr-agreements-list.html"> <link rel="import" href="../gr-email-editor/gr-email-editor.html"> <link rel="import" href="../gr-group-list/gr-group-list.html"> <link rel="import" href="../gr-http-password/gr-http-password.html"> -<link rel="import" href="../gr-change-table-editor/gr-change-table-editor.html"> <link rel="import" href="../gr-menu-editor/gr-menu-editor.html"> <link rel="import" href="../gr-ssh-editor/gr-ssh-editor.html"> <link rel="import" href="../gr-watched-projects-editor/gr-watched-projects-editor.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> - -<link rel="import" href="../../../styles/gr-settings-styles.html"> <dom-module id="gr-settings-view"> <template> - <style> - :host { - background-color: var(--view-background-color); - display: block; - } - main { - margin: 2em auto; - max-width: 46em; - } - h1 { - margin-bottom: .1em; - } - h2.edited:after { - color: #444; - content: ' *'; - } - .loading { - color: #666; - padding: 1em var(--default-horizontal-margin); - } + <style include="shared-styles"> #newEmailInput { width: 20em; } - nav { - border: 1px solid #eee; - border-top: none; - position: absolute; - top: 0; - width: 14em; - } - nav.pinned { - position: fixed; - } - nav ul { - margin: 1em 2em; - } - nav a { - color: black; - display: inline-block; - margin: .4em 0; - } - @media only screen and (max-width: 67em) { - main { - margin: 2em 0 2em 15em; - } - } - @media only screen and (max-width: 53em) { - .loading { - padding: 0 var(--default-horizontal-margin); - } - main { - margin: 2em 1em; - } - nav { - display: none; - } + #email { + margin-bottom: 1em; } </style> - <style include="gr-settings-styles"></style> + <style include="gr-form-styles"></style> + <style include="gr-menu-page-styles"></style> <div class="loading" hidden$="[[!_loading]]">Loading...</div> <div hidden$="[[_loading]]" hidden> - <nav id="settingsNav"> + <gr-page-nav> <ul> <li><a href="#Profile">Profile</a></li> <li><a href="#Preferences">Preferences</a></li> @@ -108,9 +62,14 @@ SSH Keys </a></li> <li><a href="#Groups">Groups</a></li> + <template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]"> + <li> + <a href="#Agreements">Agreements</a> + </li> + </template> </ul> - </nav> - <main class="gr-settings-styles"> + </gr-page-nav> + <main class="gr-form-styles"> <h1>User Settings</h1> <h2 id="Profile" @@ -205,6 +164,16 @@ on-change="_handleExpandInlineDiffsChanged"> </span> </section> + <section> + <span class="title">Publish comments on push</span> + <span class="value"> + <input + id="publishCommentsOnPush" + type="checkbox" + checked$="[[_localPrefs.publish_comments_on_push]]" + on-change="_handlePublishCommentsOnPushChanged"> + </span> + </section> <gr-button id="savePrefs" on-tap="_handleSavePreferences" @@ -400,6 +369,12 @@ <fieldset> <gr-group-list id="groupList"></gr-group-list> </fieldset> + <template is="dom-if" if="[[_serverConfig.auth.use_contributor_agreements]]"> + <h2 id="Agreements">Agreements</h2> + <fieldset> + <gr-agreements-list id="agreementsList"></gr-agreements-list> + </fieldset> + </template> </main> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js index 4647a2d..1e5d61a 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -14,13 +14,14 @@ (function() { 'use strict'; - var PREFS_SECTION_FIELDS = [ + const PREFS_SECTION_FIELDS = [ 'changes_per_page', 'date_format', 'time_format', 'email_strategy', 'diff_view', 'expand_inline_diffs', + 'publish_comments_on_push', 'email_format', ]; @@ -42,11 +43,11 @@ properties: { prefs: { type: Object, - value: function() { return {}; }, + value() { return {}; }, }, params: { type: Object, - value: function() { return {}; }, + value() { return {}; }, }, _accountNameMutable: Boolean, _accountInfoChanged: Boolean, @@ -54,15 +55,15 @@ _changeTableColumnsNotDisplayed: Array, _localPrefs: { type: Object, - value: function() { return {}; }, + value() { return {}; }, }, _localChangeTableColumns: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _localMenu: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _loading: { type: Boolean, @@ -102,7 +103,6 @@ value: null, }, _serverConfig: Object, - _headerHeight: Number, /** * For testing purposes. @@ -121,231 +121,215 @@ '_handleChangeTableChanged(_localChangeTableColumns)', ], - attached: function() { + attached() { this.fire('title-change', {title: 'Settings'}); - var promises = [ + const promises = [ this.$.accountInfo.loadData(), this.$.watchedProjectsEditor.loadData(), this.$.groupList.loadData(), this.$.httpPass.loadData(), ]; - promises.push(this.$.restAPI.getPreferences().then(function(prefs) { + promises.push(this.$.restAPI.getPreferences().then(prefs => { this.prefs = prefs; this._copyPrefs('_localPrefs', 'prefs'); this._cloneMenu(); this._cloneChangeTableColumns(); - }.bind(this))); + })); - promises.push(this.$.restAPI.getDiffPreferences().then(function(prefs) { + promises.push(this.$.restAPI.getDiffPreferences().then(prefs => { this._diffPrefs = prefs; - }.bind(this))); + })); - promises.push(this.$.restAPI.getConfig().then(function(config) { + promises.push(this.$.restAPI.getConfig().then(config => { this._serverConfig = config; if (this._serverConfig.sshd) { return this.$.sshEditor.loadData(); } - }.bind(this))); + })); if (this.params.emailToken) { promises.push(this.$.restAPI.confirmEmail(this.params.emailToken).then( - function(message) { - if (message) { - this.fire('show-alert', {message: message}); - } - this.$.emailEditor.loadData(); - }.bind(this))); + message => { + if (message) { + this.fire('show-alert', {message}); + } + this.$.emailEditor.loadData(); + })); } else { promises.push(this.$.emailEditor.loadData()); } - this._loadingPromise = Promise.all(promises).then(function() { + this._loadingPromise = Promise.all(promises).then(() => { this._loading = false; - }.bind(this)); - - this.listen(window, 'scroll', '_handleBodyScroll'); + }); }, - detached: function() { - this.unlisten(window, 'scroll', '_handleBodyScroll'); - }, - - reloadAccountDetail: function() { + reloadAccountDetail() { Promise.all([ this.$.accountInfo.loadData(), this.$.emailEditor.loadData(), ]); }, - _handleBodyScroll: function(e) { - if (this._headerHeight === undefined) { - var top = this.$.settingsNav.offsetTop; - for (var offsetParent = this.$.settingsNav.offsetParent; - offsetParent; - offsetParent = offsetParent.offsetParent) { - top += offsetParent.offsetTop; - } - this._headerHeight = top; - } - - this.$.settingsNav.classList.toggle('pinned', - window.scrollY >= this._headerHeight); - }, - - _isLoading: function() { + _isLoading() { return this._loading || this._loading === undefined; }, - _copyPrefs: function(to, from) { - for (var i = 0; i < PREFS_SECTION_FIELDS.length; i++) { + _copyPrefs(to, from) { + for (let i = 0; i < PREFS_SECTION_FIELDS.length; i++) { this.set([to, PREFS_SECTION_FIELDS[i]], this[from][PREFS_SECTION_FIELDS[i]]); } }, - _cloneMenu: function() { - var menu = []; - this.prefs.my.forEach(function(item) { + _cloneMenu() { + const menu = []; + for (const item of this.prefs.my) { menu.push({ name: item.name, url: item.url, target: item.target, }); - }); + } this._localMenu = menu; }, - _cloneChangeTableColumns: function() { - var columns = this.prefs.change_table; + _cloneChangeTableColumns() { + let columns = this.prefs.change_table; if (columns.length === 0) { columns = this.columnNames; this._changeTableColumnsNotDisplayed = []; } else { this._changeTableColumnsNotDisplayed = this.getComplementColumns( - this.prefs.change_table); + this.prefs.change_table); } this._localChangeTableColumns = columns; }, - _formatChangeTableColumns: function(changeTableArray) { - return changeTableArray.map(function(item) { + _formatChangeTableColumns(changeTableArray) { + return changeTableArray.map(item => { return {column: item}; }); }, - _handleChangeTableChanged: function() { + _handleChangeTableChanged() { if (this._isLoading()) { return; } this._changeTableChanged = true; }, - _handlePrefsChanged: function(prefs) { + _handlePrefsChanged(prefs) { if (this._isLoading()) { return; } this._prefsChanged = true; }, - _handleDiffPrefsChanged: function() { + _handleDiffPrefsChanged() { if (this._isLoading()) { return; } this._diffPrefsChanged = true; }, - _handleExpandInlineDiffsChanged: function() { + _handleExpandInlineDiffsChanged() { this.set('_localPrefs.expand_inline_diffs', this.$.expandInlineDiffs.checked); }, - _handleMenuChanged: function() { + _handlePublishCommentsOnPushChanged() { + this.set('_localPrefs.publish_comments_on_push', + this.$.publishCommentsOnPush.checked); + }, + + _handleMenuChanged() { if (this._isLoading()) { return; } this._menuChanged = true; }, - _handleSaveAccountInfo: function() { + _handleSaveAccountInfo() { this.$.accountInfo.save(); }, - _handleSavePreferences: function() { + _handleSavePreferences() { this._copyPrefs('prefs', '_localPrefs'); - return this.$.restAPI.savePreferences(this.prefs).then(function() { + return this.$.restAPI.savePreferences(this.prefs).then(() => { this._prefsChanged = false; - }.bind(this)); + }); }, - _handleLineWrappingChanged: function() { + _handleLineWrappingChanged() { this.set('_diffPrefs.line_wrapping', this.$.lineWrapping.checked); }, - _handleShowTabsChanged: function() { + _handleShowTabsChanged() { this.set('_diffPrefs.show_tabs', this.$.showTabs.checked); }, - _handleShowTrailingWhitespaceChanged: function() { + _handleShowTrailingWhitespaceChanged() { this.set('_diffPrefs.show_whitespace_errors', this.$.showTrailingWhitespace.checked); }, - _handleSyntaxHighlightingChanged: function() { + _handleSyntaxHighlightingChanged() { this.set('_diffPrefs.syntax_highlighting', this.$.syntaxHighlighting.checked); }, - _handleSaveChangeTable: function() { + _handleSaveChangeTable() { this.set('prefs.change_table', this._localChangeTableColumns); this._cloneChangeTableColumns(); - return this.$.restAPI.savePreferences(this.prefs).then(function() { + return this.$.restAPI.savePreferences(this.prefs).then(() => { this._changeTableChanged = false; - }.bind(this)); + }); }, - _handleSaveDiffPreferences: function() { + _handleSaveDiffPreferences() { return this.$.restAPI.saveDiffPreferences(this._diffPrefs) - .then(function() { + .then(() => { this._diffPrefsChanged = false; - }.bind(this)); + }); }, - _handleSaveMenu: function() { + _handleSaveMenu() { this.set('prefs.my', this._localMenu); this._cloneMenu(); - return this.$.restAPI.savePreferences(this.prefs).then(function() { + return this.$.restAPI.savePreferences(this.prefs).then(() => { this._menuChanged = false; - }.bind(this)); + }); }, - _handleSaveWatchedProjects: function() { + _handleSaveWatchedProjects() { this.$.watchedProjectsEditor.save(); }, - _computeHeaderClass: function(changed) { + _computeHeaderClass(changed) { return changed ? 'edited' : ''; }, - _handleSaveEmails: function() { + _handleSaveEmails() { this.$.emailEditor.save(); }, - _handleNewEmailKeydown: function(e) { + _handleNewEmailKeydown(e) { if (e.keyCode === 13) { // Enter e.stopPropagation(); this._handleAddEmailButton(); } }, - _isNewEmailValid: function(newEmail) { - return newEmail.indexOf('@') !== -1; + _isNewEmailValid(newEmail) { + return newEmail.includes('@'); }, - _computeAddEmailButtonEnabled: function(newEmail, addingEmail) { + _computeAddEmailButtonEnabled(newEmail, addingEmail) { return this._isNewEmailValid(newEmail) && !addingEmail; }, - _handleAddEmailButton: function() { + _handleAddEmailButton() { if (!this._isNewEmailValid(this._newEmail)) { return; } this._addingEmail = true; - this.$.restAPI.addAccountEmail(this._newEmail).then(function(response) { + this.$.restAPI.addAccountEmail(this._newEmail).then(response => { this._addingEmail = false; // If it was unsuccessful. @@ -353,7 +337,7 @@ this._lastSentVerificationEmail = this._newEmail; this._newEmail = ''; - }.bind(this)); + }); }, }); })();
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html index cb471d4..517f208 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-settings-view.html"> <script>void(0);</script> @@ -39,18 +38,18 @@ </test-fixture> <script> - suite('gr-settings-view tests', function() { - var element; - var account; - var preferences; - var diffPreferences; - var config; - var sandbox; + suite('gr-settings-view tests', () => { + let element; + let account; + let preferences; + let diffPreferences; + let config; + let sandbox; function valueOf(title, fieldsetid) { - var sections = element.$[fieldsetid].querySelectorAll('section'); - var titleEl; - for (var i = 0; i < sections.length; i++) { + const sections = element.$[fieldsetid].querySelectorAll('section'); + let titleEl; + for (let i = 0; i < sections.length; i++) { titleEl = sections[i].querySelector('.title'); if (titleEl.textContent === title) { return sections[i].querySelector('.value'); @@ -61,7 +60,7 @@ // Because deepEqual isn't behaving in Safari. function assertMenusEqual(actual, expected) { assert.equal(actual.length, expected.length); - for (var i = 0; i < actual.length; i++) { + for (let i = 0; i < actual.length; i++) { assert.equal(actual[i].name, expected[i].name); assert.equal(actual[i].url, expected[i].url); } @@ -69,10 +68,10 @@ function stubAddAccountEmail(statusCode) { return sandbox.stub(element.$.restAPI, 'addAccountEmail', - function() { return Promise.resolve({status: statusCode}); }); + () => { return Promise.resolve({status: statusCode}); }); } - setup(function(done) { + setup(done => { sandbox = sinon.sandbox.create(); account = { _account_id: 123, @@ -109,23 +108,23 @@ syntax_highlighting: true, auto_hide_diff_table_header: true, theme: 'DEFAULT', - ignore_whitespace: 'IGNORE_NONE' + ignore_whitespace: 'IGNORE_NONE', }; config = {auth: {editable_account_fields: []}}; stub('gr-rest-api-interface', { - getLoggedIn: function() { return Promise.resolve(true); }, - getAccount: function() { return Promise.resolve(account); }, - getPreferences: function() { return Promise.resolve(preferences); }, - getDiffPreferences: function() { + getLoggedIn() { return Promise.resolve(true); }, + getAccount() { return Promise.resolve(account); }, + getPreferences() { return Promise.resolve(preferences); }, + getDiffPreferences() { return Promise.resolve(diffPreferences); }, - getWatchedProjects: function() { + getWatchedProjects() { return Promise.resolve([]); }, - getAccountEmails: function() { return Promise.resolve(); }, - getConfig: function() { return Promise.resolve(config); }, - getAccountGroups: function() { return Promise.resolve([]); }, + getAccountEmails() { return Promise.resolve(); }, + getConfig() { return Promise.resolve(config); }, + getAccountGroups() { return Promise.resolve([]); }, }); element = fixture('basic'); @@ -133,19 +132,19 @@ element._loadingPromise.then(done); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('calls the title-change event', function() { - var titleChangedStub = sandbox.stub(); + test('calls the title-change event', () => { + const titleChangedStub = sandbox.stub(); // Create a new view. - var newElement = document.createElement('gr-settings-view'); + const newElement = document.createElement('gr-settings-view'); newElement.addEventListener('title-change', titleChangedStub); // Attach it to the fixture. - var blank = fixture('blank'); + const blank = fixture('blank'); blank.appendChild(newElement); Polymer.dom.flush(); @@ -155,7 +154,7 @@ 'Settings'); }); - test('user preferences', function(done) { + test('user preferences', done => { // Rendered with the expected preferences selected. assert.equal(valueOf('Changes per page', 'preferences') .firstElementChild.bindValue, preferences.changes_per_page); @@ -171,15 +170,17 @@ .firstElementChild.bindValue, preferences.diff_view); assert.equal(valueOf('Expand inline diffs', 'preferences') .firstElementChild.checked, false); + assert.equal(valueOf('Publish comments on push', 'preferences') + .firstElementChild.checked, false); assert.isFalse(element._prefsChanged); assert.isFalse(element._menuChanged); // Change the diff view element. - var diffSelect = valueOf('Diff view', 'preferences').firstElementChild; + const diffSelect = valueOf('Diff view', 'preferences').firstElementChild; diffSelect.bindValue = 'SIDE_BY_SIDE'; - var expandInlineDiffs = + const expandInlineDiffs = valueOf('Expand inline diffs', 'preferences').firstElementChild; diffSelect.fire('change'); @@ -189,23 +190,46 @@ assert.isFalse(element._menuChanged); stub('gr-rest-api-interface', { - savePreferences: function(prefs) { + savePreferences(prefs) { assert.equal(prefs.diff_view, 'SIDE_BY_SIDE'); assertMenusEqual(prefs.my, preferences.my); assert.equal(prefs.expand_inline_diffs, true); return Promise.resolve(); - } + }, }); // Save the change. - element._handleSavePreferences().then(function() { + element._handleSavePreferences().then(() => { assert.isFalse(element._prefsChanged); assert.isFalse(element._menuChanged); done(); }); }); - test('diff preferences', function(done) { + test('publish comments on push', done => { + const publishCommentsOnPush = + valueOf('Publish comments on push', 'preferences').firstElementChild; + MockInteractions.tap(publishCommentsOnPush); + + assert.isFalse(element._menuChanged); + assert.isTrue(element._prefsChanged); + + stub('gr-rest-api-interface', { + savePreferences(prefs) { + assert.equal(prefs.publish_comments_on_push, true); + return Promise.resolve(); + }, + }); + + // Save the change. + element._handleSavePreferences().then(() => { + assert.isFalse(element._prefsChanged); + assert.isFalse(element._menuChanged); + done(); + }); + }); + + test('diff preferences', done => { // Rendered with the expected preferences selected. assert.equal(valueOf('Context', 'diffPreferences') .firstElementChild.bindValue, diffPreferences.context); @@ -224,7 +248,7 @@ assert.isFalse(element._diffPrefsChanged); - var showTabsCheckbox = valueOf('Show tabs', 'diffPreferences') + const showTabsCheckbox = valueOf('Show tabs', 'diffPreferences') .firstElementChild; showTabsCheckbox.checked = false; element._handleShowTabsChanged(); @@ -232,20 +256,20 @@ assert.isTrue(element._diffPrefsChanged); stub('gr-rest-api-interface', { - saveDiffPreferences: function(prefs) { + saveDiffPreferences(prefs) { assert.equal(prefs.show_tabs, false); return Promise.resolve(); - } + }, }); // Save the change. - element._handleSaveDiffPreferences().then(function() { + element._handleSaveDiffPreferences().then(() => { assert.isFalse(element._diffPrefsChanged); done(); }); }); - test('columns input is hidden with fit to scsreen is selected', function() { + test('columns input is hidden with fit to scsreen is selected', () => { assert.isFalse(element.$.columnsPref.hidden); MockInteractions.tap(element.$.lineWrapping); @@ -255,14 +279,14 @@ assert.isFalse(element.$.columnsPref.hidden); }); - test('menu', function(done) { + test('menu', done => { assert.isFalse(element._menuChanged); assert.isFalse(element._prefsChanged); assertMenusEqual(element._localMenu, preferences.my); - var menu = element.$.menu.firstElementChild; - var tableRows = Polymer.dom(menu.root).querySelectorAll('tbody tr'); + const menu = element.$.menu.firstElementChild; + let tableRows = Polymer.dom(menu.root).querySelectorAll('tbody tr'); assert.equal(tableRows.length, preferences.my.length); // Add a menu item: @@ -276,13 +300,13 @@ assert.isFalse(element._prefsChanged); stub('gr-rest-api-interface', { - savePreferences: function(prefs) { + savePreferences(prefs) { assertMenusEqual(prefs.my, element._localMenu); return Promise.resolve(); - } + }, }); - element._handleSaveMenu().then(function() { + element._handleSaveMenu().then(() => { assert.isFalse(element._menuChanged); assert.isFalse(element._prefsChanged); assertMenusEqual(element.prefs.my, element._localMenu); @@ -290,7 +314,7 @@ }); }); - test('add email validation', function() { + test('add email validation', () => { assert.isFalse(element._isNewEmailValid('invalid email')); assert.isTrue(element._isNewEmailValid('vaguely@valid.email')); @@ -302,8 +326,8 @@ element._computeAddEmailButtonEnabled('vaguely@valid.email', false)); }); - test('add email does not save invalid', function() { - var addEmailStub = stubAddAccountEmail(201); + test('add email does not save invalid', () => { + const addEmailStub = stubAddAccountEmail(201); assert.isFalse(element._addingEmail); assert.isNotOk(element._lastSentVerificationEmail); @@ -318,8 +342,8 @@ assert.isFalse(addEmailStub.called); }); - test('add email does save valid', function(done) { - var addEmailStub = stubAddAccountEmail(201); + test('add email does save valid', done => { + const addEmailStub = stubAddAccountEmail(201); assert.isFalse(element._addingEmail); assert.isNotOk(element._lastSentVerificationEmail); @@ -331,14 +355,14 @@ assert.isTrue(addEmailStub.called); assert.isTrue(addEmailStub.called); - addEmailStub.lastCall.returnValue.then(function() { + addEmailStub.lastCall.returnValue.then(() => { assert.isOk(element._lastSentVerificationEmail); done(); }); }); - test('add email does not set last-email if error', function(done) { - var addEmailStub = stubAddAccountEmail(500); + test('add email does not set last-email if error', done => { + const addEmailStub = stubAddAccountEmail(500); assert.isNotOk(element._lastSentVerificationEmail); element._newEmail = 'valid@email.com'; @@ -346,58 +370,57 @@ element._handleAddEmailButton(); assert.isTrue(addEmailStub.called); - addEmailStub.lastCall.returnValue.then(function() { + addEmailStub.lastCall.returnValue.then(() => { assert.isNotOk(element._lastSentVerificationEmail); done(); }); }); - test('emails are loaded without emailToken', function() { + test('emails are loaded without emailToken', () => { sandbox.stub(element.$.emailEditor, 'loadData'); element.params = {}; element.attached(); assert.isTrue(element.$.emailEditor.loadData.calledOnce); }); - suite('when email verification token is provided', function() { - var resolveConfirm; + suite('when email verification token is provided', () => { + let resolveConfirm; - setup(function() { + setup(() => { sandbox.stub(element.$.emailEditor, 'loadData'); - sandbox.stub(element.$.restAPI, 'confirmEmail', function() { - return new Promise(function(resolve) { resolveConfirm = resolve; }); + sandbox.stub(element.$.restAPI, 'confirmEmail', () => { + return new Promise(resolve => { resolveConfirm = resolve; }); }); element.params = {emailToken: 'foo'}; element.attached(); }); - test('it is used to confirm email via rest API', function() { + test('it is used to confirm email via rest API', () => { assert.isTrue(element.$.restAPI.confirmEmail.calledOnce); assert.isTrue(element.$.restAPI.confirmEmail.calledWith('foo')); }); - test('emails are not loaded initially', function() { + test('emails are not loaded initially', () => { assert.isFalse(element.$.emailEditor.loadData.called); }); - test('user emails are loaded after email confirmed', function(done) { - element._loadingPromise.then(function() { + test('user emails are loaded after email confirmed', done => { + element._loadingPromise.then(() => { assert.isTrue(element.$.emailEditor.loadData.calledOnce); done(); }); resolveConfirm(); }); - test('show-alert is fired when email is confirmed', function(done) { + test('show-alert is fired when email is confirmed', done => { sandbox.spy(element, 'fire'); - element._loadingPromise.then(function() { + element._loadingPromise.then(() => { assert.isTrue( element.fire.calledWith('show-alert', {message: 'bar'})); done(); }); resolveConfirm('bar'); }); - }); }); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html index 339c7a7..1afd255 100644 --- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.html
@@ -16,17 +16,16 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="../../../styles/gr-settings-styles.html"> +<link rel="import" href="../../../styles/gr-form-styles.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-ssh-editor"> <template> - <style> - .commentHeader { - width: 27em; - } + <style include="shared-styles"></style> + <style include="gr-form-styles"> .statusHeader { width: 4em; } @@ -48,22 +47,29 @@ position: absolute; right: 2em; } + #existing { + margin-bottom: 1em; + } + #existing .commentColumn { + min-width: 27em; + width: auto; + } </style> - <style include="gr-settings-styles"></style> - <div class="gr-settings-styles"> - <fieldset> + <div class="gr-form-styles"> + <fieldset id="existing"> <table> <thead> <tr> - <th class="commentHeader">Comment</th> + <th class="commentColumn">Comment</th> <th class="statusHeader">Status</th> <th class="keyHeader">Public key</th> + <th></th> </tr> </thead> <tbody> <template is="dom-repeat" items="[[_keys]]" as="key"> <tr> - <td>[[key.comment]]</td> + <td class="commentColumn">[[key.comment]]</td> <td>[[_getStatusLabel(key.valid)]]</td> <td> <gr-button
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js index 2a05033..d9f02d5 100644 --- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
@@ -31,64 +31,63 @@ }, _keysToRemove: { type: Array, - value: function() { return []; }, + value() { return []; }, }, }, - loadData: function() { - return this.$.restAPI.getAccountSSHKeys().then(function(keys) { + loadData() { + return this.$.restAPI.getAccountSSHKeys().then(keys => { this._keys = keys; - }.bind(this)); + }); }, - save: function() { - var promises = this._keysToRemove.map(function(key) { + save() { + const promises = this._keysToRemove.map(key => { this.$.restAPI.deleteAccountSSHKey(key.seq); - }.bind(this)); + }); - return Promise.all(promises).then(function() { + return Promise.all(promises).then(() => { this._keysToRemove = []; this.hasUnsavedChanges = false; - }.bind(this)); + }); }, - _getStatusLabel: function(isValid) { + _getStatusLabel(isValid) { return isValid ? 'Valid' : 'Invalid'; }, - _showKey: function(e) { - var index = parseInt(e.target.getAttribute('data-index'), 10); + _showKey(e) { + const index = parseInt(e.target.getAttribute('data-index'), 10); this._keyToView = this._keys[index]; this.$.viewKeyOverlay.open(); }, - _closeOverlay: function() { + _closeOverlay() { this.$.viewKeyOverlay.close(); }, - _handleDeleteKey: function(e) { - var index = parseInt(e.target.getAttribute('data-index'), 10); + _handleDeleteKey(e) { + const index = parseInt(e.target.getAttribute('data-index'), 10); this.push('_keysToRemove', this._keys[index]); this.splice('_keys', index, 1); this.hasUnsavedChanges = true; }, - _handleAddKey: function() { + _handleAddKey() { this.$.addButton.disabled = true; this.$.newKey.disabled = true; return this.$.restAPI.addAccountSSHKey(this._newKey.trim()) - .then(function(key) { + .then(key => { this.$.newKey.disabled = false; this._newKey = ''; this.push('_keys', key); - }.bind(this)) - .catch(function() { + }).catch(() => { this.$.addButton.disabled = false; this.$.newKey.disabled = false; - }.bind(this)); + }); }, - _computeAddButtonDisabled: function(newKey) { + _computeAddButtonDisabled(newKey) { return !newKey.length; }, });
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html index 7bb5528..4273f7a 100644 --- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-ssh-editor.html"> <script>void(0);</script> @@ -33,11 +32,11 @@ </test-fixture> <script> - suite('gr-ssh-editor tests', function() { - var element; - var keys; + suite('gr-ssh-editor tests', () => { + let element; + let keys; - setup(function(done) { + setup(done => { keys = [{ seq: 1, ssh_public_key: 'ssh-rsa <key 1> comment-one@machine-one', @@ -55,37 +54,37 @@ }]; stub('gr-rest-api-interface', { - getAccountSSHKeys: function() { return Promise.resolve(keys); }, + getAccountSSHKeys() { return Promise.resolve(keys); }, }); element = fixture('basic'); - element.loadData().then(function() { flush(done); }); + element.loadData().then(() => { flush(done); }); }); - test('renders', function() { - var rows = Polymer.dom(element.root).querySelectorAll('tbody tr'); + test('renders', () => { + const rows = Polymer.dom(element.root).querySelectorAll('tbody tr'); assert.equal(rows.length, 2); - var cells = rows[0].querySelectorAll('td'); + let cells = rows[0].querySelectorAll('td'); assert.equal(cells[0].textContent, keys[0].comment); cells = rows[1].querySelectorAll('td'); assert.equal(cells[0].textContent, keys[1].comment); }); - test('remove key', function(done) { - var lastKey = keys[1]; + test('remove key', done => { + const lastKey = keys[1]; - var saveStub = sinon.stub(element.$.restAPI, 'deleteAccountSSHKey', - function() { return Promise.resolve(); }); + const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountSSHKey', + () => { return Promise.resolve(); }); assert.equal(element._keysToRemove.length, 0); assert.isFalse(element.hasUnsavedChanges); // Get the delete button for the last row. - var button = Polymer.dom(element.root).querySelector( + const button = Polymer.dom(element.root).querySelector( 'tbody tr:last-of-type td:nth-child(4) gr-button'); MockInteractions.tap(button); @@ -96,7 +95,7 @@ assert.isTrue(element.hasUnsavedChanges); assert.isFalse(saveStub.called); - element.save().then(function() { + element.save().then(() => { assert.isTrue(saveStub.called); assert.equal(saveStub.lastCall.args[0], lastKey.seq); assert.equal(element._keysToRemove.length, 0); @@ -105,11 +104,11 @@ }); }); - test('show key', function() { - var openSpy = sinon.spy(element.$.viewKeyOverlay, 'open'); + test('show key', () => { + const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open'); // Get the show button for the last row. - var button = Polymer.dom(element.root).querySelector( + const button = Polymer.dom(element.root).querySelector( 'tbody tr:last-of-type td:nth-child(3) gr-button'); MockInteractions.tap(button); @@ -118,9 +117,9 @@ assert.isTrue(openSpy.called); }); - test('add key', function(done) { - var newKeyString = 'ssh-rsa <key 3> comment-three@machine-three'; - var newKeyObject = { + test('add key', done => { + const newKeyString = 'ssh-rsa <key 3> comment-three@machine-three'; + const newKeyObject = { seq: 3, ssh_public_key: newKeyString, encoded_key: '<key 3>', @@ -129,15 +128,15 @@ valid: true, }; - var addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey', - function() { return Promise.resolve(newKeyObject); }); + const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey', + () => { return Promise.resolve(newKeyObject); }); element._newKey = newKeyString; assert.isFalse(element.$.addButton.disabled); assert.isFalse(element.$.newKey.disabled); - element._handleAddKey().then(function() { + element._handleAddKey().then(() => { assert.isTrue(element.$.addButton.disabled); assert.isFalse(element.$.newKey.disabled); assert.equal(element._keys.length, 3); @@ -151,18 +150,18 @@ assert.equal(addStub.lastCall.args[0], newKeyString); }); - test('add invalid key', function(done) { - var newKeyString = 'not even close to valid'; + test('add invalid key', done => { + const newKeyString = 'not even close to valid'; - var addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey', - function() { return Promise.reject(); }); + const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey', + () => { return Promise.reject(); }); element._newKey = newKeyString; assert.isFalse(element.$.addButton.disabled); assert.isFalse(element.$.newKey.disabled); - element._handleAddKey().then(function() { + element._handleAddKey().then(() => { assert.isFalse(element.$.addButton.disabled); assert.isFalse(element.$.newKey.disabled); assert.equal(element._keys.length, 2);
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html index 61d35f6..a834ea2 100644 --- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html +++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.html
@@ -17,46 +17,35 @@ <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/gr-settings-styles.html"> +<link rel="import" href="../../../styles/gr-form-styles.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-watched-projects-editor"> <template> - <style> - th.projectHeader { - width: 11em; - } - th.notificationHeader { - text-align: center; - } - th.notifType { + <style include="shared-styles"></style> + <style include="gr-form-styles"> + #watchedProjects .notifType { text-align: center; padding: 0 0.4em; } - td.notifControl { + .notifControl { cursor: pointer; text-align: center; } - td.notifControl:hover { - border: 1px solid #ddd; + .notifControl:hover { + outline: 1px solid #ddd; } .projectFilter { color: #777; font-style: italic; margin-left: 1em; } - input { - font-size: 1em; - } - .newProjectInput { - width: 10em; - } .newFilterInput { - width: 26em; + width: 100%; } </style> - <style include="gr-settings-styles"></style> - <div class="gr-settings-styles"> - <table> + <div class="gr-form-styles"> + <table id="watchedProjects"> <thead> <tr> <th class="projectHeader">Project</th> @@ -92,7 +81,7 @@ checked$="[[_computeCheckboxChecked(project, type.key)]]"> </td> </template> - <td class="delete-column"> + <td> <gr-button data-index$="[[projectIndex]]" on-tap="_handleRemoveProject">Delete</gr-button> @@ -105,8 +94,6 @@ <th> <gr-autocomplete id="newProject" - class="newProjectInput" - is="iron-input" query="[[_query]]" threshold="3" placeholder="Project"></gr-autocomplete>
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js index d65f512..c1eabad 100644 --- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
@@ -14,7 +14,7 @@ (function() { 'use strict'; - var NOTIFICATION_TYPES = [ + const NOTIFICATION_TYPES = [ {name: 'Changes', key: 'notify_new_changes'}, {name: 'Patches', key: 'notify_new_patch_sets'}, {name: 'Comments', key: 'notify_all_comments'}, @@ -35,24 +35,24 @@ _projects: Array, _projectsToRemove: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _query: { type: Function, - value: function() { + value() { return this._getProjectSuggestions.bind(this); }, }, }, - loadData: function() { - return this.$.restAPI.getWatchedProjects().then(function(projs) { + loadData() { + return this.$.restAPI.getWatchedProjects().then(projs => { this._projects = projs; - }.bind(this)); + }); }, - save: function() { - var deletePromise; + save() { + let deletePromise; if (this._projectsToRemove.length) { deletePromise = this.$.restAPI.deleteWatchedProjects( this._projectsToRemove); @@ -61,56 +61,57 @@ } return deletePromise - .then(function() { + .then(() => { return this.$.restAPI.saveWatchedProjects(this._projects); - }.bind(this)) - .then(function(projects) { + }) + .then(projects => { this._projects = projects; this._projectsToRemove = []; this.hasUnsavedChanges = false; - }.bind(this)); + }); }, - _getTypes: function() { + _getTypes() { return NOTIFICATION_TYPES; }, - _getTypeCount: function() { + _getTypeCount() { return this._getTypes().length; }, - _computeCheckboxChecked: function(project, key) { + _computeCheckboxChecked(project, key) { return project.hasOwnProperty(key); }, - _getProjectSuggestions: function(input) { + _getProjectSuggestions(input) { return this.$.restAPI.getSuggestedProjects(input) - .then(function(response) { - var projects = []; - for (var key in response) { - projects.push({ - name: key, - value: response[key], - }); - } - return projects; - }); + .then(response => { + const projects = []; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + projects.push({ + name: key, + value: response[key], + }); + } + return projects; + }); }, - _handleRemoveProject: function(e) { - var index = parseInt(e.target.getAttribute('data-index'), 10); - var project = this._projects[index]; + _handleRemoveProject(e) { + const index = parseInt(e.target.getAttribute('data-index'), 10); + const project = this._projects[index]; this.splice('_projects', index, 1); this.push('_projectsToRemove', project); this.hasUnsavedChanges = true; }, - _canAddProject: function(project, filter) { + _canAddProject(project, filter) { if (!project || !project.id) { return false; } // Check if the project with filter is already in the list. Compare // filters using == to coalesce null and undefined. - for (var i = 0; i < this._projects.length; i++) { + for (let i = 0; i < this._projects.length; i++) { if (this._projects[i].project === project.id && this._projects[i].filter == filter) { return false; @@ -120,8 +121,9 @@ return true; }, - _getNewProjectIndex: function(name, filter) { - for (var i = 0; i < this._projects.length; i++) { + _getNewProjectIndex(name, filter) { + let i; + for (i = 0; i < this._projects.length; i++) { if (this._projects[i].project > name || (this._projects[i].project === name && this._projects[i].filter > filter)) { @@ -131,18 +133,18 @@ return i; }, - _handleAddProject: function() { - var newProject = this.$.newProject.value; - var newProjectName = this.$.newProject.text; - var filter = this.$.newFilter.value || null; + _handleAddProject() { + const newProject = this.$.newProject.value; + const newProjectName = this.$.newProject.text; + const filter = this.$.newFilter.value || null; if (!this._canAddProject(newProject, filter)) { return; } - var insertIndex = this._getNewProjectIndex(newProjectName, filter); + const insertIndex = this._getNewProjectIndex(newProjectName, filter); this.splice('_projects', insertIndex, 0, { project: newProjectName, - filter: filter, + filter, _is_local: true, }); @@ -151,16 +153,16 @@ this.hasUnsavedChanges = true; }, - _handleCheckboxChange: function(e) { - var index = parseInt(e.target.getAttribute('data-index'), 10); - var key = e.target.getAttribute('data-key'); - var checked = e.target.checked; + _handleCheckboxChange(e) { + const index = parseInt(e.target.getAttribute('data-index'), 10); + const key = e.target.getAttribute('data-key'); + const checked = e.target.checked; this.set(['_projects', index, key], !!checked); this.hasUnsavedChanges = true; }, - _handleNotifCellTap: function(e) { - var checkbox = Polymer.dom(e.target).querySelector('input'); + _handleNotifCellTap(e) { + const checkbox = Polymer.dom(e.target).querySelector('input'); if (checkbox) { checkbox.click(); } }, });
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html index 59e87b0..a93beb1 100644 --- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-watched-projects-editor.html"> <script>void(0);</script> @@ -33,11 +32,12 @@ </test-fixture> <script> - suite('gr-watched-projects-editor tests', function() { - var element; + suite('gr-watched-projects-editor tests', () => { + let element; - setup(function(done) { - var projects = [{ + setup(done => { + const projects = [ + { project: 'project a', notify_submitted_changes: true, notify_abandoned_changes: true, @@ -57,8 +57,8 @@ ]; stub('gr-rest-api-interface', { - getSuggestedProjects: function(input) { - if (input.indexOf('the') === 0) { + getSuggestedProjects(input) { + if (input.startsWith('the')) { return Promise.resolve({'the project': { id: 'the project', state: 'ACTIVE', @@ -68,27 +68,27 @@ return Promise.resolve({}); } }, - getWatchedProjects: function() { + getWatchedProjects() { return Promise.resolve(projects); }, }); element = fixture('basic'); - element.loadData().then(function() { flush(done); }); + element.loadData().then(() => { flush(done); }); }); - test('renders', function() { - var rows = element.$$('table').querySelectorAll('tbody tr'); + test('renders', () => { + const rows = element.$$('table').querySelectorAll('tbody tr'); assert.equal(rows.length, 4); function getKeysOfRow(row) { - var boxes = rows[row].querySelectorAll('input[checked]'); + const boxes = rows[row].querySelectorAll('input[checked]'); return Array.prototype.map.call(boxes, - function(e) { return e.getAttribute('data-key'); }); + e => { return e.getAttribute('data-key'); }); } - var checkedKeys = getKeysOfRow(0); + let checkedKeys = getKeysOfRow(0); assert.equal(checkedKeys.length, 2); assert.equal(checkedKeys[0], 'notify_submitted_changes'); assert.equal(checkedKeys[1], 'notify_abandoned_changes'); @@ -107,22 +107,22 @@ assert.equal(checkedKeys[2], 'notify_all_comments'); }); - test('_getProjectSuggestions empty', function(done) { - element._getProjectSuggestions('nonexistent').then(function(projects) { + test('_getProjectSuggestions empty', done => { + element._getProjectSuggestions('nonexistent').then(projects => { assert.equal(projects.length, 0); done(); }); }); - test('_getProjectSuggestions non-empty', function(done) { - element._getProjectSuggestions('the project').then(function(projects) { + test('_getProjectSuggestions non-empty', done => { + element._getProjectSuggestions('the project').then(projects => { assert.equal(projects.length, 1); assert.equal(projects[0].name, 'the project'); done(); }); }); - test('_canAddProject', function() { + test('_canAddProject', () => { assert.isFalse(element._canAddProject(null, null)); assert.isFalse(element._canAddProject({}, null)); @@ -144,7 +144,7 @@ assert.isTrue(element._canAddProject({id: 'project b'}, 'filter 3')); }); - test('_getNewProjectIndex', function() { + test('_getNewProjectIndex', () => { // Projects are sorted in ASCII order. assert.equal(element._getNewProjectIndex('project A', 'filter'), 0); assert.equal(element._getNewProjectIndex('project a', 'filter'), 1); @@ -158,7 +158,7 @@ assert.equal(element._getNewProjectIndex('project c', 'filter'), 4); }); - test('_handleAddProject', function() { + test('_handleAddProject', () => { element.$.newProject.value = {id: 'project d'}; element.$.newProject.setText('project d'); element.$.newFilter.bindValue = ''; @@ -171,7 +171,7 @@ assert.isTrue(element._projects[4]._is_local); }); - test('_handleAddProject with invalid inputs', function() { + test('_handleAddProject with invalid inputs', () => { element.$.newProject.value = {id: 'project b'}; element.$.newProject.setText('project b'); element.$.newFilter.bindValue = 'filter 1'; @@ -181,14 +181,14 @@ assert.equal(element._projects.length, 4); }); - test('_handleRemoveProject', function() { + test('_handleRemoveProject', () => { assert.equal(element._projectsToRemove, 0); - var button = element.$$('table tbody tr:nth-child(2) gr-button'); + const button = element.$$('table tbody tr:nth-child(2) gr-button'); MockInteractions.tap(button); flushAsynchronousOperations(); - var rows = element.$$('table tbody').querySelectorAll('tr'); + const rows = element.$$('table tbody').querySelectorAll('tr'); assert.equal(rows.length, 3); assert.equal(element._projectsToRemove.length, 1);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html index ddfdae7..a517e6a 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html +++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.html
@@ -18,10 +18,11 @@ <link rel="import" href="../gr-account-link/gr-account-link.html"> <link rel="import" href="../gr-button/gr-button.html"> <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-account-chip"> <template> - <style> + <style include="shared-styles"> :host { display: block; overflow: hidden; @@ -75,6 +76,7 @@ id="remove" hidden$="[[!removable]]" hidden + tabindex="-1" aria-label="Remove" class$="remove [[_getBackgroundClass(transparentBackground)]]" on-tap="_handleRemoveTap">×</gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js index 33fc50e..c4650f3e 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js +++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -47,23 +47,23 @@ }, }, - ready: function() { - this._getHasAvatars().then(function(hasAvatars) { + ready() { + this._getHasAvatars().then(hasAvatars => { this.showAvatar = hasAvatars; - }.bind(this)); + }); }, - _getBackgroundClass: function(transparent) { + _getBackgroundClass(transparent) { return transparent ? 'transparentBackground' : ''; }, - _handleRemoveTap: function(e) { + _handleRemoveTap(e) { e.preventDefault(); this.fire('remove', {account: this.account}); }, - _getHasAvatars: function() { - return this.$.restAPI.getConfig().then(function(cfg) { + _getHasAvatars() { + return this.$.restAPI.getConfig().then(cfg => { return Promise.resolve(!!(cfg && cfg.plugin && cfg.plugin.has_avatars)); }); },
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html index 43721fe..a93198c 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html +++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.html
@@ -16,10 +16,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../gr-avatar/gr-avatar.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-account-label"> <template> - <style> + <style include="shared-styles"> :host { display: inline; }
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js index e9f18df..9ebef82 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js +++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -29,9 +29,9 @@ }, }, - _computeAccountTitle: function(account) { + _computeAccountTitle(account) { if (!account || (!account.name && !account.email)) { return; } - var result = ''; + let result = ''; if (account.name) { result += account.name; } @@ -41,11 +41,11 @@ return result; }, - _computeShowEmail: function(showEmail, account) { + _computeShowEmail(showEmail, account) { return !!(showEmail && account && account.email); }, - _computeEmailStr: function(account) { + _computeEmailStr(account) { if (!account || !account.email) { return ''; }
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html index de7d6a3..3b1e1de 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html +++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -20,6 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> <link rel="import" href="gr-account-label.html"> @@ -33,32 +34,32 @@ </test-fixture> <script> - suite('gr-account-label tests', function() { - var element; + suite('gr-account-label tests', () => { + let element; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, - getLoggedIn: function() { return Promise.resolve(false); }, + getConfig() { return Promise.resolve({}); }, + getLoggedIn() { return Promise.resolve(false); }, }); element = fixture('basic'); }); - test('null guard', function() { - assert.doesNotThrow(function() { + test('null guard', () => { + assert.doesNotThrow(() => { element.account = null; }); }); - test('missing email', function() { + test('missing email', () => { assert.equal('', element._computeEmailStr({name: 'foo'})); }); - test('computed fields', function() { + test('computed fields', () => { assert.equal(element._computeAccountTitle( { name: 'Andrew Bonventre', - email: 'andybons+gerrit@gmail.com' + email: 'andybons+gerrit@gmail.com', }), 'Andrew Bonventre <andybons+gerrit@gmail.com>'); @@ -69,7 +70,7 @@ assert.equal(element._computeShowEmail(true, { name: 'Andrew Bonventre', - email: 'andybons+gerrit@gmail.com' + email: 'andybons+gerrit@gmail.com', }), true); assert.equal(element._computeShowEmail(true, @@ -88,6 +89,5 @@ element._computeEmailStr({name: 'test', email: 'test'}), '(test)'); assert.equal(element._computeEmailStr({email: 'test'}, ''), 'test'); }); - }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html index 20b6e3f..79747ba 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html +++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
@@ -16,11 +16,13 @@ <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> <link rel="import" href="../gr-account-label/gr-account-label.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-account-link"> <template> - <style> + <style include="shared-styles"> :host { display: inline-block; } @@ -35,7 +37,7 @@ } </style> <span> - <a href$="[[_computeOwnerLink(account)]]"> + <a href$="[[_computeOwnerLink(account)]]" tabindex="-1"> <gr-account-label account="[[account]]" avatar-image-size="[[avatarImageSize]]" show-email="[[_computeShowEmail(account)]]"></gr-account-label>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js index 69beb78..9a287fc 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js +++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -29,13 +29,12 @@ Gerrit.BaseUrlBehavior, ], - _computeOwnerLink: function(account) { + _computeOwnerLink(account) { if (!account) { return; } - var accountID = account.email || account._account_id; - return this.getBaseUrl() + '/q/owner:' + encodeURIComponent(accountID); + return Gerrit.Nav.getUrlForOwner(account.email || account._account_id); }, - _computeShowEmail: function(account) { + _computeShowEmail(account) { return !!(account && !account.name); }, });
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html index 5cc0600..11b099b 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html +++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -20,7 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-account-link.html"> <script>void(0);</script> @@ -32,31 +32,19 @@ </test-fixture> <script> - suite('gr-account-link tests', function() { - var element; + suite('gr-account-link tests', () => { + let element; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, + getConfig() { return Promise.resolve({}); }, }); element = fixture('basic'); }); - test('computed fields', function() { - assert.equal(element._computeOwnerLink( - { - _account_id: 123, - email: 'andybons+gerrit@gmail.com' - }), - '/q/owner:andybons%2Bgerrit%40gmail.com'); - - assert.equal(element._computeOwnerLink({_account_id: 42}), - '/q/owner:42'); - + test('computed fields', () => { assert.equal(element._computeShowEmail({name: 'asd'}), false); - assert.equal(element._computeShowEmail({}), true); }); - }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html index 10eadf9..bf5e4e0 100644 --- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html +++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.html
@@ -16,10 +16,13 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../gr-button/gr-button.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<script src="../../../scripts/rootElement.js"></script> <dom-module id="gr-alert"> <template> - <style> + <style include="shared-styles"> /** * ALERT: DO NOT ADD TRANSITION PROPERTIES WITHOUT PROPERLY UNDERSTANDING * HOW THEY ARE USED IN THE CODE.
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js index 84846fb..bfe7c25 100644 --- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js +++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -41,50 +41,52 @@ _hideActionButton: Boolean, _boundTransitionEndHandler: { type: Function, - value: function() { return this._handleTransitionEnd.bind(this); }, + value() { return this._handleTransitionEnd.bind(this); }, }, + _actionCallback: Function, }, - attached: function() { + attached() { this.addEventListener('transitionend', this._boundTransitionEndHandler); }, - detached: function() { + detached() { this.removeEventListener('transitionend', this._boundTransitionEndHandler); }, - show: function(text, opt_actionText) { + show(text, opt_actionText, opt_actionCallback) { this.text = text; this.actionText = opt_actionText; this._hideActionButton = !opt_actionText; - document.body.appendChild(this); + this._actionCallback = opt_actionCallback; + Gerrit.getRootElement().appendChild(this); this._setShown(true); }, - hide: function() { + hide() { this._setShown(false); if (this._hasZeroTransitionDuration()) { - document.body.removeChild(this); + Gerrit.getRootElement().removeChild(this); } }, - _hasZeroTransitionDuration: function() { - var style = window.getComputedStyle(this); + _hasZeroTransitionDuration() { + const style = window.getComputedStyle(this); // transitionDuration is always given in seconds. - var duration = Math.round(parseFloat(style.transitionDuration) * 100); + const duration = Math.round(parseFloat(style.transitionDuration) * 100); return duration === 0; }, - _handleTransitionEnd: function(e) { + _handleTransitionEnd(e) { if (this.shown) { return; } - document.body.removeChild(this); + Gerrit.getRootElement().removeChild(this); }, - _handleActionTap: function(e) { + _handleActionTap(e) { e.preventDefault(); - this.fire('action', null, {bubbles: false}); + if (this._actionCallback) { this._actionCallback(); } }, }); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html index 067ac5b..b8dcb8d 100644 --- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html +++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
@@ -20,25 +20,24 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-alert.html"> <script> - suite('gr-alert tests', function() { - var element; + suite('gr-alert tests', () => { + let element; - setup(function() { + setup(() => { element = document.createElement('gr-alert'); }); - teardown(function() { + teardown(() => { if (element.parentNode) { element.parentNode.removeChild(element); } }); - test('show/hide', function() { + test('show/hide', () => { assert.isNull(element.parentNode); element.show(); assert.equal(element.parentNode, document.body); @@ -48,13 +47,10 @@ assert.isNull(element.parentNode); }); - test('action event', function(done) { + test('action event', done => { element.show(); - element.addEventListener('action', function() { - done(); - }); + element._actionCallback = done; MockInteractions.tap(element.$$('.action')); }); - }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html new file mode 100644 index 0000000..dcb38d4 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html
@@ -0,0 +1,73 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> +<script src="../../../scripts/rootElement.js"></script> +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-autocomplete-dropdown"> + <template> + <style include="shared-styles"> + :host { + background: #fff; + box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px; + position: absolute; + z-index: 104; + } + /* This must be set here vs. the container component because in some cases + the element is moved in the DOM to a base element and is no longer a + child of its original parent. */ + :host(.fixed){ + position: fixed; + } + ul { + list-style: none; + } + li { + cursor: pointer; + padding: .5em .75em; + } + li:focus { + outline: none; + } + li.selected { + background-color: #eee; + } + </style> + <div id="suggestions" role="listbox"> + <ul> + <template is="dom-repeat" items="[[suggestions]]"> + <li data-index$="[[index]]" + data-value$="[[item.dataValue]]" + tabindex="-1" + aria-label$="[[item.name]]" + role="option" + on-tap="_handleTapItem">[[item.text]]</li> + </template> + </ul> + </div> + <gr-cursor-manager + id="cursor" + index="{{index}}" + cursor-target-class="selected" + scroll-behavior="never" + focus-on-move + stops="[[_suggestionEls]]"></gr-cursor-manager> + </template> + <script src="gr-autocomplete-dropdown.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js new file mode 100644 index 0000000..68800a1 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -0,0 +1,168 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-autocomplete-dropdown', + + /** + * Fired when the dropdown is closed. + * + * @event dropdown-closed + */ + + /** + * Fired when item is selected. + * + * @event item-selected + */ + + properties: { + index: Number, + moveToRoot: Boolean, + fixedPosition: Boolean, + suggestions: { + type: Array, + observer: '_resetCursorStops', + }, + _suggestionEls: { + type: Array, + observer: '_resetCursorIndex', + }, + }, + + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + ], + + keyBindings: { + up: '_handleUp', + down: '_handleDown', + enter: '_handleEnter', + esc: '_handleEscape', + tab: '_handleTab', + }, + + attached() { + if (this.fixedPosition) { + this.classList.add('fixed'); + } + }, + + close() { + if (this.moveToRoot) { + Gerrit.getRootElement().removeChild(this); + } else { + this.hidden = true; + } + }, + + open() { + if (this.moveToRoot) { + Gerrit.getRootElement().appendChild(this); + } + this._resetCursorStops(); + this._resetCursorIndex(); + }, + + setPosition(top, left) { + this.style.top = top; + this.style.left = left; + }, + + getCurrentText() { + return this.getCursorTarget().dataset.value; + }, + + _handleUp(e) { + if (!this.hidden) { + e.preventDefault(); + e.stopPropagation(); + this.cursorUp(); + } + }, + + _handleDown(e) { + if (!this.hidden) { + e.preventDefault(); + e.stopPropagation(); + this.cursorDown(); + } + }, + + cursorDown(e) { + if (!this.hidden) { + this.$.cursor.next(); + } + }, + + cursorUp(e) { + if (!this.hidden) { + this.$.cursor.previous(); + } + }, + + _handleTab(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('item-selected', { + trigger: 'tab', + selected: this.$.cursor.target, + }); + }, + + _handleEnter(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('item-selected', { + trigger: 'enter', + selected: this.$.cursor.target, + }); + }, + + _handleEscape() { + this._fireClose(); + if (!this.hidden) { + this.close(); + } + }, + + _handleTapItem(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('item-selected', { + trigger: 'tap', + selected: e.target, + }); + }, + + _fireClose() { + this.fire('dropdown-closed'); + }, + + getCursorTarget() { + return this.$.cursor.target; + }, + + _resetCursorStops() { + Polymer.dom.flush(); + this._suggestionEls = this.$.suggestions.querySelectorAll('li'); + }, + + _resetCursorIndex() { + this.$.cursor.setCursorAtIndex(0); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html new file mode 100644 index 0000000..db9440c --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
@@ -0,0 +1,204 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-autocomplete-dropdown</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-autocomplete-dropdown.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-autocomplete-dropdown id="dropdown"></gr-autocomplete-dropdown> + </template> +</test-fixture> + +<test-fixture id="move"> + <template> + <gr-autocomplete-dropdown id="dropdown" move-to-root></gr-autocomplete-dropdown> + </template> +</test-fixture> + +<script> + suite('gr-autocomplete-dropdown', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.open(); + element.suggestions = [ + {dataValue: 'test value 1', name: 'test name 1', text: 1}, + {dataValue: 'test value 2', name: 'test name 2', text: 2}]; + }); + + teardown(() => { + sandbox.restore(); + if (element.isOpen) element.close(); + }); + + test('dropdown has not been moved from text fixture to the body', () => { + assert.equal(Polymer.dom(document.root) + .querySelectorAll('gr-autocomplete-dropdown').length, 1); + const dropdown = Polymer.dom(document.root) + .querySelector('gr-autocomplete-dropdown'); + assert.isOk(dropdown); + assert.notDeepEqual(dropdown.parentElement, + Polymer.dom(document.root).querySelector('body')); + }); + + test('escape key', () => { + const listener = sandbox.spy(); + element.hidden = false; + element.addEventListener('dropdown-closed', listener); + const closeSpy = sandbox.spy(element, 'close'); + MockInteractions.pressAndReleaseKeyOn(element, 27); + flushAsynchronousOperations(); + assert.isTrue(listener.called); + assert.isTrue(closeSpy.called); + assert.isTrue(element.hidden); + }); + + test('tab key', () => { + const handleTabSpy = sandbox.spy(element, '_handleTab'); + const itemSelectedStub = sandbox.stub(); + element.addEventListener('item-selected', itemSelectedStub); + MockInteractions.pressAndReleaseKeyOn(element, 9); + assert.isTrue(handleTabSpy.called); + assert.equal(element.$.cursor.index, 0); + assert.isTrue(itemSelectedStub.called); + assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, { + trigger: 'tab', + selected: element.getCursorTarget(), + }); + }); + + test('enter key', () => { + const handleEnterSpy = sandbox.spy(element, '_handleEnter'); + const itemSelectedStub = sandbox.stub(); + element.addEventListener('item-selected', itemSelectedStub); + MockInteractions.pressAndReleaseKeyOn(element, 13); + assert.isTrue(handleEnterSpy.called); + assert.equal(element.$.cursor.index, 0); + assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, { + trigger: 'enter', + selected: element.getCursorTarget(), + }); + }); + + test('down key', () => { + element.hidden = true; + const nextSpy = sandbox.spy(element.$.cursor, 'next'); + MockInteractions.pressAndReleaseKeyOn(element, 40); + assert.isFalse(nextSpy.called); + assert.equal(element.$.cursor.index, 0); + element.hidden = false; + MockInteractions.pressAndReleaseKeyOn(element, 40); + assert.isTrue(nextSpy.called); + assert.equal(element.$.cursor.index, 1); + }); + + test('up key', () => { + element.hidden = true; + const prevSpy = sandbox.spy(element.$.cursor, 'previous'); + MockInteractions.pressAndReleaseKeyOn(element, 38); + assert.isFalse(prevSpy.called); + assert.equal(element.$.cursor.index, 0); + element.hidden = false; + element.$.cursor.setCursorAtIndex(1); + assert.equal(element.$.cursor.index, 1); + MockInteractions.pressAndReleaseKeyOn(element, 38); + assert.isTrue(prevSpy.called); + assert.equal(element.$.cursor.index, 0); + }); + + test('tapping selects item', () => { + const itemSelectedStub = sandbox.stub(); + element.addEventListener('item-selected', itemSelectedStub); + + MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[1]); + flushAsynchronousOperations(); + assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, { + trigger: 'tap', + selected: element.$.suggestions.querySelectorAll('li')[1], + }); + }); + + test('updated suggestions resets cursor stops', () => { + resetStopsSpy = sandbox.spy(element, '_resetCursorStops'); + element.suggestions = []; + assert.isTrue(resetStopsSpy.called); + }); + }); + + suite('gr-autocomplete-dropdown to root', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + fixture('move').open(); + // The element was moved to the body, so look for it there. + element = Polymer.dom(document.root) + .querySelector('gr-autocomplete-dropdown'); + element.suggestions = [ + {dataValue: 'test value 1', name: 'test name 1', text: 1}, + {dataValue: 'test value 2', name: 'test name 2', text: 2}]; + }); + + teardown(() => { + sandbox.restore(); + if (!element.hidden) element.close(); + }); + + test('dropdown has been moved from the text fixture to the body', () => { + assert.equal(Polymer.dom(document.root) + .querySelectorAll('gr-autocomplete-dropdown').length, 1); + const dropdown = Polymer.dom(document.root) + .querySelector('gr-autocomplete-dropdown'); + assert.isOk(dropdown); + assert.deepEqual(dropdown.parentElement, Polymer.dom(document.root) + .querySelector('body')); + }); + + test('closing removes from body and adding adds to body', () => { + element.close(); + assert.equal(Polymer.dom(document.root) + .querySelectorAll('gr-autocomplete-dropdown').length, 0); + element.open(); + assert.equal(Polymer.dom(document.root) + .querySelectorAll('gr-autocomplete-dropdown').length, 1); + }); + + test('setPosition', () => { + const top = '10px'; + const left = '20px'; + element.setPosition(top, left); + assert.equal(getComputedStyle(element).top, top); + assert.equal(getComputedStyle(element).left, left); + }); + + test('getCurrentText', () => { + assert.equal(element.getCurrentText(), 'test value 1'); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html index aeb7e5f..2a86a0c 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.html
@@ -16,39 +16,26 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> <link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html"> <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-autocomplete"> <template> - <style> + <style include="shared-styles"> input { font-size: 1em; height: 100%; width: 100%; + @apply --gr-autocomplete; } input.borderless, input.borderless:focus { border: none; outline: none; } - #suggestions { - background-color: #fff; - box-shadow: 0 1px 3px rgba(0, 0, 0, .3); - position: absolute; - z-index: 10; - } - ul { - list-style: none; - } - li { - cursor: pointer; - padding: .5em .75em; - } - li:focus { - outline: none; - } - li.selected { - background-color: #eee; + input.warnUncommitted { + color: red; } </style> <input @@ -60,30 +47,18 @@ placeholder="[[placeholder]]" on-keydown="_handleKeydown" on-focus="_onInputFocus" + on-blur="_onInputBlur" autocomplete="off" /> - <div - id="suggestions" - role="listbox" - hidden$="[[_computeSuggestionsHidden(_suggestions, _focused)]]"> - <ul> - <template is="dom-repeat" items="[[_suggestions]]"> - <li - data-index$="[[index]]" - tabindex="-1" - aria-label$="[[item.name]]" - on-keydown="_handleKeydown" - role="option" - on-tap="_handleSuggestionTap">[[item.name]]</li> - </template> - </ul> + <!-- This container is needed for Safari and Firefox --> + <div id="suggestionContainer"> + <gr-autocomplete-dropdown id="suggestions" + on-item-selected="_handleItemSelect" + suggestions="[[_suggestions]]" + role="listbox" + index="[[_index]]" + hidden$="[[_computeSuggestionsHidden(_suggestions, _focused)]]"> + </gr-autocomplete-dropdown> </div> - <gr-cursor-manager - id="cursor" - index="{{_index}}" - cursor-target-class="selected" - scroll-behavior="keep-visible" - focus-on-move - stops="[[_getSuggestionElems(_suggestions)]]"></gr-cursor-manager> </template> <script src="gr-autocomplete.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js index 9ebb794..4670c73 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -14,7 +14,7 @@ (function() { 'use strict'; - var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g; + const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g; Polymer({ is: 'gr-autocomplete', @@ -52,7 +52,7 @@ */ query: { type: Function, - value: function() { + value() { return function() { return Promise.resolve([]); }; @@ -61,7 +61,7 @@ /** * The number of characters that must be typed before suggestions are - * made. + * made. If threshold is zero, default suggestions are enabled. */ threshold: { type: Number, @@ -105,30 +105,42 @@ value: false, }, + /** + * When true and uncommitted text is left in the autocomplete input after + * blurring, the text will appear red. + */ + warnUncommitted: { + type: Boolean, + value: false, + }, + _suggestions: { type: Array, - value: function() { return []; }, + value() { return []; }, + }, + + _suggestionEls: { + type: Array, + value() { return []; }, }, _index: Number, - _disableSuggestions: { type: Boolean, value: false, }, - _focused: { type: Boolean, value: false, }, - + _selected: Object, }, - attached: function() { + attached() { this.listen(document.body, 'tap', '_handleBodyTap'); }, - detached: function() { + detached() { this.unlisten(document.body, 'tap', '_handleBodyTap'); }, @@ -136,82 +148,96 @@ return this.$.input; }, - focus: function() { + focus() { this.$.input.focus(); }, - selectAll: function() { + selectAll() { this.$.input.setSelectionRange(0, this.$.input.value.length); }, - clear: function() { + clear() { this.text = ''; }, + _handleItemSelect(e) { + let silent = false; + if (e.detail.trigger === 'tab' && this.tabCompleteWithoutCommit) { + silent = true; + } + this._selected = e.detail.selected; + this._commit(silent); + if (silent) { this.focus(); } + }, + /** * Set the text of the input without triggering the suggestion dropdown. * @param {String} text The new text for the input. */ - setText: function(text) { + setText(text) { this._disableSuggestions = true; this.text = text; this._disableSuggestions = false; }, - _onInputFocus: function() { + _onInputFocus() { this._focused = true; this._updateSuggestions(); + this.$.input.classList.remove('warnUncommitted'); }, - _updateSuggestions: function() { - if (!this.text || this._disableSuggestions) { return; } - if (this.text.length < this.threshold) { + _onInputBlur() { + this.$.input.classList.toggle('warnUncommitted', + this.warnUncommitted && this.text.length); + }, + + _updateSuggestions() { + if (this._disableSuggestions) { return; } + if (this.text === undefined || this.text.length < this.threshold) { this._suggestions = []; - this.value = null; + this.value = ''; return; } - var text = this.text; + const text = this.text; - this.query(text).then(function(suggestions) { + this.query(text).then(suggestions => { if (text !== this.text) { // Late response. return; } - this._suggestions = suggestions; - this.$.cursor.moveToStart(); - if (this._index === -1) { - this.value = null; + for (const suggestion of suggestions) { + suggestion.text = suggestion.name; } - }.bind(this)); + this._suggestions = suggestions; + Polymer.dom.flush(); + if (this._index === -1) { + this.value = ''; + } + }); }, - _computeSuggestionsHidden: function(suggestions, focused) { + _computeSuggestionsHidden(suggestions, focused) { return !(suggestions.length && focused); }, - _computeClass: function(borderless) { + _computeClass(borderless) { return borderless ? 'borderless' : ''; }, - _getSuggestionElems: function() { - Polymer.dom.flush(); - return this.$.suggestions.querySelectorAll('li'); - }, - /** * _handleKeydown used for key handling in the this.$.input AND all child * autocomplete options. */ - _handleKeydown: function(e) { + _handleKeydown(e) { this._focused = true; switch (e.keyCode) { case 38: // Up e.preventDefault(); - this.$.cursor.previous(); + this.$.suggestions.cursorUp(); break; case 40: // Down e.preventDefault(); - this.$.cursor.next(); + this.$.suggestions.cursorDown(); break; case 27: // Escape e.preventDefault(); @@ -220,12 +246,12 @@ case 9: // Tab if (this._suggestions.length > 0) { e.preventDefault(); - this._commit(this.tabCompleteWithoutCommit); + this._handleInputCommit(this.tabCompleteWithoutCommit); } break; case 13: // Enter e.preventDefault(); - this._commit(); + this._handleInputCommit(); break; default: // For any normal keypress, return focus to the input to allow for @@ -235,18 +261,26 @@ this.fire('input-keydown', {keyCode: e.keyCode, input: this.$.input}); }, - _cancel: function() { - this._suggestions = []; - this.fire('cancel'); + _cancel() { + if (this._suggestions.length) { + this._suggestions = []; + } else { + this.fire('cancel'); + } }, - _updateValue: function(suggestions, index) { - if (!suggestions.length || index === -1) { return; } - var completed = suggestions[index].value; + _handleInputCommit(opt_tabCompleteWithoutCommit) { + this._selected = this.$.suggestions.getCursorTarget(); + this._commit(opt_tabCompleteWithoutCommit); + }, + + _updateValue(suggestion, suggestions) { + if (!suggestion) { return; } + const completed = suggestions[suggestion.dataset.index].value; if (this.multi) { // Append the completed text to the end of the string. // Allow spaces within quoted terms. - var tokens = this.text.match(TOKENIZE_REGEX); + const tokens = this.text.match(TOKENIZE_REGEX); tokens[tokens.length - 1] = completed; this.value = tokens.join(' '); } else { @@ -254,9 +288,9 @@ } }, - _handleBodyTap: function(e) { - var eventPath = Polymer.dom(e).path; - for (var i = 0; i < eventPath.length; i++) { + _handleBodyTap(e) { + const eventPath = Polymer.dom(e).path; + for (let i = 0; i < eventPath.length; i++) { if (eventPath[i] === this) { return; } @@ -264,11 +298,10 @@ this._focused = false; }, - _handleSuggestionTap: function(e) { + _handleSuggestionTap(e) { e.stopPropagation(); this.$.cursor.setCursor(e.target); this._commit(); - this.focus(); }, /** @@ -278,22 +311,22 @@ * suggestion in order to handle cases like tab-to-complete without * firing the commit event. */ - _commit: function(silent) { + _commit(silent) { // Allow values that are not in suggestion list iff suggestions are empty. if (this._suggestions.length > 0) { - this._updateValue(this._suggestions, this._index); + this._updateValue(this._selected, this._suggestions); } else { this.value = this.text || ''; } - var value = this.value; + const value = this.value; // Value and text are mirrors of each other in multi mode. if (this.multi) { this.setText(this.value); } else { - if (!this.clearOnCommit && this._suggestions[this._index]) { - this.setText(this._suggestions[this._index].name); + if (!this.clearOnCommit && this._selected) { + this.setText(this._suggestions[this._selected.dataset.index].name); } else { this.clear(); } @@ -301,7 +334,7 @@ this._suggestions = []; if (!silent) { - this.fire('commit', {value: value}); + this.fire('commit', {value}); } }, });
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html index 4234388..adfd1f6 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-autocomplete.html"> <script>void(0);</script> @@ -33,16 +32,22 @@ </test-fixture> <script> - suite('gr-autocomplete tests', function() { - var element; + suite('gr-autocomplete tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { element = fixture('basic'); + sandbox = sinon.sandbox.create(); }); - test('renders', function(done) { - var promise; - var queryStub = sinon.spy(function(input) { + teardown(() => { + sandbox.restore(); + }); + + test('renders', done => { + let promise; + const queryStub = sandbox.spy(input => { return promise = Promise.resolve([ {name: input + ' 0', value: 0}, {name: input + ' 1', value: 1}, @@ -52,61 +57,63 @@ ]); }); element.query = queryStub; - - assert.isTrue(element.$.suggestions.hasAttribute('hidden')); - assert.equal(element.$.cursor.index, -1); + assert.isTrue(element.$.suggestions.hidden); + assert.equal(element.$.suggestions.$.cursor.index, -1); element.text = 'blah'; assert.isTrue(queryStub.called); element._focused = true; - promise.then(function() { + promise.then(() => { assert.isFalse(element.$.suggestions.hasAttribute('hidden')); - - var suggestions = element.$.suggestions.querySelectorAll('li'); + const suggestions = + Polymer.dom(element.$.suggestions.root).querySelectorAll('li'); assert.equal(suggestions.length, 5); - for (var i = 0; i < 5; i++) { + for (let i = 0; i < 5; i++) { assert.equal(suggestions[i].textContent, 'blah ' + i); } - assert.notEqual(element.$.cursor.index, -1); + assert.notEqual(element.$.suggestions.$.cursor.index, -1); done(); }); }); - test('emits cancel', function(done) { - var promise; - var queryStub = sinon.spy(function() { + test('esc key behavior', done => { + let promise; + const queryStub = sandbox.spy(() => { return promise = Promise.resolve([ {name: 'blah', value: 123}, ]); }); element.query = queryStub; - assert.isTrue(element.$.suggestions.hasAttribute('hidden')); + assert.isTrue(element.$.suggestions.hidden); element._focused = true; element.text = 'blah'; - promise.then(function() { - assert.isFalse(element.$.suggestions.hasAttribute('hidden')); + promise.then(() => { + assert.isFalse(element.$.suggestions.hidden); - var cancelHandler = sinon.spy(); + const cancelHandler = sandbox.spy(); element.addEventListener('cancel', cancelHandler); MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc'); - assert.isTrue(cancelHandler.called); - assert.isTrue(element.$.suggestions.hasAttribute('hidden')); + assert.isFalse(cancelHandler.called); + assert.isTrue(element.$.suggestions.hidden); + assert.equal(element._suggestions.length, 0); + MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc'); + assert.isTrue(cancelHandler.called); done(); }); }); - test('emits commit and handles cursor movement', function(done) { - var promise; - var queryStub = sinon.spy(function(input) { + test('emits commit and handles cursor movement', done => { + let promise; + const queryStub = sandbox.spy(input => { return promise = Promise.resolve([ {name: input + ' 0', value: 0}, {name: input + ' 1', value: 1}, @@ -117,32 +124,32 @@ }); element.query = queryStub; - assert.isTrue(element.$.suggestions.hasAttribute('hidden')); - assert.equal(element.$.cursor.index, -1); + assert.isTrue(element.$.suggestions.hidden); + assert.equal(element.$.suggestions.$.cursor.index, -1); element._focused = true; element.text = 'blah'; - promise.then(function() { - assert.isFalse(element.$.suggestions.hasAttribute('hidden')); + promise.then(() => { + assert.isFalse(element.$.suggestions.hidden); - var commitHandler = sinon.spy(); + const commitHandler = sandbox.spy(); element.addEventListener('commit', commitHandler); - assert.equal(element.$.cursor.index, 0); + assert.equal(element.$.suggestions.$.cursor.index, 0); MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null, 'down'); - assert.equal(element.$.cursor.index, 1); + assert.equal(element.$.suggestions.$.cursor.index, 1); MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null, 'down'); - assert.equal(element.$.cursor.index, 2); + assert.equal(element.$.suggestions.$.cursor.index, 2); MockInteractions.pressAndReleaseKeyOn(element.$.input, 38, null, 'up'); - assert.equal(element.$.cursor.index, 1); + assert.equal(element.$.suggestions.$.cursor.index, 1); MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, 'enter'); @@ -150,22 +157,22 @@ assert.equal(element.value, 1); assert.isTrue(commitHandler.called); assert.equal(commitHandler.getCall(0).args[0].detail.value, 1); - assert.isTrue(element.$.suggestions.hasAttribute('hidden')); - + assert.isTrue(element.$.suggestions.hidden); + assert.isTrue(element._focused); done(); }); }); - test('clear-on-commit behavior (off)', function(done) { - var promise; - var queryStub = sinon.spy(function() { + test('clear-on-commit behavior (off)', done => { + let promise; + const queryStub = sandbox.spy(() => { return promise = Promise.resolve([{name: 'suggestion', value: 0}]); }); element.query = queryStub; element.text = 'blah'; - promise.then(function() { - var commitHandler = sinon.spy(); + promise.then(() => { + const commitHandler = sandbox.spy(); element.addEventListener('commit', commitHandler); MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, @@ -177,17 +184,17 @@ }); }); - test('clear-on-commit behavior (on)', function(done) { - var promise; - var queryStub = sinon.spy(function() { + test('clear-on-commit behavior (on)', done => { + let promise; + const queryStub = sandbox.spy(() => { return promise = Promise.resolve([{name: 'suggestion', value: 0}]); }); element.query = queryStub; element.text = 'blah'; element.clearOnCommit = true; - promise.then(function() { - var commitHandler = sinon.spy(); + promise.then(() => { + const commitHandler = sandbox.spy(); element.addEventListener('commit', commitHandler); MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, @@ -199,8 +206,8 @@ }); }); - test('threshold guards the query', function() { - var queryStub = sinon.spy(function() { + test('threshold guards the query', () => { + const queryStub = sandbox.spy(() => { return Promise.resolve([]); }); element.query = queryStub; @@ -216,29 +223,29 @@ assert.isTrue(queryStub.called); }); - test('_computeClass respects border property', function() { + test('_computeClass respects border property', () => { assert.equal(element._computeClass(), ''); assert.equal(element._computeClass(false), ''); assert.equal(element._computeClass(true), 'borderless'); }); - test('undefined or empty text results in no suggestions', function() { - sinon.spy(element, '_updateSuggestions'); + test('undefined or empty text results in no suggestions', () => { + sandbox.spy(element, '_updateSuggestions'); element.text = undefined; assert(element._updateSuggestions.calledOnce); assert.equal(element._suggestions.length, 0); }); - test('multi completes only the last part of the query', function(done) { - var promise; - var queryStub = sinon.stub() + test('multi completes only the last part of the query', done => { + let promise; + const queryStub = sandbox.stub() .returns(promise = Promise.resolve([{name: 'suggestion', value: 0}])); element.query = queryStub; element.text = 'blah blah'; element.multi = true; - promise.then(function() { - var commitHandler = sinon.spy(); + promise.then(() => { + const commitHandler = sandbox.spy(); element.addEventListener('commit', commitHandler); MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, @@ -250,19 +257,19 @@ }); }); - test('tab key completes only when suggestions exist', function() { - var commitStub = sinon.stub(element, '_commit'); + test('tab key completes only when suggestions exist', () => { + const commitStub = sandbox.stub(element, '_commit'); element._suggestions = []; MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); assert.isFalse(commitStub.called); element._suggestions = ['tunnel snakes rule!']; MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); assert.isTrue(commitStub.called); - commitStub.restore(); + assert.isTrue(element._focused); }); - test('tabCompleteWithoutCommit flag functions', function() { - var commitHandler = sinon.spy(); + test('tabCompleteWithoutCommit flag functions', () => { + const commitHandler = sandbox.spy(); element.addEventListener('commit', commitHandler); element._suggestions = ['tunnel snakes rule!']; element.tabCompleteWithoutCommit = true; @@ -274,44 +281,143 @@ assert.isTrue(commitHandler.called); }); - test('_focused flag properly triggered', function(done) { - flush(function() { + test('_focused flag properly triggered', done => { + flush(() => { assert.isFalse(element._focused); - var input = element.$$('input'); + const input = element.$$('input'); MockInteractions.focus(input); assert.isTrue(element._focused); done(); }); }); - test('_focused flag shows/hides the suggestions', function() { - var suggestions = ['hello', 'its me']; + test('_focused flag shows/hides the suggestions', () => { + const suggestions = ['hello', 'its me']; assert.isTrue(element._computeSuggestionsHidden(suggestions, false)); assert.isFalse(element._computeSuggestionsHidden(suggestions, true)); }); - test('tap on suggestion commits and refocuses on input', function() { - var focusSpy = sinon.spy(element, 'focus'); - var commitSpy = sinon.spy(element, '_commit'); - element._focused = true; - element._suggestions = [{name: 'first suggestion'}]; - assert.isFalse(element.$.suggestions.hasAttribute('hidden')); - MockInteractions.tap(element.$$('#suggestions li:first-child')); - flushAsynchronousOperations(); - assert.isTrue(focusSpy.called); - assert.isTrue(commitSpy.called); - assert.isTrue(element.$.suggestions.hasAttribute('hidden')); - assert.isTrue(element._focused); - focusSpy.restore(); - commitSpy.restore(); + suite('focus', () => { + let commitSpy; + let focusSpy; + + setup(() => { + commitSpy = sandbox.spy(element, '_commit'); + }); + + test('enter does not call focus', () => { + element._suggestions = ['sugar bombs']; + focusSpy = sandbox.spy(element, 'focus'); + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, + 'enter'); + flushAsynchronousOperations(); + + assert.isTrue(commitSpy.called); + assert.isFalse(focusSpy.called); + assert.equal(element._suggestions.length, 0); + }); + + test('tab in input does not call focus', () => { + element._suggestions = ['sugar bombs']; + focusSpy = sandbox.spy(element, 'focus'); + MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); + flushAsynchronousOperations(); + + assert.isTrue(commitSpy.called); + assert.isFalse(focusSpy.called); + assert.equal(element._suggestions.length, 0); + + element.tabCompleteWithoutCommit = true; + element._suggestions = ['tunnel snakes drool']; + MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); + assert.isFalse(focusSpy.called); + }); + + test('tab on suggestion, tabCompleteWithoutCommit = false', () => { + element._suggestions = [{name: 'sugar bombs'}]; + element._focused = true; + // When tabCompleteWithoutCommit is false, do not focus. + element.tabCompleteWithoutCommit = false; + focusSpy = sandbox.spy(element, 'focus'); + Polymer.dom.flush(); + assert.isFalse(element.$.suggestions.hidden); + + MockInteractions.pressAndReleaseKeyOn( + element.$.suggestions.$$('li:first-child'), 9, null, 'tab'); + flushAsynchronousOperations(); + + assert.isTrue(commitSpy.called); + assert.isFalse(focusSpy.called); + }); + + test('tab on suggestion, tabCompleteWithoutCommit = true', () => { + element._suggestions = [{name: 'sugar bombs'}]; + element._focused = true; + // When tabCompleteWithoutCommit is true, focus. + element.tabCompleteWithoutCommit = true; + focusSpy = sandbox.spy(element, 'focus'); + Polymer.dom.flush(); + assert.isFalse(element.$.suggestions.hidden); + + MockInteractions.pressAndReleaseKeyOn( + element.$.suggestions.$$('li:first-child'), 9, null, 'tab'); + flushAsynchronousOperations(); + + assert.isTrue(commitSpy.called); + assert.isTrue(focusSpy.called); + }); + + test('tap on suggestion commits, does not call focus', () => { + focusSpy = sandbox.spy(element, 'focus'); + element._focused = true; + element._suggestions = [{name: 'first suggestion'}]; + Polymer.dom.flush(); + assert.isFalse(element.$.suggestions.hidden); + MockInteractions.tap(element.$.suggestions.$$('li:first-child')); + flushAsynchronousOperations(); + + assert.isFalse(focusSpy.called); + assert.isTrue(commitSpy.called); + assert.isTrue(element.$.suggestions.hidden); + }); }); - test('input-keydown event fired', function() { - var listener = sinon.spy(); + test('input-keydown event fired', () => { + const listener = sandbox.spy(); element.addEventListener('input-keydown', listener); MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); flushAsynchronousOperations(); assert.isTrue(listener.called); }); + + suite('warnUncommitted', () => { + let inputClassList; + setup(() => { + inputClassList = element.$.input.classList; + }); + + test('enabled', () => { + element.warnUncommitted = true; + element.text = 'blah blah blah'; + MockInteractions.blur(element.$.input); + assert.isTrue(inputClassList.contains('warnUncommitted')); + MockInteractions.focus(element.$.input); + assert.isFalse(inputClassList.contains('warnUncommitted')); + }); + + test('disabled', () => { + element.warnUncommitted = false; + element.text = 'blah blah blah'; + MockInteractions.blur(element.$.input); + assert.isFalse(inputClassList.contains('warnUncommitted')); + }); + + test('no text', () => { + element.warnUncommitted = true; + element.text = ''; + MockInteractions.blur(element.$.input); + assert.isFalse(inputClassList.contains('warnUncommitted')); + }); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html index 55655c0..4095d3e 100644 --- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html +++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.html
@@ -16,10 +16,12 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-avatar"> <template> - <style> + <style include="shared-styles"> :host { display: inline-block; border-radius: 50%;
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js index 38e9924..5fca577 100644 --- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js +++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -28,43 +28,48 @@ }, }, - created: function() { + behaviors: [ + Gerrit.BaseUrlBehavior, + ], + + created() { this.hidden = true; }, - attached: function() { - this.$.restAPI.getConfig().then(function(cfg) { - var hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); + attached() { + this.$.restAPI.getConfig().then(cfg => { + const hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); if (hasAvatars) { this.hidden = false; // src needs to be set if avatar becomes visible this._updateAvatarURL(this.account); } - }.bind(this)); + }); }, - _accountChanged: function(account) { + _accountChanged(account) { this._updateAvatarURL(account); }, - _updateAvatarURL: function(account) { + _updateAvatarURL(account) { if (!this.hidden && account) { - var url = this._buildAvatarURL(this.account); + const url = this._buildAvatarURL(this.account); if (url) { this.style.backgroundImage = 'url("' + url + '")'; } } }, - _buildAvatarURL: function(account) { + _buildAvatarURL(account) { if (!account) { return ''; } - var avatars = account.avatars || []; - for (var i = 0; i < avatars.length; i++) { + const avatars = account.avatars || []; + for (let i = 0; i < avatars.length; i++) { if (avatars[i].height === this.imageSize) { return avatars[i].url; } } - return '/accounts/' + account._account_id + '/avatar?s=' + this.imageSize; + return this.getBaseUrl() + '/accounts/' + + account._account_id + '/avatar?s=' + this.imageSize; }, }); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html index fd05d62..4a51142 100644 --- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html +++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -20,7 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-avatar.html"> <script>void(0);</script> @@ -32,20 +32,20 @@ </test-fixture> <script> - suite('gr-avatar tests', function() { - var element; + suite('gr-avatar tests', () => { + let element; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, + getConfig() { return Promise.resolve({}); }, }); element = fixture('basic'); }); - test('methods', function() { + test('methods', () => { assert.equal(element._buildAvatarURL( { - _account_id: 123 + _account_id: 123, }), '/accounts/123/avatar?s=16'); assert.equal(element._buildAvatarURL( @@ -54,15 +54,15 @@ avatars: [ { url: 'https://cdn.example.com/s12-p/photo.jpg', - height: 12 + height: 12, }, { url: 'https://cdn.example.com/s16-p/photo.jpg', - height: 16 + height: 16, }, { url: 'https://cdn.example.com/s100-p/photo.jpg', - height: 100 + height: 100, }, ], }), @@ -73,32 +73,31 @@ avatars: [ { url: 'https://cdn.example.com/s95-p/photo.jpg', - height: 95 + height: 95, }, ], }), '/accounts/123/avatar?s=16'); }); - test('dom for existing account', function() { + test('dom for existing account', () => { assert.isTrue(element.hasAttribute('hidden'), 'element not hidden initially'); element.hidden = false; element.imageSize = 64; element.account = { - _account_id: 123 + _account_id: 123, }; assert.isFalse(element.hasAttribute('hidden'), 'element hidden'); - assert.isTrue(element.style.backgroundImage.indexOf( - '/accounts/123/avatar?s=64') > -1); + assert.isTrue( + element.style.backgroundImage.includes('/accounts/123/avatar?s=64')); }); - test('dom for non available account', function() { + test('dom for non available account', () => { assert.isTrue(element.hasAttribute('hidden'), 'element not hidden initially'); element.account = undefined; assert.isTrue(element.hasAttribute('hidden'), 'element not hidden'); }); - }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html index 2ec32d0..1fecbe7 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -17,10 +17,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-button"> <template strip-whitespace> - <style> + <style include="shared-styles"> :host { background-color: #f5f5f5; border: 1px solid #d1d2d3;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js index 800b1df..ddb2bc3 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -34,8 +34,8 @@ }, listeners: { - 'tap': '_handleAction', - 'click': '_handleAction', + tap: '_handleAction', + click: '_handleAction', }, behaviors: [ @@ -52,21 +52,21 @@ 'space enter': '_handleCommitKey', }, - _handleAction: function(e) { + _handleAction(e) { if (this.disabled) { e.preventDefault(); e.stopImmediatePropagation(); } }, - _disabledChanged: function(disabled) { + _disabledChanged(disabled) { if (disabled) { this._enabledTabindex = this.getAttribute('tabindex'); } this.setAttribute('tabindex', disabled ? '-1' : this._enabledTabindex); }, - _handleCommitKey: function(e) { + _handleCommitKey(e) { e.preventDefault(); this.click(); },
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html index c269cb5..de8b5b1 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-button.html"> <script>void(0);</script> @@ -33,63 +32,65 @@ </test-fixture> <script> - suite('gr-select tests', function() { - var element; - var sandbox; + suite('gr-select tests', () => { + let element; + let sandbox; - var addSpyOn = function(eventName) { - var spy = sandbox.spy(); + const addSpyOn = function(eventName) { + const spy = sandbox.spy(); element.addEventListener(eventName, spy); return spy; }; - setup(function() { + setup(() => { element = fixture('basic'); sandbox = sinon.sandbox.create(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - ['tap', 'click'].forEach(function(eventName) { - test('dispatches ' + eventName + ' event', function() { - var spy = addSpyOn(eventName); + for (const eventName of ['tap', 'click']) { + test('dispatches ' + eventName + ' event', () => { + const spy = addSpyOn(eventName); MockInteractions.tap(element); assert.isTrue(spy.calledOnce); }); - }); + } // Keycodes: 32 for Space, 13 for Enter. - [32, 13].forEach(function(key) { - test('dispatches tap event on keycode ' + key, function() { - var tapSpy = sandbox.spy(); + for (const key of [32, 13]) { + test('dispatches tap event on keycode ' + key, () => { + const tapSpy = sandbox.spy(); element.addEventListener('tap', tapSpy); MockInteractions.pressAndReleaseKeyOn(element, key); assert.isTrue(tapSpy.calledOnce); - })}); + }); + } - suite('disabled', function() { - setup(function() { + suite('disabled', () => { + setup(() => { element.disabled = true; }); - ['tap', 'click'].forEach(function(eventName) { - test('stops ' + eventName + ' event', function() { - var spy = addSpyOn(eventName); + for (const eventName of ['tap', 'click']) { + test('stops ' + eventName + ' event', () => { + const spy = addSpyOn(eventName); MockInteractions.tap(element); assert.isFalse(spy.called); }); - }); + } // Keycodes: 32 for Space, 13 for Enter. - [32, 13].forEach(function(key) { - test('stops tap event on keycode ' + key, function() { - var tapSpy = sandbox.spy(); + for (const key of [32, 13]) { + test('stops tap event on keycode ' + key, () => { + const tapSpy = sandbox.spy(); element.addEventListener('tap', tapSpy); MockInteractions.pressAndReleaseKeyOn(element, key); assert.isFalse(tapSpy.called); - })}); + }); + } }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html index 03be4bb..08bc6eb 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.html
@@ -16,10 +16,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-change-star"> <template> - <style> + <style include="shared-styles"> :host { display: inline-block; overflow: hidden;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js index beb0ff1..0e5ff04 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -26,16 +26,16 @@ _xhrPromise: Object, // Used for testing. }, - _computeStarClass: function(starred) { - var classes = ['starButton']; + _computeStarClass(starred) { + const classes = ['starButton']; if (starred) { classes.push('starButton-active'); } return classes.join(' '); }, - toggleStar: function() { - var newVal = !this.change.starred; + toggleStar() { + const newVal = !this.change.starred; this.set('change.starred', newVal); this._xhrPromise = this.$.restAPI.saveChangeStarred(this.change._number, newVal);
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html index 460d860..6c15a46 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-change-star.html"> <script>void(0);</script> @@ -33,12 +32,12 @@ </test-fixture> <script> - suite('gr-change-star tests', function() { - var element; + suite('gr-change-star tests', () => { + let element; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - saveChangeStarred: function() { return Promise.resolve({ok: true}); }, + saveChangeStarred() { return Promise.resolve({ok: true}); }, }); element = fixture('basic'); element.change = { @@ -47,7 +46,7 @@ }; }); - test('star visibility states', function() { + test('star visibility states', () => { element.set('change.starred', true); assert.isTrue(element.$$('button').classList.contains('starButton')); assert.isTrue( @@ -59,21 +58,21 @@ element.$$('button').classList.contains('starButton-active')); }); - test('starring', function(done) { + test('starring', done => { element.set('change.starred', false); MockInteractions.tap(element.$$('button')); - element._xhrPromise.then(function(req) { + element._xhrPromise.then(req => { assert.equal(element.change.starred, true); done(); }); }); - test('unstarring', function(done) { + test('unstarring', done => { element.set('change.starred', true); MockInteractions.tap(element.$$('button')); - element._xhrPromise.then(function(req) { + element._xhrPromise.then(req => { assert.equal(element.change.starred, false); done(); });
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html index bec75ee..d4a98e4 100644 --- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html +++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.html
@@ -16,10 +16,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../gr-button/gr-button.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-confirm-dialog"> <template> - <style> + <style include="shared-styles"> :host { display: block; max-height: 90vh;
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js index dbddb04..3d5e781 100644 --- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js +++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog.js
@@ -44,12 +44,12 @@ role: 'dialog', }, - _handleConfirmTap: function(e) { + _handleConfirmTap(e) { e.preventDefault(); this.fire('confirm', null, {bubbles: false}); }, - _handleCancelTap: function(e) { + _handleCancelTap(e) { e.preventDefault(); this.fire('cancel', null, {bubbles: false}); },
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html index 3eec979..309e15c 100644 --- a/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html +++ b/polygerrit-ui/app/elements/shared/gr-confirm-dialog/gr-confirm-dialog_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-confirm-dialog.html"> <script>void(0);</script> @@ -33,15 +32,15 @@ </test-fixture> <script> - suite('gr-confirm-dialog tests', function() { - var element; + suite('gr-confirm-dialog tests', () => { + let element; - setup(function() { + setup(() => { element = fixture('basic'); }); - test('events', function(done) { - var numEvents = 0; + test('events', done => { + let numEvents = 0; function handler() { if (++numEvents == 2) { done(); } } element.addEventListener('confirm', handler); @@ -50,6 +49,5 @@ MockInteractions.tap(element.$$('gr-button[primary]')); MockInteractions.tap(element.$$('gr-button:not([primary])')); }); - }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html new file mode 100644 index 0000000..64e5439 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.html
@@ -0,0 +1,64 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-copy-clipboard"> + <template> + <style include="shared-styles"> + .text { + display: flex; + flex-wrap: wrap; + margin-bottom: .5em; + width: 60em; + } + .text label { + flex: 0 0 100%; + } + .copyText { + flex-grow: 1; + margin-right: .3em; + } + .hideInput { + display: none; + } + input { + font-family: var(--monospace-font-family); + font-size: inherit; + } + </style> + <div class="text"> + <label>[[title]]</label> + <input id="input" is="iron-input" + class$="copyText [[_computeInputClass(hideInput)]]" + type="text" + bind-value="[[text]]" + on-tap="_handleInputTap" + readonly> + <gr-button id="button" + class="copyToClipboard" + on-tap="_copyToClipboard"> + copy + </gr-button> + </div> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-copy-clipboard.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js new file mode 100644 index 0000000..a371374 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
@@ -0,0 +1,52 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + const COPY_TIMEOUT_MS = 1000; + + Polymer({ + is: 'gr-copy-clipboard', + + properties: { + text: String, + title: String, + hideInput: { + type: Boolean, + value: false, + }, + }, + + focusOnCopy() { + this.$.button.focus(); + }, + + _computeInputClass(hideInput) { + return hideInput ? 'hideInput' : ''; + }, + + _handleInputTap(e) { + e.preventDefault(); + Polymer.dom(e).rootTarget.select(); + }, + + _copyToClipboard(e) { + this.$.input.select(); + document.execCommand('copy'); + window.getSelection().removeAllRanges(); + e.target.textContent = 'done'; + this.async(() => { e.target.textContent = 'copy'; }, COPY_TIMEOUT_MS); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html new file mode 100644 index 0000000..7310629 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
@@ -0,0 +1,82 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-copy-clipboard</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-copy-clipboard.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-copy-clipboard></gr-copy-clipboard> + </template> +</test-fixture> + +<script> + suite('gr-copy-clipboard tests', () => { + let element; + let sandbox; + + setup(() => { + stub('gr-rest-api-interface', { + saveChangeStarred() { return Promise.resolve({ok: true}); }, + }); + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.title = 'Checkout'; + element.text = `git fetch http://gerrit@localhost:8080/a/test-project + refs/changes/05/5/1 && git checkout FETCH_HEAD`; + flushAsynchronousOperations(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('copy to clipboard', () => { + const clipboardSpy = sandbox.spy(element, '_copyToClipboard'); + const copyBtn = element.$$('.copyToClipboard'); + MockInteractions.tap(copyBtn); + assert.isTrue(clipboardSpy.called); + }); + + test('focusOnCopy', () => { + element.focusOnCopy(); + assert.deepEqual(Polymer.dom(element.root).activeElement, + element.$$('.copyToClipboard')); + }); + + test('_handleInputTap', () => { + const inputElement = element.$$('input'); + MockInteractions.tap(inputElement); + assert.equal(inputElement.selectionStart, 0); + assert.equal(inputElement.selectionEnd, element.text.length - 1); + }); + + test('hideInput', () => { + assert.notEqual(getComputedStyle(element.$.input).display, 'none'); + element.hideInput = true; + flushAsynchronousOperations(); + assert.equal(getComputedStyle(element.$.input).display, 'none'); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js index 9bfdcfb..6f03a3e 100644 --- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js +++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -14,7 +14,7 @@ (function() { 'use strict'; - var ScrollBehavior = { + const ScrollBehavior = { NEVER: 'never', KEEP_VISIBLE: 'keep-visible', }; @@ -25,7 +25,7 @@ properties: { stops: { type: Array, - value: function() { + value() { return []; }, observer: '_updateIndex', @@ -76,15 +76,15 @@ }, }, - detached: function() { + detached() { this.unsetCursor(); }, - next: function(opt_condition, opt_getTargetHeight) { + next(opt_condition, opt_getTargetHeight) { this._moveCursor(1, opt_condition, opt_getTargetHeight); }, - previous: function(opt_condition) { + previous(opt_condition) { this._moveCursor(-1, opt_condition); }, @@ -94,8 +94,8 @@ * @param {boolean} opt_noScroll prevent any potential scrolling in response * setting the cursor. */ - setCursor: function(element, opt_noScroll) { - var behavior; + setCursor(element, opt_noScroll) { + let behavior; if (opt_noScroll) { behavior = this.scrollBehavior; this.scrollBehavior = ScrollBehavior.NEVER; @@ -109,28 +109,28 @@ if (opt_noScroll) { this.scrollBehavior = behavior; } }, - unsetCursor: function() { + unsetCursor() { this._unDecorateTarget(); this.index = -1; this.target = null; this._targetHeight = null; }, - isAtStart: function() { + isAtStart() { return this.index === 0; }, - isAtEnd: function() { + isAtEnd() { return this.index === this.stops.length - 1; }, - moveToStart: function() { + moveToStart() { if (this.stops.length) { this.setCursor(this.stops[0]); } }, - setCursorAtIndex: function(index, opt_noScroll) { + setCursorAtIndex(index, opt_noScroll) { this.setCursor(this.stops[index], opt_noScroll); }, @@ -146,7 +146,7 @@ * sometimes different, used by the diff cursor. * @private */ - _moveCursor: function(delta, opt_condition, opt_getTargetHeight) { + _moveCursor(delta, opt_condition, opt_getTargetHeight) { if (!this.stops.length) { this.unsetCursor(); return; @@ -154,9 +154,9 @@ this._unDecorateTarget(); - var newIndex = this._getNextindex(delta, opt_condition); + const newIndex = this._getNextindex(delta, opt_condition); - var newTarget = null; + let newTarget = null; if (newIndex != -1) { newTarget = this.stops[newIndex]; } @@ -175,13 +175,13 @@ this._decorateTarget(); }, - _decorateTarget: function() { + _decorateTarget() { if (this.target && this.cursorTargetClass) { this.target.classList.add(this.cursorTargetClass); } }, - _unDecorateTarget: function() { + _unDecorateTarget() { if (this.target && this.cursorTargetClass) { this.target.classList.remove(this.cursorTargetClass); } @@ -194,12 +194,12 @@ * @return {Number} the new index. * @private */ - _getNextindex: function(delta, opt_condition) { + _getNextindex(delta, opt_condition) { if (!this.stops.length || this.index === -1) { return -1; } - var newIndex = this.index; + let newIndex = this.index; do { newIndex = newIndex + delta; } while (newIndex > 0 && @@ -221,13 +221,13 @@ return newIndex; }, - _updateIndex: function() { + _updateIndex() { if (!this.target) { this.index = -1; return; } - var newIndex = Array.prototype.indexOf.call(this.stops, this.target); + const newIndex = Array.prototype.indexOf.call(this.stops, this.target); if (newIndex === -1) { this.unsetCursor(); } else { @@ -240,9 +240,9 @@ * @param {object} target Target to scroll to. * @return {number} Distance to top of the target. */ - _getTop: function(target) { - var top = target.offsetTop; - for (var offsetParent = target.offsetParent; + _getTop(target) { + let top = target.offsetTop; + for (let offsetParent = target.offsetParent; offsetParent; offsetParent = offsetParent.offsetParent) { top += offsetParent.offsetTop; @@ -253,25 +253,25 @@ /** * @return {boolean} */ - _targetIsVisible: function(top) { + _targetIsVisible(top) { return this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE && top > window.pageYOffset && top < window.pageYOffset + window.innerHeight; }, - _calculateScrollToValue: function(top, target) { + _calculateScrollToValue(top, target) { return top - (window.innerHeight / 3) + (target.offsetHeight / 2); }, - _scrollToTarget: function() { + _scrollToTarget() { if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) { return; } - var top = this._getTop(this.target); - var bottomIsVisible = this._targetHeight ? + const top = this._getTop(this.target); + const bottomIsVisible = this._targetHeight ? this._targetIsVisible(top + this._targetHeight) : true; - var scrollToValue = this._calculateScrollToValue(top, this.target); + const scrollToValue = this._calculateScrollToValue(top, this.target); if (this._targetIsVisible(top)) { // Don't scroll if either the bottom is visible or if the position that
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html index 5d9af80..a8102b5 100644 --- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html +++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-cursor-manager.html"> <script>void(0);</script> @@ -39,23 +38,23 @@ </test-fixture> <script> - suite('gr-cursor-manager tests', function() { - var sandbox; - var element; - var list; + suite('gr-cursor-manager tests', () => { + let sandbox; + let element; + let list; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); - var fixtureElements = fixture('basic'); + const fixtureElements = fixture('basic'); element = fixtureElements[0]; list = fixtureElements[1]; }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('core cursor functionality', function() { + test('core cursor functionality', () => { // The element is initialized into the proper state. assert.isArray(element.stops); assert.equal(element.stops.length, 0); @@ -111,7 +110,7 @@ assert.isTrue(element.isAtStart()); assert.isTrue(list.children[0].classList.contains('targeted')); - var newLi = document.createElement('li'); + const newLi = document.createElement('li'); newLi.textContent = 'Z'; list.insertBefore(newLi, list.children[0]); element.stops = list.querySelectorAll('li'); @@ -128,12 +127,12 @@ }); - test('_moveCursor', function() { + test('_moveCursor', () => { // Initialize the cursor with its stops. element.stops = list.querySelectorAll('li'); // Select the first stop. element.setCursor(list.children[0]); - var getTargetHeight = sinon.stub(); + const getTargetHeight = sinon.stub(); // Move the cursor without an optional get target height function. element._moveCursor(1); @@ -144,9 +143,9 @@ assert.isTrue(getTargetHeight.called); }); - test('opt_noScroll', function() { - sandbox.stub(element, '_targetIsVisible', function() { return false; }); - var scrollStub = sandbox.stub(window, 'scrollTo'); + test('opt_noScroll', () => { + sandbox.stub(element, '_targetIsVisible', () => false); + const scrollStub = sandbox.stub(window, 'scrollTo'); element.stops = list.querySelectorAll('li'); element.scrollBehavior = 'keep-visible'; @@ -157,8 +156,8 @@ assert.isTrue(scrollStub.called); }); - test('_getNextindex', function() { - var isLetterB = function(row) { + test('_getNextindex', () => { + const isLetterB = function(row) { return row.textContent === 'B'; }; element.stops = list.querySelectorAll('li'); @@ -185,9 +184,9 @@ assert.equal(element._getNextindex(-1, isLetterB), 0); }); - test('focusOnMove prop', function() { - var listEls = list.querySelectorAll('li'); - for (var i = 0; i < listEls.length; i++) { + test('focusOnMove prop', () => { + const listEls = list.querySelectorAll('li'); + for (let i = 0; i < listEls.length; i++) { sandbox.spy(listEls[i], 'focus'); } element.stops = listEls; @@ -202,9 +201,9 @@ assert.isTrue(element.target.focus.called); }); - suite('_scrollToTarget', function() { - var scrollStub; - setup(function() { + suite('_scrollToTarget', () => { + let scrollStub; + setup(() => { element.stops = list.querySelectorAll('li'); element.scrollBehavior = 'keep-visible'; @@ -215,29 +214,27 @@ window.innerHeight = 60; }); - test('Called when top and bottom not visible', function() { - sandbox.stub(element, '_targetIsVisible', function() { + test('Called when top and bottom not visible', () => { + sandbox.stub(element, '_targetIsVisible', () => { return false; }); element._scrollToTarget(); assert.isTrue(scrollStub.called); }); - test('Not called when top and bottom visible', function() { - sandbox.stub(element, '_targetIsVisible', function() { + test('Not called when top and bottom visible', () => { + sandbox.stub(element, '_targetIsVisible', () => { return true; }); element._scrollToTarget(); assert.isFalse(scrollStub.called); }); - test('Called when top is visible, bottom is not, and scroll is lower', - function() { - var visibleStub = sandbox.stub(element, '_targetIsVisible', function() { - return visibleStub.callCount == 2; - }); + test('Called when top is visible, bottom is not, scroll is lower', () => { + const visibleStub = sandbox.stub(element, '_targetIsVisible', + () => visibleStub.callCount === 2); window.scrollY = 15; - sandbox.stub(element, '_calculateScrollToValue', function() { + sandbox.stub(element, '_calculateScrollToValue', () => { return 20; }); element._scrollToTarget(); @@ -245,13 +242,11 @@ assert.equal(visibleStub.callCount, 2); }); - test('Called when top is visible, bottom is not, and scroll is higher', - function() { - var visibleStub = sandbox.stub(element, '_targetIsVisible', function() { - return visibleStub.callCount == 2; - }); + test('Called when top is visible, bottom not, scroll is higher', () => { + const visibleStub = sandbox.stub(element, '_targetIsVisible', + () => visibleStub.callCount === 2); window.scrollY = 25; - sandbox.stub(element, '_calculateScrollToValue', function() { + sandbox.stub(element, '_calculateScrollToValue', () => { return 20; }); element._scrollToTarget();
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html index 4d31241..ab91fc5 100644 --- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html +++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.html
@@ -17,12 +17,13 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <script src="../../../bower_components/moment/moment.js"></script> <dom-module id="gr-date-formatter"> <template> - <style> + <style include="shared-styles"> :host { display: inline; }
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js index 6f4cd3d..65a4c68 100644 --- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js +++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -14,12 +14,12 @@ (function() { 'use strict'; - var Duration = { + const Duration = { HOUR: 1000 * 60 * 60, DAY: 1000 * 60 * 60 * 24, }; - var TimeFormats = { + const TimeFormats = { TIME_12: 'h:mm A', // 2:14 PM TIME_12_WITH_SEC: 'h:mm:ss A', // 2:14:00 PM TIME_24: 'HH:mm', // 14:14 @@ -61,16 +61,16 @@ Gerrit.TooltipBehavior, ], - attached: function() { + attached() { this._loadPreferences(); }, - _getUtcOffsetString: function() { + _getUtcOffsetString() { return ' UTC' + moment().format('Z'); }, - _loadPreferences: function() { - return this._getLoggedIn().then(function(loggedIn) { + _loadPreferences() { + return this._getLoggedIn().then(loggedIn => { if (!loggedIn) { this._timeFormat = TimeFormats.TIME_24; this._relative = false; @@ -80,12 +80,12 @@ this._loadTimeFormat(), this._loadRelative(), ]); - }.bind(this)); + }); }, - _loadTimeFormat: function() { - return this._getPreferences().then(function(preferences) { - var timeFormat = preferences && preferences.time_format; + _loadTimeFormat() { + return this._getPreferences().then(preferences => { + const timeFormat = preferences && preferences.time_format; switch (timeFormat) { case 'HHMM_12': this._timeFormat = TimeFormats.TIME_12; @@ -96,55 +96,55 @@ default: throw Error('Invalid time format: ' + timeFormat); } - }.bind(this)); + }); }, - _loadRelative: function() { - return this._getPreferences().then(function(prefs) { + _loadRelative() { + return this._getPreferences().then(prefs => { // prefs.relative_date_in_change_table is not set when false. this._relative = !!(prefs && prefs.relative_date_in_change_table); - }.bind(this)); + }); }, - _getLoggedIn: function() { + _getLoggedIn() { return this.$.restAPI.getLoggedIn(); }, - _getPreferences: function() { + _getPreferences() { return this.$.restAPI.getPreferences(); }, /** * Return true if date is within 24 hours and on the same day. */ - _isWithinDay: function(now, date) { - var diff = -date.diff(now); + _isWithinDay(now, date) { + const diff = -date.diff(now); return diff < Duration.DAY && date.day() === now.getDay(); }, /** * Returns true if date is from one to six months. */ - _isWithinHalfYear: function(now, date) { - var diff = -date.diff(now); + _isWithinHalfYear(now, date) { + const diff = -date.diff(now); return (date.day() !== now.getDay() || diff >= Duration.DAY) && diff < 180 * Duration.DAY; }, - _computeDateStr: function(dateStr, timeFormat, relative) { + _computeDateStr(dateStr, timeFormat, relative) { if (!dateStr) { return ''; } - var date = moment(util.parseDate(dateStr)); + const date = moment(util.parseDate(dateStr)); if (!date.isValid()) { return ''; } if (relative) { - var dateFromNow = date.fromNow(); + const dateFromNow = date.fromNow(); if (dateFromNow === 'a few seconds ago') { return 'just now'; } else { return dateFromNow; } } - var now = new Date(); - var format = TimeFormats.MONTH_DAY_YEAR; + const now = new Date(); + let format = TimeFormats.MONTH_DAY_YEAR; if (this._isWithinDay(now, date)) { format = timeFormat; } else if (this._isWithinHalfYear(now, date)) { @@ -153,17 +153,17 @@ return date.format(format); }, - _timeToSecondsFormat: function(timeFormat) { + _timeToSecondsFormat(timeFormat) { return timeFormat === TimeFormats.TIME_12 ? TimeFormats.TIME_12_WITH_SEC : TimeFormats.TIME_24_WITH_SEC; }, - _computeFullDateStr: function(dateStr, timeFormat) { + _computeFullDateStr(dateStr, timeFormat) { if (!dateStr) { return ''; } - var date = moment(util.parseDate(dateStr)); + const date = moment(util.parseDate(dateStr)); if (!date.isValid()) { return ''; } - var format = TimeFormats.MONTH_DAY_YEAR + ', '; + let format = TimeFormats.MONTH_DAY_YEAR + ', '; format += this._timeToSecondsFormat(timeFormat); return date.format(format) + this._getUtcOffsetString(); },
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html index 4c6dfdf..2c15ef6 100644 --- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html +++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -20,6 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> <link rel="import" href="gr-date-formatter.html"> @@ -33,15 +34,15 @@ </test-fixture> <script> - suite('gr-date-formatter tests', function() { - var element; - var sandbox; + suite('gr-date-formatter tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); @@ -49,7 +50,7 @@ * Parse server-formatter date and normalize into current timezone. */ function normalizedDate(dateStr) { - var d = util.parseDate(dateStr); + const d = util.parseDate(dateStr); d.setMinutes(d.getMinutes() + d.getTimezoneOffset()); return d; } @@ -60,8 +61,8 @@ .toJSON().replace('T', ' ').slice(0, -1); sandbox.useFakeTimers(normalizedDate(nowStr).getTime()); element.dateStr = dateStr; - flush(function() { - var span = element.$$('span'); + flush(() => { + const span = element.$$('span'); assert.equal(span.textContent.trim(), expected); assert.equal(element.title, expectedTooltip); done(); @@ -69,8 +70,8 @@ } function stubRestAPI(preferences) { - var loggedInPromise = Promise.resolve(preferences !== null); - var preferencesPromise = Promise.resolve(preferences); + const loggedInPromise = Promise.resolve(preferences !== null); + const preferencesPromise = Promise.resolve(preferences); stub('gr-rest-api-interface', { getLoggedIn: sinon.stub().returns(loggedInPromise), getPreferences: sinon.stub().returns(preferencesPromise), @@ -78,115 +79,115 @@ return Promise.all([loggedInPromise, preferencesPromise]); } - suite('24 hours time format preference', function() { - setup(function() { + suite('24 hours time format preference', () => { + setup(() => { return stubRestAPI( {time_format: 'HHMM_24', relative_date_in_change_table: false} - ).then(function() { + ).then(() => { element = fixture('basic'); sandbox.stub(element, '_getUtcOffsetString').returns(''); return element._loadPreferences(); }); }); - test('invalid dates are quietly rejected', function() { + test('invalid dates are quietly rejected', () => { assert.notOk((new Date('foo')).valueOf()); assert.equal(element._computeDateStr('foo', 'h:mm A'), ''); }); - test('Within 24 hours on same day', function(done) { + test('Within 24 hours on same day', done => { testDates('2015-07-29 20:34:14.985000000', '2015-07-29 15:34:14.985000000', '15:34', 'Jul 29, 2015, 15:34:14', done); }); - test('Within 24 hours on different days', function(done) { + test('Within 24 hours on different days', done => { testDates('2015-07-29 03:34:14.985000000', '2015-07-28 20:25:14.985000000', 'Jul 28', 'Jul 28, 2015, 20:25:14', done); }); - test('More than 24 hours but less than six months', function(done) { + test('More than 24 hours but less than six months', done => { testDates('2015-07-29 20:34:14.985000000', '2015-06-15 03:25:14.985000000', 'Jun 15', 'Jun 15, 2015, 03:25:14', done); }); - test('More than six months', function(done) { + test('More than six months', done => { testDates('2015-09-15 20:34:00.000000000', '2015-01-15 03:25:00.000000000', 'Jan 15, 2015', 'Jan 15, 2015, 03:25:00', done); }); }); - suite('12 hours time format preference', function() { - setup(function() { + suite('12 hours time format preference', () => { + setup(() => { // relative_date_in_change_table is not set when false. return stubRestAPI( {time_format: 'HHMM_12'} - ).then(function() { + ).then(() => { element = fixture('basic'); sandbox.stub(element, '_getUtcOffsetString').returns(''); return element._loadPreferences(); }); }); - test('Within 24 hours on same day', function(done) { + test('Within 24 hours on same day', done => { testDates('2015-07-29 20:34:14.985000000', '2015-07-29 15:34:14.985000000', '3:34 PM', 'Jul 29, 2015, 3:34:14 PM', done); }); }); - suite('relative date preference', function() { - setup(function() { + suite('relative date preference', () => { + setup(() => { return stubRestAPI( {time_format: 'HHMM_12', relative_date_in_change_table: true} - ).then(function() { + ).then(() => { element = fixture('basic'); sandbox.stub(element, '_getUtcOffsetString').returns(''); return element._loadPreferences(); }); }); - test('Within 24 hours on same day', function(done) { + test('Within 24 hours on same day', done => { testDates('2015-07-29 20:34:14.985000000', '2015-07-29 15:34:14.985000000', '5 hours ago', 'Jul 29, 2015, 3:34:14 PM', done); }); - test('More than six months', function(done) { + test('More than six months', done => { testDates('2015-09-15 20:34:00.000000000', '2015-01-15 03:25:00.000000000', '8 months ago', 'Jan 15, 2015, 3:25:00 AM', done); }); }); - suite('logged in', function() { - setup(function() { + suite('logged in', () => { + setup(() => { return stubRestAPI( {time_format: 'HHMM_12', relative_date_in_change_table: true} - ).then(function() { + ).then(() => { element = fixture('basic'); return element._loadPreferences(); }); }); - test('Preferences are respected', function() { + test('Preferences are respected', () => { assert.equal(element._timeFormat, 'h:mm A'); assert.isTrue(element._relative); }); }); - suite('logged out', function() { - setup(function() { - return stubRestAPI(null).then(function() { + suite('logged out', () => { + setup(() => { + return stubRestAPI(null).then(() => { element = fixture('basic'); return element._loadPreferences(); }); }); - test('Default preferences are respected', function() { + test('Default preferences are respected', () => { assert.equal(element._timeFormat, 'HH:mm'); assert.isFalse(element._relative); });
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html new file mode 100644 index 0000000..05075bd --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.html
@@ -0,0 +1,83 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-download-commands"> + <template> + <style include="shared-styles"> + ul { + list-style: none; + margin-bottom: .5em; + } + li { + display: inline-block; + margin: 0; + padding: 0; + } + li gr-button { + margin-right: 1em; + } + label, + input { + display: block; + } + label { + font-weight: bold; + } + li[selected] gr-button { + color: #000; + font-weight: bold; + text-decoration: none; + } + .schemes { + display: flex; + justify-content: space-between; + } + .commands { + border-bottom: 1px solid #ddd; + border-top: 1px solid #ddd; + padding: .5em; + } + </style> + <div class="schemes"> + <ul hidden$="[[!schemes.length]]" hidden> + <template is="dom-repeat" items="[[schemes]]" as="scheme"> + <li selected$="[[_computeSelected(scheme, selectedScheme)]]"> + <gr-button link data-scheme$="[[scheme]]" on-tap="_handleSchemeTap"> + [[scheme]] + </gr-button> + </li> + </template> + </ul> + </div> + <div class="commands" hidden$="[[!schemes.length]]" hidden> + <template is="dom-repeat" + items="[[commands]]" + as="command"> + <gr-copy-clipboard + title=[[command.title]] + text=[[command.command]]></gr-copy-clipboard> + </template> + </div> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + </template> + <script src="gr-download-commands.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js new file mode 100644 index 0000000..7c114c6 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
@@ -0,0 +1,73 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-download-commands', + properties: { + commands: Array, + _loggedIn: { + type: Boolean, + value: false, + observer: '_loggedInChanged', + }, + selectedScheme: { + type: String, + notify: true, + }, + }, + + behaviors: [ + Gerrit.RESTClientBehavior, + ], + + attached() { + this._getLoggedIn().then(loggedIn => { + this._loggedIn = loggedIn; + }); + }, + + focusOnCopy() { + this.$$('gr-copy-clipboard').focusOnCopy(); + }, + + _getLoggedIn() { + return this.$.restAPI.getLoggedIn(); + }, + + _loggedInChanged(loggedIn) { + if (!loggedIn) { return; } + return this.$.restAPI.getPreferences().then(prefs => { + if (prefs.download_scheme) { + // Note (issue 5180): normalize the download scheme with lower-case. + this.selectedScheme = prefs.download_scheme.toLowerCase(); + } + }); + }, + + _computeSelected(item, selectedItem) { + return item === selectedItem; + }, + + _handleSchemeTap(e) { + e.preventDefault(); + const el = Polymer.dom(e).rootTarget; + this.selectedScheme = el.getAttribute('data-scheme'); + if (this._loggedIn) { + this.$.restAPI.savePreferences({download_scheme: this.selectedScheme}); + } + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html new file mode 100644 index 0000000..41ef8f3 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
@@ -0,0 +1,153 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-download-commands</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-download-commands.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-download-commands></gr-download-commands> + </template> +</test-fixture> + +<script> + suite('gr-download-commands', () => { + let element; + let sandbox; + const SCHEMES = ['http', 'repo', 'ssh']; + const COMMANDS = [{ + title: 'Checkout', + command: `git fetch http://andybons@localhost:8080/a/test-project + refs/changes/05/5/1 && git checkout FETCH_HEAD`, + }, { + title: 'Cherry Pick', + command: `git fetch http://andybons@localhost:8080/a/test-project + refs/changes/05/5/1 && git cherry-pick FETCH_HEAD`, + }, { + title: 'Format Patch', + command: `git fetch http://andybons@localhost:8080/a/test-project + refs/changes/05/5/1 && git format-patch -1 --stdout FETCH_HEAD`, + }, { + title: 'Pull', + command: `git pull http://andybons@localhost:8080/a/test-project + refs/changes/05/5/1`, + }]; + const SELECTED_SCHEME = 'http'; + + setup(() => { + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('unauthenticated', () => { + setup(() => { + element = fixture('basic'); + element.schemes = SCHEMES; + element.commands = COMMANDS; + element.selectedScheme = SELECTED_SCHEME; + flushAsynchronousOperations(); + }); + + test('focusOnCopy', () => { + const focusStub = sandbox.stub(element.$$('gr-copy-clipboard'), + 'focusOnCopy'); + element.focusOnCopy(); + assert.isTrue(focusStub.called); + }); + + test('element visibility', () => { + assert.isFalse(element.$$('ul').hasAttribute('hidden')); + assert.isFalse(element.$$('.commands').hasAttribute('hidden')); + + element.schemes = []; + assert.isTrue(element.$$('ul').hasAttribute('hidden')); + assert.isTrue(element.$$('.commands').hasAttribute('hidden')); + }); + + test('tab selection', () => { + flushAsynchronousOperations(); + let el = element.$$('[data-scheme="http"]').parentElement; + assert.isTrue(el.hasAttribute('selected')); + for (const scheme of ['repo', 'ssh']) { + const el = element.$$('[data-scheme="' + scheme + '"]').parentElement; + assert.isFalse(el.hasAttribute('selected')); + } + + MockInteractions.tap(element.$$('[data-scheme="ssh"]')); + el = element.$$('[data-scheme="ssh"]').parentElement; + assert.isTrue(el.hasAttribute('selected')); + for (const scheme of ['http', 'repo']) { + const el = element.$$('[data-scheme="' + scheme + '"]').parentElement; + assert.isFalse(el.hasAttribute('selected')); + } + }); + + test('loads scheme from preferences', done => { + stub('gr-rest-api-interface', { + getPreferences() { + return Promise.resolve({download_scheme: 'repo'}); + }, + }); + element._loggedIn = true; + assert.isTrue(element.$.restAPI.getPreferences.called); + element.$.restAPI.getPreferences.lastCall.returnValue.then(() => { + assert.equal(element.selectedScheme, 'repo'); + done(); + }); + }); + + test('normalize scheme from preferences', done => { + stub('gr-rest-api-interface', { + getPreferences() { + return Promise.resolve({download_scheme: 'REPO'}); + }, + }); + element._loggedIn = true; + element.$.restAPI.getPreferences.lastCall.returnValue.then(() => { + assert.equal(element.selectedScheme, 'repo'); + done(); + }); + }); + + test('saves scheme to preferences', () => { + element._loggedIn = true; + const savePrefsStub = sinon.stub(element.$.restAPI, 'savePreferences', + () => { return Promise.resolve(); }); + + flushAsynchronousOperations(); + + const firstSchemeButton = element.$$('li gr-button[data-scheme]'); + + MockInteractions.tap(firstSchemeButton); + + assert.isTrue(savePrefsStub.called); + assert.equal(savePrefsStub.lastCall.args[0].download_scheme, + firstSchemeButton.getAttribute('data-scheme')); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html index e89bf05..687b035 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -15,14 +15,17 @@ --> <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html"> <link rel="import" href="../../shared/gr-button/gr-button.html"> +<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-dropdown"> <template> - <style> + <style include="shared-styles"> :host { display: inline-block; } @@ -40,7 +43,7 @@ font: inherit; padding: .3em 0; } - :host[down-arrow] .dropdown-trigger { + :host([down-arrow]) .dropdown-trigger { padding-right: 1.4em; } gr-avatar { @@ -49,7 +52,10 @@ vertical-align: middle; } gr-button[link] { - padding: 1em 0; + padding: 0.5em; + } + gr-button[link]:focus { + outline: 5px auto -webkit-focus-ring-color; } ul { list-style: none; @@ -76,6 +82,11 @@ background-color: #6B82D6; color: #fff; } + li:focus, + li.selected { + background-color: #EBF5FB; + outline: none; + } .topContent { display: block; padding: .85em 1em; @@ -84,7 +95,7 @@ font-weight: bold; } :host:not([down-arrow]) .downArrow { display: none; } - :host[down-arrow] .downArrow { + :host([down-arrow]) .downArrow { border-left: .36em solid transparent; border-right: .36em solid transparent; border-top: .36em solid #ccc; @@ -119,7 +130,9 @@ items="[[topContent]]" as="item" initial-count="75"> - <div class$="[[_getClassIfBold(item.bold)]] top-item"> + <div + class$="[[_getClassIfBold(item.bold)]] top-item" + tabindex="-1"> [[item.text]] </div> </template> @@ -130,23 +143,31 @@ items="[[items]]" as="link" initial-count="75"> - <li> + <li tabindex="-1"> <span class$="itemAction [[_computeDisabledClass(link.id, disabledIds.*)]]" data-id$="[[link.id]]" on-tap="_handleItemTap" - hidden$="[[link.url]]">[[link.name]]</span> + hidden$="[[link.url]]" + tabindex="-1">[[link.name]]</span> <a class="itemAction" href$="[[_computeLinkURL(link)]]" rel$="[[_computeLinkRel(link)]]" target$="[[link.target]]" - hidden$="[[!link.url]]">[[link.name]]</a> + hidden$="[[!link.url]]" + tabindex="-1">[[link.name]]</a> </li> </template> </ul> </div> </iron-dropdown> + <gr-cursor-manager + id="cursor" + cursor-target-class="selected" + scroll-behavior="never" + focus-on-move + stops="[[_listElements]]"></gr-cursor-manager> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </template> <script src="gr-dropdown.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js index bf587d7..19c948f 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -23,8 +23,17 @@ * @event tap-item-<id> */ + /** + * Fired when a non-link dropdown item is tapped. + * + * @event tap-item + */ + properties: { - items: Array, + items: { + type: Array, + observer: '_resetCursorStops', + }, topContent: Object, horizontalAlign: { type: String, @@ -50,44 +59,113 @@ */ disabledIds: { type: Array, - value: function() { return []; }, + value() { return []; }, }, _hasAvatars: String, + + /** + * The elements of the list. + */ + _listElements: { + type: Array, + value() { return []; }, + }, }, behaviors: [ Gerrit.BaseUrlBehavior, + Gerrit.KeyboardShortcutBehavior, ], - attached: function() { - this.$.restAPI.getConfig().then(function(cfg) { + keyBindings: { + 'down': '_handleDown', + 'enter space': '_handleEnter', + 'tab': '_handleTab', + 'up': '_handleUp', + }, + + attached() { + this.$.restAPI.getConfig().then(cfg => { this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); - }.bind(this)); + }); }, - _handleDropdownTap: function(e) { - this.$.dropdown.close(); + _handleUp(e) { + if (this.$.dropdown.opened) { + e.preventDefault(); + e.stopPropagation(); + this.$.cursor.previous(); + } else { + this._open(); + } }, - _showDropdownTapHandler: function(e) { + _handleDown(e) { + if (this.$.dropdown.opened) { + e.preventDefault(); + e.stopPropagation(); + this.$.cursor.next(); + } else { + this._open(); + } + }, + + _handleTab(e) { + if (this.$.dropdown.opened) { + // Tab in a native select is a no-op. Emulate this. + e.preventDefault(); + e.stopPropagation(); + } + }, + + _handleEnter(e) { + e.preventDefault(); + e.stopPropagation(); + if (this.$.dropdown.opened) { + // TODO(kaspern): This solution will not work in Shadow DOM, and + // is not particularly robust in general. Find a better solution + // when page.js has been abstracted away from components. + const el = this.$.cursor.target.querySelector(':not([hidden])'); + if (el) { el.click(); } + } else { + this._open(); + } + }, + + _handleDropdownTap(e) { + // async is needed so that that the click event is fired before the + // dropdown closes (This was a bug for touch devices). + this.async(() => { + this.$.dropdown.close(); + }, 1); + }, + + _showDropdownTapHandler(e) { + this._open(); + }, + + _open() { this.$.dropdown.open(); + this.$.cursor.setCursorAtIndex(0); + Polymer.dom.flush(); + this.$.cursor.target.focus(); }, - _getClassIfBold: function(bold) { + _getClassIfBold(bold) { return bold ? 'bold-text' : ''; }, - _computeURLHelper: function(host, path) { + _computeURLHelper(host, path) { return '//' + host + this.getBaseUrl() + path; }, - _computeRelativeURL: function(path) { - var host = window.location.host; + _computeRelativeURL(path) { + const host = window.location.host; return this._computeURLHelper(host, path); }, - _computeLinkURL: function(link) { + _computeLinkURL(link) { if (typeof link.url === 'undefined') { return ''; } @@ -97,19 +175,28 @@ return this._computeRelativeURL(link.url); }, - _computeLinkRel: function(link) { + _computeLinkRel(link) { return link.target ? 'noopener' : null; }, - _handleItemTap: function(e) { - var id = e.target.getAttribute('data-id'); - if (id && this.disabledIds.indexOf(id) === -1) { + _handleItemTap(e) { + const id = e.target.getAttribute('data-id'); + const item = this.items.find(item => item.id === id); + if (id && !this.disabledIds.includes(id)) { + if (item) { + this.dispatchEvent(new CustomEvent('tap-item', {detail: item})); + } this.dispatchEvent(new CustomEvent('tap-item-' + id)); } }, - _computeDisabledClass: function(id, disabledIdsRecord) { - return disabledIdsRecord.base.indexOf(id) === -1 ? '' : 'disabled'; + _computeDisabledClass(id, disabledIdsRecord) { + return disabledIdsRecord.base.includes(id) ? 'disabled' : ''; + }, + + _resetCursorStops() { + Polymer.dom.flush(); + this._listElements = Polymer.dom(this.root).querySelectorAll('li'); }, }); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html index 74ad85e..e335870 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-dropdown.html"> <script>void(0);</script> @@ -33,30 +32,37 @@ </test-fixture> <script> - suite('gr-dropdown tests', function() { - var element; + suite('gr-dropdown tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, + getConfig() { return Promise.resolve({}); }, }); element = fixture('basic'); + sandbox = sinon.sandbox.create(); }); - test('tap on trigger opens menu', function() { + teardown(() => { + sandbox.restore(); + }); + + test('tap on trigger opens menu', () => { + sandbox.stub(element, '_open', () => {element.$.dropdown.open();}); assert.isFalse(element.$.dropdown.opened); MockInteractions.tap(element.$.trigger); assert.isTrue(element.$.dropdown.opened); }); - test('_computeURLHelper', function() { - var path = '/test'; - var host = 'http://www.testsite.com'; - var computedPath = element._computeURLHelper(host, path); + test('_computeURLHelper', () => { + const path = '/test'; + const host = 'http://www.testsite.com'; + const computedPath = element._computeURLHelper(host, path); assert.equal(computedPath, '//http://www.testsite.com/test'); }); - test('link URLs', function() { + test('link URLs', () => { assert.equal( element._computeLinkURL({url: '/test'}), '//' + window.location.host + '/test'); @@ -65,49 +71,97 @@ '/test'); }); - test('link rel', function() { + test('link rel', () => { assert.isNull(element._computeLinkRel({url: '/test'})); assert.equal( element._computeLinkRel({url: '/test', target: '_blank'}), 'noopener'); }); - test('_getClassIfBold', function() { - var bold = true; + test('_getClassIfBold', () => { + let bold = true; assert.equal(element._getClassIfBold(bold), 'bold-text'); bold = false; assert.equal(element._getClassIfBold(bold), ''); }); - test('Top text exists and is bolded correctly', function() { + test('Top text exists and is bolded correctly', () => { element.topContent = [{text: 'User', bold: true}, {text: 'email'}]; flushAsynchronousOperations(); - var topItems = Polymer.dom(element.root).querySelectorAll('.top-item'); + const topItems = Polymer.dom(element.root).querySelectorAll('.top-item'); assert.equal(topItems.length, 2); assert.isTrue(topItems[0].classList.contains('bold-text')); assert.isFalse(topItems[1].classList.contains('bold-text')); }); - test('non link items', function() { - element.items = [ - {name: 'item one', id: 'foo'}, {name: 'item two', id: 'bar'}]; - var stub = sinon.stub(); - element.addEventListener('tap-item-foo', stub); + test('non link items', () => { + const item0 = {name: 'item one', id: 'foo'}; + element.items = [item0, {name: 'item two', id: 'bar'}]; + const fooTapped = sandbox.stub(); + const tapped = sandbox.stub(); + element.addEventListener('tap-item-foo', fooTapped); + element.addEventListener('tap-item', tapped); flushAsynchronousOperations(); MockInteractions.tap(element.$$('.itemAction')); - assert.isTrue(stub.called); + assert.isTrue(fooTapped.called); + assert.isTrue(tapped.called); + assert.deepEqual(tapped.lastCall.args[0].detail, item0); }); - test('disabled non link item', function() { + test('disabled non link item', () => { element.items = [{name: 'item one', id: 'foo'}]; element.disabledIds = ['foo']; - var stub = sinon.stub(); + const stub = sandbox.stub(); + const tapped = sandbox.stub(); element.addEventListener('tap-item-foo', stub); + element.addEventListener('tap-item', tapped); flushAsynchronousOperations(); MockInteractions.tap(element.$$('.itemAction')); assert.isFalse(stub.called); + assert.isFalse(tapped.called); + }); + + suite('keyboard navigation', () => { + setup(() => { + element.items = [ + {name: 'item one', id: 'foo'}, + {name: 'item two', id: 'bar'}, + ]; + flushAsynchronousOperations(); + }); + + test('down', () => { + const stub = sandbox.stub(element.$.cursor, 'next'); + assert.isFalse(element.$.dropdown.opened); + MockInteractions.pressAndReleaseKeyOn(element, 40); + assert.isTrue(element.$.dropdown.opened); + MockInteractions.pressAndReleaseKeyOn(element, 40); + assert.isTrue(stub.called); + }); + + test('up', () => { + const stub = sandbox.stub(element.$.cursor, 'previous'); + assert.isFalse(element.$.dropdown.opened); + MockInteractions.pressAndReleaseKeyOn(element, 38); + assert.isTrue(element.$.dropdown.opened); + MockInteractions.pressAndReleaseKeyOn(element, 38); + assert.isTrue(stub.called); + }); + + test('enter/space', () => { + // Because enter and space are handled by the same fn, we need only to + // test one. + assert.isFalse(element.$.dropdown.opened); + MockInteractions.pressAndReleaseKeyOn(element, 32); // Space + assert.isTrue(element.$.dropdown.opened); + + const el = element.$.cursor.target.querySelector(':not([hidden])'); + const stub = sandbox.stub(el, 'click'); + MockInteractions.pressAndReleaseKeyOn(element, 32); // Space + assert.isTrue(stub.called); + }); }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html index bd87db3..76453e4 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html +++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.html
@@ -16,10 +16,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-editable-content"> <template> - <style> + <style include="shared-styles"> :host { display: block; }
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js index 86211a4..e6ea72d 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js +++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
@@ -53,11 +53,11 @@ _newContent: String, }, - focusTextarea: function() { + focusTextarea() { this.$$('iron-autogrow-textarea').textarea.focus(); }, - _editingChanged: function(editing) { + _editingChanged(editing) { if (!editing) { return; } // TODO(wyatta) switch linkify sequence, see issue 5526. @@ -65,16 +65,16 @@ this.content.replace(/^R=\u200B/gm, 'R=') : this.content; }, - _computeSaveDisabled: function(disabled, content, newContent) { + _computeSaveDisabled(disabled, content, newContent) { return disabled || (content === newContent); }, - _handleSave: function(e) { + _handleSave(e) { e.preventDefault(); this.fire('editable-content-save', {content: this._newContent}); }, - _handleCancel: function(e) { + _handleCancel(e) { e.preventDefault(); this.editing = false; this.fire('editable-content-cancel');
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html index b25e815..d8e5b21 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html +++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
@@ -20,6 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script> <link rel="import" href="gr-editable-content.html"> @@ -33,37 +34,37 @@ </test-fixture> <script> - suite('gr-editable-content tests', function() { - var element; + suite('gr-editable-content tests', () => { + let element; - setup(function() { + setup(() => { element = fixture('basic'); }); - test('save event', function(done) { + test('save event', done => { element._newContent = 'foo'; - element.addEventListener('editable-content-save', function(e) { + element.addEventListener('editable-content-save', e => { assert.equal(e.detail.content, 'foo'); done(); }); MockInteractions.tap(element.$$('gr-button[primary]')); }); - test('cancel event', function(done) { - element.addEventListener('editable-content-cancel', function() { + test('cancel event', done => { + element.addEventListener('editable-content-cancel', () => { done(); }); MockInteractions.tap(element.$$('gr-button:not([primary])')); }); - test('enabling editing updates edit field contents', function() { + test('enabling editing updates edit field contents', () => { element.content = 'current content'; element._newContent = 'stale content'; element.editing = true; assert.equal(element._newContent, 'current content'); }); - test('disabling editing does not update edit field contents', function() { + test('disabling editing does not update edit field contents', () => { element.content = 'current content'; element.editing = true; element._newContent = 'stale content'; @@ -71,24 +72,24 @@ assert.equal(element._newContent, 'stale content'); }); - test('zero width spaces are removed properly', function() { + test('zero width spaces are removed properly', () => { element.removeZeroWidthSpace = true; element.content = 'R=\u200Btest@google.com'; element.editing = true; assert.equal(element._newContent, 'R=test@google.com'); }); - suite('editing', function() { - setup(function() { + suite('editing', () => { + setup(() => { element.content = 'current content'; element.editing = true; }); - test('save button is disabled initially', function() { + test('save button is disabled initially', () => { assert.isTrue(element.$$('gr-button[primary]').disabled); }); - test('save button is enabled when content changes', function() { + test('save button is enabled when content changes', () => { element._newContent = 'new content'; assert.isFalse(element.$$('gr-button[primary]').disabled); });
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html index 32cff2a..08ecf6f 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html +++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.html
@@ -13,11 +13,14 @@ See the License for the specific language governing permissions and limitations under the License. --> +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/iron-input/iron-input.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + <dom-module id="gr-editable-label"> <template> - <style> + <style include="shared-styles"> :host { align-items: center; display: inline-flex; @@ -37,10 +40,8 @@ white-space: nowrap; } label.editable { - cursor: pointer; - } - label.editable.placeholder { color: #00f; + cursor: pointer; text-decoration: underline; } </style> @@ -48,7 +49,6 @@ is="iron-input" id="input" hidden$="[[!editing]]" - on-keydown="_handleInputKeydown" bind-value="{{_inputText}}"> <label hidden$="[[editing]]"
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js index f3e83f9..7ccc3ab 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js +++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -45,34 +45,43 @@ _inputText: String, }, + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + ], + + keyBindings: { + enter: '_handleEnter', + esc: '_handleEsc', + }, + hostAttributes: { tabindex: '0', }, - _usePlaceholder: function(value, placeholder) { + _usePlaceholder(value, placeholder) { return (!value || !value.length) && placeholder; }, - _computeLabel: function(value, placeholder) { + _computeLabel(value, placeholder) { if (this._usePlaceholder(value, placeholder)) { return placeholder; } return value; }, - _open: function() { + _open() { if (this.readOnly || this.editing) { return; } this._inputText = this.value; this.editing = true; - this.async(function() { + this.async(() => { this.$.input.focus(); this.$.input.setSelectionRange(0, this.$.input.value.length); }); }, - _save: function() { + _save() { if (!this.editing) { return; } this.value = this._inputText; @@ -80,25 +89,33 @@ this.fire('changed', this.value); }, - _cancel: function() { + _cancel() { if (!this.editing) { return; } this.editing = false; this._inputText = this.value; }, - _handleInputKeydown: function(e) { - if (e.keyCode === 13) { // Enter key + _handleEnter(e) { + e = this.getKeyboardEvent(e); + const target = Polymer.dom(e).rootTarget; + if (target === this.$.input) { e.preventDefault(); this._save(); - } else if (e.keyCode === 27) { // Escape key + } + }, + + _handleEsc(e) { + e = this.getKeyboardEvent(e); + const target = Polymer.dom(e).rootTarget; + if (target === this.$.input) { e.preventDefault(); this._cancel(); } }, - _computeLabelClass: function(readOnly, value, placeholder) { - var classes = []; + _computeLabelClass(readOnly, value, placeholder) { + const classes = []; if (!readOnly) { classes.push('editable'); } if (this._usePlaceholder(value, placeholder)) { classes.push('placeholder'); @@ -106,7 +123,7 @@ return classes.join(' '); }, - _updateTitle: function(value) { + _updateTitle(value) { this.setAttribute('title', (value && value.length) ? value : null); }, });
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html index 42770aa..dc1a5aa 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html +++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -20,6 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script> <link rel="import" href="gr-editable-label.html"> @@ -44,19 +45,25 @@ </test-fixture> <script> - suite('gr-editable-label tests', function() { - var element; - var input; - var label; + suite('gr-editable-label tests', () => { + let element; + let input; + let label; + let sandbox; - setup(function() { + setup(() => { element = fixture('basic'); input = element.$$('input'); label = element.$$('label'); + sandbox = sinon.sandbox.create(); }); - test('element render', function() { + teardown(() => { + sandbox.restore(); + }); + + test('element render', () => { // The input is hidden and the label is visible: assert.isNotNull(input.getAttribute('hidden')); assert.isNull(label.getAttribute('hidden')); @@ -76,8 +83,8 @@ assert.equal(input.value, 'value text'); }); - test('edit value', function(done) { - var editedStub = sinon.stub(); + test('edit value', done => { + const editedStub = sandbox.stub(); element.addEventListener('changed', editedStub); MockInteractions.tap(label); @@ -88,7 +95,7 @@ assert.isFalse(editedStub.called); - element.async(function() { + element.async(() => { assert.isTrue(editedStub.called); assert.equal(input.value, 'new text'); done(); @@ -99,19 +106,19 @@ }); }); - suite('gr-editable-label read-only tests', function() { - var element; - var input; - var label; + suite('gr-editable-label read-only tests', () => { + let element; + let input; + let label; - setup(function() { + setup(() => { element = fixture('read-only'); input = element.$$('input'); label = element.$$('label'); }); - test('disallows edit when read-only', function() { + test('disallows edit when read-only', () => { // The input is hidden and the label is visible: assert.isNotNull(input.getAttribute('hidden')); assert.isNull(label.getAttribute('hidden')); @@ -125,7 +132,7 @@ assert.isNull(label.getAttribute('hidden')); }); - test('label is not marked as editable', function() { + test('label is not marked as editable', () => { assert.isFalse(label.classList.contains('editable')); }); });
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html new file mode 100644 index 0000000..ca385c2 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.html
@@ -0,0 +1,46 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-fixed-panel"> + <template> + <style include="shared-styles"> + :host { + display: block; + min-height: var(--header-height); + position: relative; + } + header { + background: inherit; + border: inherit; + display: inline; + height: inherit; + } + .floating { + left: 0; + position: fixed; + width: 100%; + will-change: top; + } + </style> + <header id="header" class$="[[_computeHeaderClass(_headerFloating)]]"> + <content></content> + </header> + </template> + <script src="gr-fixed-panel.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js new file mode 100644 index 0000000..fc38daa --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -0,0 +1,183 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-fixed-panel', + + properties: { + floatingDisabled: Boolean, + readyForMeasure: { + type: Boolean, + observer: '_readyForMeasureObserver', + }, + keepOnScroll: { + type: Boolean, + value: false, + }, + _isMeasured: { + type: Boolean, + value: false, + }, + _topInitial: Number, + _topLast: Number, + _headerHeight: Number, + _headerFloating: { + type: Boolean, + value: false, + }, + _observer: { + type: Object, + value: null, + }, + _webComponentsReady: Boolean, + }, + + attached() { + if (this.floatingDisabled) { + return; + } + // Enable content measure unless blocked by param. + if (this.readyForMeasure !== false) { + this.readyForMeasure = true; + } + this.listen(window, 'resize', 'update'); + this.listen(window, 'scroll', '_updateOnScroll'); + this._observer = new MutationObserver(this.update.bind(this)); + this._observer.observe(this.$.header, {childList: true, subtree: true}); + }, + + detached() { + this.unlisten(window, 'scroll', '_updateOnScroll'); + this.unlisten(window, 'resize', 'update'); + if (this._observer) { + this._observer.disconnect(); + } + }, + + _readyForMeasureObserver(readyForMeasure) { + if (readyForMeasure) { + this.update(); + } + }, + + _computeHeaderClass(headerFloating) { + return headerFloating ? 'floating' : ''; + }, + + _getScrollY() { + return window.scrollY; + }, + + unfloat() { + if (this.floatingDisabled) { + return; + } + this.$.header.style.top = ''; + this._headerFloating = false; + this.customStyle['--header-height'] = ''; + this.updateStyles(); + }, + + update() { + this.debounce('update', () => { + this._updateDebounced(); + }, 100); + }, + + _updateOnScroll() { + this.debounce('update', () => { + this._updateDebounced(); + }); + }, + + _updateDebounced() { + if (this.floatingDisabled) { + return; + } + this._isMeasured = false; + this._maybeFloatHeader(); + this._reposition(); + }, + + _reposition() { + if (!this._headerFloating) { + return; + } + const header = this.$.header; + const scrollY = this._topInitial - this._getScrollY(); + let newTop; + if (this.keepOnScroll) { + if (scrollY > 0) { + // Reposition to imitate natural scrolling. + newTop = scrollY; + } else { + newTop = 0; + } + } else if (scrollY > -this._headerHeight || + this._topLast < -this._headerHeight) { + // Allow to scroll away, but ignore when far behind the edge. + newTop = scrollY; + } else { + newTop = -this._headerHeight; + } + if (this._topLast !== newTop) { + if (newTop === undefined) { + header.style.top = ''; + } else { + header.style.top = newTop + 'px'; + } + this._topLast = newTop; + } + }, + + _measure() { + if (this._isMeasured) { + return; // Already measured. + } + const rect = this.$.header.getBoundingClientRect(); + if (rect.height === 0 && rect.width === 0) { + return; // Not ready for measurement yet. + } + const top = document.body.scrollTop + rect.top; + this._topLast = top; + this._headerHeight = rect.height; + this._topInitial = + this.getBoundingClientRect().top + document.body.scrollTop; + this._isMeasured = true; + }, + + _isFloatingNeeded() { + return this.keepOnScroll || + document.body.scrollWidth > document.body.clientWidth; + }, + + _maybeFloatHeader() { + if (!this._isFloatingNeeded()) { + return; + } + this._measure(); + if (this._isMeasured) { + this._floatHeader(); + } + }, + + _floatHeader() { + this.customStyle['--header-height'] = this._headerHeight + 'px'; + this.updateStyles(); + this._headerFloating = true; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html new file mode 100644 index 0000000..d813a44 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
@@ -0,0 +1,103 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-fixed-panel</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-fixed-panel.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-fixed-panel> + <div style="height: 100px"></div> + </gr-fixed-panel> + </template> +</test-fixture> + +<script> + suite('gr-fixed-panel', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.readyForMeasure = true; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('can be disabled with floatingDisabled', () => { + element.floatingDisabled = true; + sandbox.stub(element, '_reposition'); + window.dispatchEvent(new CustomEvent('resize')); + element.flushDebouncer('update'); + assert.isFalse(element._reposition.called); + }); + + test('header is the height of the content', () => { + assert.equal(element.getBoundingClientRect().height, 100); + }); + + test('scroll triggers _reposition', () => { + sandbox.stub(element, '_reposition'); + window.dispatchEvent(new CustomEvent('scroll')); + element.flushDebouncer('update'); + assert.isTrue(element._reposition.called); + }); + + suite('_reposition', () => { + const getHeaderTop = function() { + return element.$.header.style.top; + }; + + const emulateScrollY = function(distance) { + element._getScrollY.returns(distance); + element._updateDebounced(); + element.flushDebouncer('scroll'); + }; + + setup(() => { + element._headerTopInitial = 10; + sandbox.stub(element, '_getScrollY').returns(0); + }); + + test('scrolls header along with document', () => { + emulateScrollY(20); + assert.equal(getHeaderTop(), '-12px'); + }); + + test('does not stick to the top by default', () => { + emulateScrollY(150); + assert.equal(getHeaderTop(), '-100px'); + }); + + test('sticks to the top if enabled', () => { + element.keepOnScroll = true; + emulateScrollY(120); + assert.equal(getHeaderTop(), '0px'); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html index d719f70..28a9e0e 100644 --- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.html
@@ -15,10 +15,11 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../gr-linked-text/gr-linked-text.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-formatted-text"> <template> - <style> + <style include="shared-styles"> :host { display: block; font-family: var(--font-family); @@ -29,10 +30,15 @@ gr-linked-text.pre { margin: 0 0 1.4em 0; } - :host.noTrailingMargin p:last-child, - :host.noTrailingMargin ul:last-child, - :host.noTrailingMargin blockquote:last-child, - :host.noTrailingMargin gr-linked-text.pre:last-child { + p, + ul, + blockquote { + max-width: var(--gr-formatted-text-prose-max-width, none); + } + :host(.noTrailingMargin) p:last-child, + :host(.noTrailingMargin) ul:last-child, + :host(.noTrailingMargin) blockquote:last-child, + :host(.noTrailingMargin) gr-linked-text.pre:last-child { margin: 0; } blockquote { @@ -40,6 +46,7 @@ padding: 0 .7em; } li { + list-style-type: disc; margin-left: 1.4em; } gr-linked-text.pre {
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js index a2130b2..b26226e 100644 --- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
@@ -14,7 +14,8 @@ (function() { 'use strict'; - var QUOTE_MARKER_PATTERN = /\n\s?>\s/g; + // eslint-disable-next-line no-unused-vars + const QUOTE_MARKER_PATTERN = /\n\s?>\s/g; Polymer({ is: 'gr-formatted-text', @@ -35,7 +36,7 @@ '_contentOrConfigChanged(content, config)', ], - ready: function() { + ready() { if (this.noTrailingMargin) { this.classList.add('noTrailingMargin'); } @@ -50,11 +51,11 @@ * * @return {string} */ - getTextContent: function() { + getTextContent() { return this._blocksToText(this._computeBlocks(this.content)); }, - _contentChanged: function(content) { + _contentChanged(content) { // In the case where the config may not be set (perhaps due to the // request for it still being in flight), set the content anyway to // prevent waiting on the config to display the text. @@ -65,8 +66,8 @@ /** * Given a source string, update the DOM inside #container. */ - _contentOrConfigChanged: function(content) { - var container = Polymer.dom(this.$.container); + _contentOrConfigChanged(content) { + const container = Polymer.dom(this.$.container); // Remove existing content. while (container.firstChild) { @@ -74,10 +75,9 @@ } // Add new content. - this._computeNodes(this._computeBlocks(content)) - .forEach(function(node) { + for (const node of this._computeNodes(this._computeBlocks(content))) { container.appendChild(node); - }); + } }, /** @@ -102,14 +102,14 @@ * @param {string} content * @return {!Array<!Object>} */ - _computeBlocks: function(content) { + _computeBlocks(content) { if (!content) { return []; } - var result = []; - var split = content.split('\n\n'); - var p; + const result = []; + const split = content.split('\n\n'); + let p; - for (var i = 0; i < split.length; i++) { + for (let i = 0; i < split.length; i++) { p = split[i]; if (!p.length) { continue; } @@ -153,14 +153,14 @@ * potential paragraph). * @param {!Array<!Object>} out The list of blocks to append to. */ - _makeList: function(p, out) { - var block = null; - var inList = false; - var inParagraph = false; - var lines = p.split('\n'); - var line; + _makeList(p, out) { + let block = null; + let inList = false; + let inParagraph = false; + const lines = p.split('\n'); + let line; - for (var i = 0; i < lines.length; i++) { + for (let i = 0; i < lines.length; i++) { line = lines[i]; if (line[0] === '-' || line[0] === '*') { @@ -198,10 +198,10 @@ } }, - _makeQuote: function(p) { - var quotedLines = p + _makeQuote(p) { + const quotedLines = p .split('\n') - .map(function(l) { return l.replace(/^[ ]?>[ ]?/, ''); }) + .map(l => l.replace(/^[ ]?>[ ]?/, '')) .join('\n'); return { type: 'quote', @@ -209,22 +209,22 @@ }; }, - _isQuote: function(p) { - return p.indexOf('> ') === 0 || p.indexOf(' > ') === 0; + _isQuote(p) { + return p.startsWith('> ') || p.startsWith(' > '); }, - _isPreFormat: function(p) { - return p.indexOf('\n ') !== -1 || p.indexOf('\n\t') !== -1 || - p.indexOf(' ') === 0 || p.indexOf('\t') === 0; + _isPreFormat(p) { + return p.includes('\n ') || p.includes('\n\t') || + p.startsWith(' ') || p.startsWith('\t'); }, - _isList: function(p) { - return p.indexOf('\n- ') !== -1 || p.indexOf('\n* ') !== -1 || - p.indexOf('- ') === 0 || p.indexOf('* ') === 0; + _isList(p) { + return p.includes('\n- ') || p.includes('\n* ') || + p.startsWith('- ') || p.startsWith('* '); }, - _makeLinkedText: function(content, isPre) { - var text = document.createElement('gr-linked-text'); + _makeLinkedText(content, isPre) { + const text = document.createElement('gr-linked-text'); text.config = this.config; text.content = content; text.pre = true; @@ -239,19 +239,19 @@ * @param {!Array<!Object>} blocks * @return {!Array<!HTMLElement>} */ - _computeNodes: function(blocks) { - return blocks.map(function(block) { + _computeNodes(blocks) { + return blocks.map(block => { if (block.type === 'paragraph') { - var p = document.createElement('p'); + const p = document.createElement('p'); p.appendChild(this._makeLinkedText(block.text)); return p; } if (block.type === 'quote') { - var bq = document.createElement('blockquote'); - this._computeNodes(block.blocks).forEach(function(node) { + const bq = document.createElement('blockquote'); + for (const node of this._computeNodes(block.blocks)) { bq.appendChild(node); - }); + } return bq; } @@ -260,19 +260,19 @@ } if (block.type === 'list') { - var ul = document.createElement('ul'); - block.items.forEach(function(item) { - var li = document.createElement('li'); + const ul = document.createElement('ul'); + for (const item of block.items) { + const li = document.createElement('li'); li.appendChild(this._makeLinkedText(item)); ul.appendChild(li); - }.bind(this)); + } return ul; } - }.bind(this)); + }); }, - _blocksToText: function(blocks) { - return blocks.map(function(block) { + _blocksToText(blocks) { + return blocks.map(block => { if (block.type === 'paragraph' || block.type === 'pre') { return block.text; } @@ -282,7 +282,7 @@ if (block.type === 'list') { return block.items.join('\n'); } - }.bind(this)).join('\n\n'); + }).join('\n\n'); }, }); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html index 5afe60a..fe5b92f 100644 --- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
@@ -20,7 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-formatted-text.html"> <script>void(0);</script> @@ -32,9 +32,9 @@ </test-fixture> <script> - suite('gr-formatted-text tests', function() { - var element; - var sandbox; + suite('gr-formatted-text tests', () => { + let element; + let sandbox; function assertBlock(result, index, type, text) { assert.equal(result[index].type, type); @@ -46,73 +46,73 @@ assert.equal(result[resultIndex].items[itemIndex], text); } - setup(function() { + setup(() => { element = fixture('basic'); sandbox = sinon.sandbox.create(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('parse null undefined and empty', function() { + test('parse null undefined and empty', () => { assert.lengthOf(element._computeBlocks(null), 0); assert.lengthOf(element._computeBlocks(undefined), 0); assert.lengthOf(element._computeBlocks(''), 0); }); - test('parse simple', function() { - var comment = 'Para1'; - var result = element._computeBlocks(comment); + test('parse simple', () => { + const comment = 'Para1'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assertBlock(result, 0, 'paragraph', comment); }); - test('parse multiline para', function() { - var comment = 'Para 1\nStill para 1'; - var result = element._computeBlocks(comment); + test('parse multiline para', () => { + const comment = 'Para 1\nStill para 1'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assertBlock(result, 0, 'paragraph', comment); }); - test('parse para break', function() { - var comment = 'Para 1\n\nPara 2\n\nPara 3'; - var result = element._computeBlocks(comment); + test('parse para break', () => { + const comment = 'Para 1\n\nPara 2\n\nPara 3'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 3); assertBlock(result, 0, 'paragraph', 'Para 1'); assertBlock(result, 1, 'paragraph', 'Para 2'); assertBlock(result, 2, 'paragraph', 'Para 3'); }); - test('parse quote', function() { - var comment = '> Quote text'; - var result = element._computeBlocks(comment); + test('parse quote', () => { + const comment = '> Quote text'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assert.equal(result[0].type, 'quote'); assert.lengthOf(result[0].blocks, 1); assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text'); }); - test('parse quote lead space', function() { - var comment = ' > Quote text'; - var result = element._computeBlocks(comment); + test('parse quote lead space', () => { + const comment = ' > Quote text'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assert.equal(result[0].type, 'quote'); assert.lengthOf(result[0].blocks, 1); assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text'); }); - test('parse excludes empty', function() { - var comment = 'Para 1\n\n\n\nPara 2'; - var result = element._computeBlocks(comment); + test('parse excludes empty', () => { + const comment = 'Para 1\n\n\n\nPara 2'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 2); assertBlock(result, 0, 'paragraph', 'Para 1'); assertBlock(result, 1, 'paragraph', 'Para 2'); }); - test('parse multiline quote', function() { - var comment = '> Quote line 1\n> Quote line 2\n > Quote line 3\n'; - var result = element._computeBlocks(comment); + test('parse multiline quote', () => { + const comment = '> Quote line 1\n> Quote line 2\n > Quote line 3\n'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assert.equal(result[0].type, 'quote'); assert.lengthOf(result[0].blocks, 1); @@ -120,55 +120,55 @@ 'Quote line 1\nQuote line 2\nQuote line 3\n'); }); - test('parse pre', function() { - var comment = ' Four space indent.'; - var result = element._computeBlocks(comment); + test('parse pre', () => { + const comment = ' Four space indent.'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assertBlock(result, 0, 'pre', comment); }); - test('parse one space pre', function() { - var comment = ' One space indent.\n Another line.'; - var result = element._computeBlocks(comment); + test('parse one space pre', () => { + const comment = ' One space indent.\n Another line.'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assertBlock(result, 0, 'pre', comment); }); - test('parse tab pre', function() { - var comment = '\tOne tab indent.\n\tAnother line.\n Yet another!'; - var result = element._computeBlocks(comment); + test('parse tab pre', () => { + const comment = '\tOne tab indent.\n\tAnother line.\n Yet another!'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assertBlock(result, 0, 'pre', comment); }); - test('parse intermediate leading whitespace pre', function() { - var comment = 'No indent.\n\tNonzero indent.\nNo indent again.'; - var result = element._computeBlocks(comment); + test('parse intermediate leading whitespace pre', () => { + const comment = 'No indent.\n\tNonzero indent.\nNo indent again.'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assertBlock(result, 0, 'pre', comment); }); - test('parse star list', function() { - var comment = '* Item 1\n* Item 2\n* Item 3'; - var result = element._computeBlocks(comment); + test('parse star list', () => { + const comment = '* Item 1\n* Item 2\n* Item 3'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assertListBlock(result, 0, 0, 'Item 1'); assertListBlock(result, 0, 1, 'Item 2'); assertListBlock(result, 0, 2, 'Item 3'); }); - test('parse dash list', function() { - var comment = '- Item 1\n- Item 2\n- Item 3'; - var result = element._computeBlocks(comment); + test('parse dash list', () => { + const comment = '- Item 1\n- Item 2\n- Item 3'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assertListBlock(result, 0, 0, 'Item 1'); assertListBlock(result, 0, 1, 'Item 2'); assertListBlock(result, 0, 2, 'Item 3'); }); - test('parse mixed list', function() { - var comment = '- Item 1\n* Item 2\n- Item 3\n* Item 4'; - var result = element._computeBlocks(comment); + test('parse mixed list', () => { + const comment = '- Item 1\n* Item 2\n- Item 3\n* Item 4'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assertListBlock(result, 0, 0, 'Item 1'); assertListBlock(result, 0, 1, 'Item 2'); @@ -176,8 +176,8 @@ assertListBlock(result, 0, 3, 'Item 4'); }); - test('parse mixed block types', function() { - var comment = 'Paragraph\nacross\na\nfew\nlines.' + + test('parse mixed block types', () => { + const comment = 'Paragraph\nacross\na\nfew\nlines.' + '\n\n' + '> Quote\n> across\n> not many lines.' + '\n\n' + @@ -190,7 +190,7 @@ '\tPreformatted text.' + '\n\n' + 'Parting words.'; - var result = element._computeBlocks(comment); + const result = element._computeBlocks(comment); assert.lengthOf(result, 7); assertBlock(result, 0, 'paragraph', 'Paragraph\nacross\na\nfew\nlines.'); @@ -209,18 +209,18 @@ assertBlock(result, 6, 'paragraph', 'Parting words.'); }); - test('bullet list 1', function() { - var comment = 'A\n\n* line 1\n* 2nd line'; - var result = element._computeBlocks(comment); + test('bullet list 1', () => { + const comment = 'A\n\n* line 1\n* 2nd line'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 2); assertBlock(result, 0, 'paragraph', 'A'); assertListBlock(result, 1, 0, 'line 1'); assertListBlock(result, 1, 1, '2nd line'); }); - test('bullet list 2', function() { - var comment = 'A\n\n* line 1\n* 2nd line\n\nB'; - var result = element._computeBlocks(comment); + test('bullet list 2', () => { + const comment = 'A\n\n* line 1\n* 2nd line\n\nB'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 3); assertBlock(result, 0, 'paragraph', 'A'); assertListBlock(result, 1, 0, 'line 1'); @@ -228,50 +228,50 @@ assertBlock(result, 2, 'paragraph', 'B'); }); - test('bullet list 3', function() { - var comment = '* line 1\n* 2nd line\n\nB'; - var result = element._computeBlocks(comment); + test('bullet list 3', () => { + const comment = '* line 1\n* 2nd line\n\nB'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 2); assertListBlock(result, 0, 0, 'line 1'); assertListBlock(result, 0, 1, '2nd line'); assertBlock(result, 1, 'paragraph', 'B'); }); - test('bullet list 4', function() { - var comment = 'To see this bug, you have to:\n' + + test('bullet list 4', () => { + const comment = 'To see this bug, you have to:\n' + '* Be on IMAP or EAS (not on POP)\n' + '* Be very unlucky\n'; - var result = element._computeBlocks(comment); + const result = element._computeBlocks(comment); assert.lengthOf(result, 2); assertBlock(result, 0, 'paragraph', 'To see this bug, you have to:'); assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)'); assertListBlock(result, 1, 1, 'Be very unlucky'); }); - test('bullet list 5', function() { - var comment = 'To see this bug,\n' + + test('bullet list 5', () => { + const comment = 'To see this bug,\n' + 'you have to:\n' + '* Be on IMAP or EAS (not on POP)\n' + '* Be very unlucky\n'; - var result = element._computeBlocks(comment); + const result = element._computeBlocks(comment); assert.lengthOf(result, 2); assertBlock(result, 0, 'paragraph', 'To see this bug, you have to:'); assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)'); assertListBlock(result, 1, 1, 'Be very unlucky'); }); - test('dash list 1', function() { - var comment = 'A\n\n- line 1\n- 2nd line'; - var result = element._computeBlocks(comment); + test('dash list 1', () => { + const comment = 'A\n\n- line 1\n- 2nd line'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 2); assertBlock(result, 0, 'paragraph', 'A'); assertListBlock(result, 1, 0, 'line 1'); assertListBlock(result, 1, 1, '2nd line'); }); - test('dash list 2', function() { - var comment = 'A\n\n- line 1\n- 2nd line\n\nB'; - var result = element._computeBlocks(comment); + test('dash list 2', () => { + const comment = 'A\n\n- line 1\n- 2nd line\n\nB'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 3); assertBlock(result, 0, 'paragraph', 'A'); assertListBlock(result, 1, 0, 'line 1'); @@ -279,52 +279,52 @@ assertBlock(result, 2, 'paragraph', 'B'); }); - test('dash list 3', function() { - var comment = '- line 1\n- 2nd line\n\nB'; - var result = element._computeBlocks(comment); + test('dash list 3', () => { + const comment = '- line 1\n- 2nd line\n\nB'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 2); assertListBlock(result, 0, 0, 'line 1'); assertListBlock(result, 0, 1, '2nd line'); assertBlock(result, 1, 'paragraph', 'B'); }); - test('pre format 1', function() { - var comment = 'A\n\n This is pre\n formatted'; - var result = element._computeBlocks(comment); + test('pre format 1', () => { + const comment = 'A\n\n This is pre\n formatted'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 2); assertBlock(result, 0, 'paragraph', 'A'); assertBlock(result, 1, 'pre', ' This is pre\n formatted'); }); - test('pre format 2', function() { - var comment = 'A\n\n This is pre\n formatted\n\nbut this is not'; - var result = element._computeBlocks(comment); + test('pre format 2', () => { + const comment = 'A\n\n This is pre\n formatted\n\nbut this is not'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 3); assertBlock(result, 0, 'paragraph', 'A'); assertBlock(result, 1, 'pre', ' This is pre\n formatted'); assertBlock(result, 2, 'paragraph', 'but this is not'); }); - test('pre format 3', function() { - var comment = 'A\n\n Q\n <R>\n S\n\nB'; - var result = element._computeBlocks(comment); + test('pre format 3', () => { + const comment = 'A\n\n Q\n <R>\n S\n\nB'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 3); assertBlock(result, 0, 'paragraph', 'A'); assertBlock(result, 1, 'pre', ' Q\n <R>\n S'); assertBlock(result, 2, 'paragraph', 'B'); }); - test('pre format 4', function() { - var comment = ' Q\n <R>\n S\n\nB'; - var result = element._computeBlocks(comment); + test('pre format 4', () => { + const comment = ' Q\n <R>\n S\n\nB'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 2); assertBlock(result, 0, 'pre', ' Q\n <R>\n S'); assertBlock(result, 1, 'paragraph', 'B'); }); - test('quote 1', function() { - var comment = '> I\'m happy\n > with quotes!\n\nSee above.'; - var result = element._computeBlocks(comment); + test('quote 1', () => { + const comment = '> I\'m happy\n > with quotes!\n\nSee above.'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 2); assert.equal(result[0].type, 'quote'); assert.lengthOf(result[0].blocks, 1); @@ -332,9 +332,9 @@ assertBlock(result, 1, 'paragraph', 'See above.'); }); - test('quote 2', function() { - var comment = 'See this said:\n\n > a quoted\n > string block\n\nOK?'; - var result = element._computeBlocks(comment); + test('quote 2', () => { + const comment = 'See this said:\n\n > a quoted\n > string block\n\nOK?'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 3); assertBlock(result, 0, 'paragraph', 'See this said:'); assert.equal(result[1].type, 'quote'); @@ -343,9 +343,9 @@ assertBlock(result, 2, 'paragraph', 'OK?'); }); - test('nested quotes', function() { - var comment = ' > > prior\n > \n > next\n'; - var result = element._computeBlocks(comment); + test('nested quotes', () => { + const comment = ' > > prior\n > \n > next\n'; + const result = element._computeBlocks(comment); assert.lengthOf(result, 1); assert.equal(result[0].type, 'quote'); assert.lengthOf(result[0].blocks, 2); @@ -355,25 +355,25 @@ assertBlock(result[0].blocks, 1, 'paragraph', 'next\n'); }); - test('getTextContent', function() { - var comment = 'Paragraph\n\n pre\n\n* List\n* Of\n* Items\n\n> Quote'; + test('getTextContent', () => { + const comment = 'Paragraph\n\n pre\n\n* List\n* Of\n* Items\n\n> Quote'; element.content = comment; - var result = element.getTextContent(); - var expected = 'Paragraph\n\n pre\n\nList\nOf\nItems\n\nQuote'; + const result = element.getTextContent(); + const expected = 'Paragraph\n\n pre\n\nList\nOf\nItems\n\nQuote'; assert.equal(result, expected); }); - test('_contentOrConfigChanged not called without config', function() { - var contentStub = sandbox.stub(element, '_contentChanged'); - var contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged'); + test('_contentOrConfigChanged not called without config', () => { + const contentStub = sandbox.stub(element, '_contentChanged'); + const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged'); element.content = 'some text'; assert.isTrue(contentStub.called); assert.isFalse(contentConfigStub.called); }); - test('_contentOrConfigChanged called with config', function() { - var contentStub = sandbox.stub(element, '_contentChanged'); - var contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged'); + test('_contentOrConfigChanged called with config', () => { + const contentStub = sandbox.stub(element, '_contentChanged'); + const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged'); element.content = 'some text'; element.config = {}; assert.isTrue(contentStub.called);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js index 72c7f6e..99c4fed 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api.js
@@ -22,17 +22,27 @@ } GrChangeActionsInterface.prototype.addPrimaryActionKey = function(key) { - if (this._el.primaryActionKeys.indexOf(key) !== -1) { return; } + if (this._el.primaryActionKeys.includes(key)) { return; } this._el.push('primaryActionKeys', key); }; GrChangeActionsInterface.prototype.removePrimaryActionKey = function(key) { - this._el.primaryActionKeys = this._el.primaryActionKeys.filter(function(k) { + this._el.primaryActionKeys = this._el.primaryActionKeys.filter(k => { return k !== key; }); }; + GrChangeActionsInterface.prototype.setActionOverflow = function(type, key, + overflow) { + return this._el.setActionOverflow(type, key, overflow); + }; + + GrChangeActionsInterface.prototype.setActionPriority = function(type, key, + priority) { + return this._el.setActionPriority(type, key, priority); + }; + GrChangeActionsInterface.prototype.setActionHidden = function(type, key, hidden) { return this._el.setActionHidden(type, key, hidden);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html index 20b5dcb..53d7345 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -20,8 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <!-- This must refer to the element this interface is wrapping around. Otherwise breaking changes to gr-change-actions won’t be noticed. @@ -37,43 +36,44 @@ </test-fixture> <script> - suite('gr-js-api-interface tests', function() { - var element; - var changeActions; + suite('gr-js-api-interface tests', () => { + let element; + let changeActions; // Because deepEqual doesn’t behave in Safari. function assertArraysEqual(actual, expected) { assert.equal(actual.length, expected.length); - for (var i = 0; i < actual.length; i++) { + for (let i = 0; i < actual.length; i++) { assert.equal(actual[i], expected[i]); } } - setup(function() { + setup(() => { element = fixture('basic'); element.change = {}; element._hasKnownChainState = false; - var plugin; - Gerrit.install(function(p) { plugin = p; }, '0.1', + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', 'http://test.com/plugins/testplugin/static/test.js'); changeActions = plugin.changeActions(); }); - teardown(function() { + teardown(() => { changeActions = null; }); - test('property existence', function() { - [ + test('property existence', () => { + const properties = [ 'ActionType', 'ChangeActions', 'RevisionActions', - ].forEach(function(p) { + ]; + for (const p of properties) { assertArraysEqual(changeActions[p], element[p]); - }); + } }); - test('add/remove primary action keys', function() { + test('add/remove primary action keys', () => { element.primaryActionKeys = []; changeActions.addPrimaryActionKey('foo'); assertArraysEqual(element.primaryActionKeys, ['foo']); @@ -89,34 +89,34 @@ assertArraysEqual(element.primaryActionKeys, []); }); - test('action buttons', function(done) { - var key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); - var handler = sinon.spy(); + test('action buttons', done => { + const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + const handler = sinon.spy(); changeActions.addTapListener(key, handler); - flush(function() { + flush(() => { MockInteractions.tap(element.$$('[data-action-key="' + key + '"]')); assert(handler.calledOnce); changeActions.removeTapListener(key, handler); MockInteractions.tap(element.$$('[data-action-key="' + key + '"]')); assert(handler.calledOnce); changeActions.remove(key); - flush(function() { + flush(() => { assert.isNull(element.$$('[data-action-key="' + key + '"]')); done(); }); }); }); - test('action button properties', function(done) { - var key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); - flush(function() { - var button = element.$$('[data-action-key="' + key + '"]'); + test('action button properties', done => { + const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + flush(() => { + const button = element.$$('[data-action-key="' + key + '"]'); assert.isOk(button); assert.equal(button.getAttribute('data-label'), 'Bork!'); assert.isNotOk(button.disabled); changeActions.setLabel(key, 'Yo'); changeActions.setEnabled(key, false); - flush(function() { + flush(() => { assert.equal(button.getAttribute('data-label'), 'Yo'); assert.isTrue(button.disabled); done(); @@ -124,20 +124,58 @@ }); }); - test('hide action buttons', function(done) { - var key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); - flush(function() { - var button = element.$$('[data-action-key="' + key + '"]'); + test('hide action buttons', done => { + const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + flush(() => { + const button = element.$$('[data-action-key="' + key + '"]'); assert.isOk(button); assert.isFalse(button.hasAttribute('hidden')); - changeActions.setActionHidden(changeActions.ActionType.REVISION, key, - true); - flush(function() { - var button = element.$$('[data-action-key="' + key + '"]'); + changeActions.setActionHidden( + changeActions.ActionType.REVISION, key, true); + flush(() => { + const button = element.$$('[data-action-key="' + key + '"]'); assert.isNotOk(button); done(); }); }); }); + + test('move action button to overflow', done => { + const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + flush(() => { + assert.isTrue(element.$.moreActions.hidden); + assert.isOk(element.$$('[data-action-key="' + key + '"]')); + changeActions.setActionOverflow( + changeActions.ActionType.REVISION, key, true); + flush(() => { + assert.isNotOk(element.$$('[data-action-key="' + key + '"]')); + assert.isFalse(element.$.moreActions.hidden); + assert.strictEqual(element.$.moreActions.items[0].name, 'Bork!'); + done(); + }); + }); + }); + + test('change actions priority', done => { + const key1 = + changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + const key2 = + changeActions.add(changeActions.ActionType.CHANGE, 'Squanch?'); + flush(() => { + let buttons = + Polymer.dom(element.root).querySelectorAll('[data-action-key]'); + assert.equal(buttons[0].getAttribute('data-action-key'), key1); + assert.equal(buttons[1].getAttribute('data-action-key'), key2); + changeActions.setActionPriority( + changeActions.ActionType.REVISION, key1, 10); + flush(() => { + buttons = + Polymer.dom(element.root).querySelectorAll('[data-action-key]'); + assert.equal(buttons[0].getAttribute('data-action-key'), key2); + assert.equal(buttons[1].getAttribute('data-action-key'), key1); + done(); + }); + }); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js index 9d6b83b..796c847 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
@@ -14,17 +14,67 @@ (function(window) { 'use strict'; - function GrChangeReplyInterface(el) { + /** + * @deprecated + */ + function GrChangeReplyInterfaceOld(el) { this._el = el; } - GrChangeReplyInterface.prototype.setLabelValue = function(label, value) { + GrChangeReplyInterfaceOld.prototype.getLabelValue = function(label) { + return this._el.getLabelValue(label); + }; + + GrChangeReplyInterfaceOld.prototype.setLabelValue = function(label, value) { this._el.setLabelValue(label, value); }; - GrChangeReplyInterface.prototype.send = function() { - return this._el.send(); + GrChangeReplyInterfaceOld.prototype.send = function(opt_includeComments) { + return this._el.send(opt_includeComments); }; + function GrChangeReplyInterface(plugin, el) { + GrChangeReplyInterfaceOld.call(this, el); + this.plugin = plugin; + this._hookName = (plugin.getPluginName() || 'test') + '-autogenerated-' + + String(Math.random()).split('.')[1]; + } + GrChangeReplyInterface.prototype._hookName = ''; + GrChangeReplyInterface.prototype._hookClass = null; + GrChangeReplyInterface.prototype._hookPromise = null; + + GrChangeReplyInterface.prototype = + Object.create(GrChangeReplyInterfaceOld.prototype); + GrChangeReplyInterface.prototype.constructor = GrChangeReplyInterface; + + GrChangeReplyInterface.prototype.getDomHook = function() { + if (!this._hookPromise) { + this._hookPromise = new Promise((resolve, reject) => { + this._hookClass = Polymer({ + is: this._hookName, + properties: { + plugin: Object, + content: Object, + }, + attached() { + resolve(this); + }, + }); + this.plugin.registerCustomComponent('reply-text', this._hookName); + }); + } + return this._hookPromise; + }; + + GrChangeReplyInterface.prototype.addReplyTextChangedCallback = + function(handler) { + this.getDomHook().then(el => { + if (!el.content) { return; } + el.content.addEventListener('value-changed', e => { + handler(e.detail.value); + }); + }); + }; + window.GrChangeReplyInterface = GrChangeReplyInterface; })(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html index d7d5cfe..2f67035 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
@@ -20,12 +20,11 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> +<link rel="import" href="../../../test/common-test-setup.html"/> <!-- - This must refer to the element this interface is wrapping around. Otherwise - breaking changes to gr-reply-dialog won’t be noticed. - --> +This must refer to the element this interface is wrapping around. Otherwise +breaking changes to gr-reply-dialog won’t be noticed. +--> <link rel="import" href="../../change/gr-reply-dialog/gr-reply-dialog.html"> <script>void(0);</script> @@ -37,37 +36,41 @@ </test-fixture> <script> - suite('gr-change-reply-js-api tests', function() { - var element; - var sandbox; - var changeReply; + suite('gr-change-reply-js-api tests', () => { + let element; + let sandbox; + let changeReply; - setup(function() { + setup(() => { stub('gr-rest-api-interface', { - getConfig: function() { return Promise.resolve({}); }, - getAccount: function() { return Promise.resolve(null); }, + getConfig() { return Promise.resolve({}); }, + getAccount() { return Promise.resolve(null); }, }); element = fixture('basic'); sandbox = sinon.sandbox.create(); - var plugin; - Gerrit.install(function(p) { plugin = p; }, '0.1', + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', 'http://test.com/plugins/testplugin/static/test.js'); changeReply = plugin.changeReply(); }); - teardown(function() { + teardown(() => { changeReply = null; sandbox.restore(); }); - test('calls', function() { - var setLabelValueStub = sinon.stub(element, 'setLabelValue'); - changeReply.setLabelValue('My-Label', '+1337'); - assert(setLabelValueStub.calledWithExactly('My-Label', '+1337')); + test('calls', () => { + sandbox.stub(element, 'getLabelValue').returns('+123'); + assert.equal(changeReply.getLabelValue('My-Label'), '+123'); - var sendStub = sinon.stub(element, 'send'); - changeReply.send(); - assert(sendStub.calledWithExactly()); + sandbox.stub(element, 'setLabelValue'); + changeReply.setLabelValue('My-Label', '+1337'); + assert.isTrue( + element.setLabelValue.calledWithExactly('My-Label', '+1337')); + + sandbox.stub(element, 'send'); + changeReply.send(false); + assert.isTrue(element.send.calledWithExactly(false)); }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html index 5c0535b..3cda49b 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -15,10 +15,10 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../core/gr-reporting/gr-reporting.html"> +<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> <dom-module id="gr-js-api-interface"> - <template></template> <script src="gr-change-actions-js-api.js"></script> <script src="gr-change-reply-js-api.js"></script> <script src="gr-js-api-interface.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js index 34ca728..70a9bf5 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -14,7 +14,7 @@ (function() { 'use strict'; - var EventType = { + const EventType = { HISTORY: 'history', LABEL_CHANGE: 'labelchange', SHOW_CHANGE: 'showchange', @@ -25,7 +25,7 @@ POST_REVERT: 'postrevert', }; - var Element = { + const Element = { CHANGE_ACTIONS: 'changeactions', REPLY_DIALOG: 'replydialog', }; @@ -44,11 +44,11 @@ }, }, - Element: Element, - EventType: EventType, + Element, + EventType, - handleEvent: function(type, detail) { - Gerrit.awaitPluginsLoaded().then(function() { + handleEvent(type, detail) { + Gerrit.awaitPluginsLoaded().then(() => { switch (type) { case EventType.HISTORY: this._handleHistory(detail); @@ -67,27 +67,27 @@ type); break; } - }.bind(this)); + }); }, - addElement: function(key, el) { + addElement(key, el) { this._elements[key] = el; }, - getElement: function(key) { + getElement(key) { return this._elements[key]; }, - addEventCallback: function(eventName, callback) { + addEventCallback(eventName, callback) { if (!this._eventCallbacks[eventName]) { this._eventCallbacks[eventName] = []; } this._eventCallbacks[eventName].push(callback); }, - canSubmitChange: function(change, revision) { - var submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE); - var cancelSubmit = submitCallbacks.some(function(callback) { + canSubmitChange(change, revision) { + const submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE); + const cancelSubmit = submitCallbacks.some(callback => { try { return callback(change, revision) === false; } catch (err) { @@ -99,28 +99,29 @@ return !cancelSubmit; }, - _removeEventCallbacks: function() { - for (var k in EventType) { + _removeEventCallbacks() { + for (const k in EventType) { + if (!EventType.hasOwnProperty(k)) { continue; } this._eventCallbacks[EventType[k]] = []; } }, - _handleHistory: function(detail) { - this._getEventCallbacks(EventType.HISTORY).forEach(function(cb) { + _handleHistory(detail) { + for (const cb of this._getEventCallbacks(EventType.HISTORY)) { try { cb(detail.path); } catch (err) { console.error(err); } - }); + } }, - _handleShowChange: function(detail) { - this._getEventCallbacks(EventType.SHOW_CHANGE).forEach(function(cb) { - var change = detail.change; - var patchNum = detail.patchNum; - var revision; - for (var rev in change.revisions) { + _handleShowChange(detail) { + for (const cb of this._getEventCallbacks(EventType.SHOW_CHANGE)) { + const change = detail.change; + const patchNum = detail.patchNum; + let revision; + for (const rev in change.revisions) { if (change.revisions[rev]._number == patchNum) { revision = change.revisions[rev]; break; @@ -131,67 +132,63 @@ } catch (err) { console.error(err); } - }); + } }, - handleCommitMessage: function(change, msg) { - this._getEventCallbacks(EventType.COMMIT_MSG_EDIT).forEach( - function(cb) { - try { - cb(change, msg); - } catch (err) { - console.error(err); - } - } - ); + handleCommitMessage(change, msg) { + for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) { + try { + cb(change, msg); + } catch (err) { + console.error(err); + } + } }, - _handleComment: function(detail) { - this._getEventCallbacks(EventType.COMMENT).forEach(function(cb) { + _handleComment(detail) { + for (const cb of this._getEventCallbacks(EventType.COMMENT)) { try { cb(detail.node); } catch (err) { console.error(err); } - }); + } }, - _handleLabelChange: function(detail) { - this._getEventCallbacks(EventType.LABEL_CHANGE).forEach(function(cb) { + _handleLabelChange(detail) { + for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) { try { cb(detail.change); } catch (err) { console.error(err); } - }); + } }, - modifyRevertMsg: function(change, revertMsg, origMsg) { - this._getEventCallbacks(EventType.REVERT).forEach(function(callback) { + modifyRevertMsg(change, revertMsg, origMsg) { + for (const cb of this._getEventCallbacks(EventType.REVERT)) { try { - revertMsg = callback(change, revertMsg, origMsg); + revertMsg = cb(change, revertMsg, origMsg); } catch (err) { console.error(err); } - }); + } return revertMsg; }, - getLabelValuesPostRevert: function(change) { - var labels = {}; - this._getEventCallbacks(EventType.POST_REVERT).forEach( - function(callback) { - try { - labels = callback(change); - } catch (err) { - console.error(err); - } - } - ); + getLabelValuesPostRevert(change) { + let labels = {}; + for (const cb of this._getEventCallbacks(EventType.POST_REVERT)) { + try { + labels = cb(change); + } catch (err) { + console.error(err); + } + } return labels; }, - _getEventCallbacks: function(type) { + _getEventCallbacks(type) { return this._eventCallbacks[type] || []; }, });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html index d62bdd8..1ff08ea 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -20,6 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-js-api-interface.html"> <script>void(0);</script> @@ -31,45 +32,97 @@ </test-fixture> <script> - suite('gr-js-api-interface tests', function() { - var element; - var plugin; - var errorStub; - var sandbox; + suite('gr-js-api-interface tests', () => { + let element; + let plugin; + let errorStub; + let sandbox; + let getResponseObjectStub; + let sendStub; - var throwErrFn = function() { + const throwErrFn = function() { throw Error('Unfortunately, this handler has stopped'); }; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); + getResponseObjectStub = sandbox.stub().returns(Promise.resolve()); + sendStub = sandbox.stub().returns(Promise.resolve({status: 200})); stub('gr-rest-api-interface', { - getAccount: function() { + getAccount() { return Promise.resolve({name: 'Judy Hopps'}); }, + getResponseObject: getResponseObjectStub, + send(...args) { + return sendStub(...args); + }, }); element = fixture('basic'); errorStub = sandbox.stub(console, 'error'); Gerrit._setPluginsCount(1); - Gerrit.install(function(p) { plugin = p; }, '0.1', + Gerrit.install(p => { plugin = p; }, '0.1', 'http://test.com/plugins/testplugin/static/test.js'); }); - teardown(function() { + teardown(() => { sandbox.restore(); element._removeEventCallbacks(); plugin = null; }); - test('url', function() { + test('url', () => { assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/'); assert.equal(plugin.url('/static/test.js'), 'http://test.com/plugins/testplugin/static/test.js'); }); - test('history event', function(done) { + test('_send on failure rejects with response text', () => { + sendStub.returns(Promise.resolve( + {status: 400, text() {return Promise.resolve('text');}})); + return plugin._send().catch(r => { + assert.equal(r, 'text'); + }); + }); + + test('_send on failure without text rejects with code', () => { + sendStub.returns(Promise.resolve( + {status: 400, text() {return Promise.resolve(null);}})); + return plugin._send().catch(r => { + assert.equal(r, '400'); + }); + }); + + test('get', () => { + const response = {foo: 'foo'}; + getResponseObjectStub.returns(Promise.resolve(response)); + return plugin.get('/url', r => { + assert.isTrue(sendStub.calledWith('GET', '/url')); + assert.strictEqual(r, response); + }); + }); + + test('get using Promise', () => { + const response = {foo: 'foo'}; + getResponseObjectStub.returns(Promise.resolve(response)); + return plugin.get('/url', r => 'rubbish').then(r => { + assert.isTrue(sendStub.calledWith('GET', '/url')); + assert.strictEqual(r, response); + }); + }); + + test('post', () => { + const payload = {foo: 'foo'}; + const response = {bar: 'bar'}; + getResponseObjectStub.returns(Promise.resolve(response)); + return plugin.post('/url', payload, r => { + assert.isTrue(sendStub.calledWith('POST', '/url', payload)); + assert.strictEqual(r, response); + }); + }); + + test('history event', done => { plugin.on(element.EventType.HISTORY, throwErrFn); - plugin.on(element.EventType.HISTORY, function(path) { + plugin.on(element.EventType.HISTORY, path => { assert.equal(path, '/path/to/awesomesauce'); assert.isTrue(errorStub.calledOnce); done(); @@ -78,13 +131,13 @@ {path: '/path/to/awesomesauce'}); }); - test('showchange event', function(done) { - var testChange = { + test('showchange event', done => { + const testChange = { _number: 42, revisions: {def: {_number: 2}, abc: {_number: 1}}, }; plugin.on(element.EventType.SHOW_CHANGE, throwErrFn); - plugin.on(element.EventType.SHOW_CHANGE, function(change, revision) { + plugin.on(element.EventType.SHOW_CHANGE, (change, revision) => { assert.deepEqual(change, testChange); assert.deepEqual(revision, testChange.revisions.abc); assert.isTrue(errorStub.calledOnce); @@ -94,28 +147,28 @@ {change: testChange, patchNum: 1}); }); - test('handleEvent awaits plugins load', function(done) { - var testChange = { + test('handleEvent awaits plugins load', done => { + const testChange = { _number: 42, revisions: {def: {_number: 2}, abc: {_number: 1}}, }; - var spy = sandbox.spy(); + const spy = sandbox.spy(); Gerrit._setPluginsCount(1); plugin.on(element.EventType.SHOW_CHANGE, spy); element.handleEvent(element.EventType.SHOW_CHANGE, {change: testChange, patchNum: 1}); assert.isFalse(spy.called); Gerrit._setPluginsCount(0); - flush(function() { + flush(() => { assert.isTrue(spy.called); done(); }); }); - test('comment event', function(done) { - var testCommentNode = {foo: 'bar'}; + test('comment event', done => { + const testCommentNode = {foo: 'bar'}; plugin.on(element.EventType.COMMENT, throwErrFn); - plugin.on(element.EventType.COMMENT, function(commentNode) { + plugin.on(element.EventType.COMMENT, commentNode => { assert.deepEqual(commentNode, testCommentNode); assert.isTrue(errorStub.calledOnce); done(); @@ -123,7 +176,7 @@ element.handleEvent(element.EventType.COMMENT, {node: testCommentNode}); }); - test('revert event', function() { + test('revert event', () => { function appendToRevertMsg(c, revertMsg, originalMsg) { return revertMsg + '\n' + originalMsg.replace(/^/gm, '> ') + '\ninfo'; } @@ -134,16 +187,16 @@ plugin.on(element.EventType.REVERT, throwErrFn); plugin.on(element.EventType.REVERT, appendToRevertMsg); assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), - 'test\n> origTest\ninfo'); + 'test\n> origTest\ninfo'); assert.isTrue(errorStub.calledOnce); plugin.on(element.EventType.REVERT, appendToRevertMsg); assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), - 'test\n> origTest\ninfo\n> origTest\ninfo'); + 'test\n> origTest\ninfo\n> origTest\ninfo'); assert.isTrue(errorStub.calledTwice); }); - test('postrevert event', function() { + test('postrevert event', () => { function getLabels(c) { return {'Code-Review': 1}; } @@ -158,10 +211,10 @@ assert.isTrue(errorStub.calledOnce); }); - test('commitmsgedit event', function(done) { - var testMsg = 'Test CL commit message'; + test('commitmsgedit event', done => { + const testMsg = 'Test CL commit message'; plugin.on(element.EventType.COMMIT_MSG_EDIT, throwErrFn); - plugin.on(element.EventType.COMMIT_MSG_EDIT, function(change, msg) { + plugin.on(element.EventType.COMMIT_MSG_EDIT, (change, msg) => { assert.deepEqual(msg, testMsg); assert.isTrue(errorStub.calledOnce); done(); @@ -169,10 +222,10 @@ element.handleCommitMessage(null, testMsg); }); - test('labelchange event', function(done) { - var testChange = {_number: 42}; + test('labelchange event', done => { + const testChange = {_number: 42}; plugin.on(element.EventType.LABEL_CHANGE, throwErrFn); - plugin.on(element.EventType.LABEL_CHANGE, function(change) { + plugin.on(element.EventType.LABEL_CHANGE, change => { assert.deepEqual(change, testChange); assert.isTrue(errorStub.calledOnce); done(); @@ -180,41 +233,41 @@ element.handleEvent(element.EventType.LABEL_CHANGE, {change: testChange}); }); - test('submitchange', function() { + test('submitchange', () => { plugin.on(element.EventType.SUBMIT_CHANGE, throwErrFn); - plugin.on(element.EventType.SUBMIT_CHANGE, function() { return true; }); + plugin.on(element.EventType.SUBMIT_CHANGE, () => { return true; }); assert.isTrue(element.canSubmitChange()); assert.isTrue(errorStub.calledOnce); - plugin.on(element.EventType.SUBMIT_CHANGE, function() { return false; }); - plugin.on(element.EventType.SUBMIT_CHANGE, function() { return true; }); + plugin.on(element.EventType.SUBMIT_CHANGE, () => { return false; }); + plugin.on(element.EventType.SUBMIT_CHANGE, () => { return true; }); assert.isFalse(element.canSubmitChange()); assert.isTrue(errorStub.calledTwice); }); - test('versioning', function() { - var callback = sandbox.spy(); + test('versioning', () => { + const callback = sandbox.spy(); Gerrit.install(callback, '0.0pre-alpha'); assert(callback.notCalled); }); - test('getAccount', function(done) { - Gerrit.getLoggedIn().then(function(loggedIn) { + test('getAccount', done => { + Gerrit.getLoggedIn().then(loggedIn => { assert.isTrue(loggedIn); done(); }); }); - test('_setPluginsCount', function(done) { + test('_setPluginsCount', done => { stub('gr-reporting', { - pluginsLoaded: function() { + pluginsLoaded() { assert.equal(Gerrit._pluginsPending, 0); done(); - } + }, }); Gerrit._setPluginsCount(0); }); - test('_arePluginsLoaded', function() { + test('_arePluginsLoaded', () => { assert.isTrue(Gerrit._arePluginsLoaded()); Gerrit._setPluginsCount(1); assert.isFalse(Gerrit._arePluginsLoaded()); @@ -222,12 +275,12 @@ assert.isTrue(Gerrit._arePluginsLoaded()); }); - test('_pluginInstalled', function(done) { + test('_pluginInstalled', done => { stub('gr-reporting', { - pluginsLoaded: function() { + pluginsLoaded() { assert.equal(Gerrit._pluginsPending, 0); done(); - } + }, }); Gerrit._setPluginsCount(2); Gerrit._pluginInstalled(); @@ -235,34 +288,34 @@ Gerrit._pluginInstalled(); }); - test('install calls _pluginInstalled', function() { + test('install calls _pluginInstalled', () => { sandbox.stub(Gerrit, '_pluginInstalled'); - Gerrit.install(function(p) { plugin = p; }, '0.1', + Gerrit.install(p => { plugin = p; }, '0.1', 'http://test.com/plugins/testplugin/static/test.js'); assert.isTrue(Gerrit._pluginInstalled.calledOnce); }); - test('install calls _pluginInstalled on error', function() { + test('install calls _pluginInstalled on error', () => { sandbox.stub(Gerrit, '_pluginInstalled'); - Gerrit.install(function() {}, '0.0pre-alpha'); + Gerrit.install(() => {}, '0.0pre-alpha'); assert.isTrue(Gerrit._pluginInstalled.calledOnce); }); - test('installGwt calls _pluginInstalled', function() { + test('installGwt calls _pluginInstalled', () => { sandbox.stub(Gerrit, '_pluginInstalled'); Gerrit.installGwt(); assert.isTrue(Gerrit._pluginInstalled.calledOnce); }); - test('installGwt returns a stub object', function() { - var plugin = Gerrit.installGwt(); + test('installGwt returns a stub object', () => { + const plugin = Gerrit.installGwt(); sandbox.stub(console, 'warn'); assert.isAbove(Object.keys(plugin).length, 0); - Object.keys(plugin).forEach(function(name) { + for (const name of Object.keys(plugin)) { console.warn.reset(); plugin[name](); assert.isTrue(console.warn.calledOnce); - }); + } }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js index b3ae649..912896f 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -14,23 +14,39 @@ (function(window) { 'use strict'; - var warnNotSupported = function(opt_name) { + const warnNotSupported = function(opt_name) { console.warn('Plugin API method ' + (opt_name || '') + ' is not supported'); }; - var stubbedMethods = ['_loadedGwt', 'screen', 'settingsScreen', 'panel']; - var GWT_PLUGIN_STUB = {}; - stubbedMethods.forEach(function(name) { + const stubbedMethods = ['_loadedGwt', 'screen', 'settingsScreen', 'panel']; + const GWT_PLUGIN_STUB = {}; + for (const name of stubbedMethods) { GWT_PLUGIN_STUB[name] = warnNotSupported.bind(null, name); - }); + } - var API_VERSION = '0.1'; + let _restAPI; + const getRestAPI = () => { + if (!_restAPI) { + _restAPI = document.createElement('gr-rest-api-interface'); + } + return _restAPI; + }; + + const API_VERSION = '0.1'; + + const EndpointType = { + STYLE: 'style', + DOM_DECORATION: 'dom', + }; // GWT JSNI uses $wnd to refer to window. // http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsJSNI.html window.$wnd = window; function Plugin(opt_url) { + this._generatedHookNames = []; + this._hooks = []; + if (!opt_url) { console.warn('Plugin not being loaded from /plugins base path.', 'Unable to determine name.'); @@ -38,7 +54,7 @@ } this._url = new URL(opt_url); - if (this._url.pathname.indexOf('/plugins') !== 0) { + if (!this._url.pathname.startsWith('/plugins')) { console.warn('Plugin not being loaded from /plugins base path:', this._url.href, '— Unable to determine name.'); return; @@ -54,6 +70,30 @@ return this._name; }; + Plugin.prototype.registerStyleModule = function(endpointName, moduleName) { + this._registerEndpointModule( + endpointName, EndpointType.STYLE, moduleName); + }; + + Plugin.prototype.registerCustomComponent = + function(endpointName, moduleName) { + this._registerEndpointModule( + endpointName, EndpointType.DOM_DECORATION, moduleName); + }; + + Plugin.prototype._registerEndpointModule = function(endpoint, type, module) { + const endpoints = Gerrit._endpoints; + if (!endpoints[endpoint]) { + endpoints[endpoint] = []; + } + endpoints[endpoint].push({ + moduleName: module, + plugin: this, + pluginUrl: this._url, + type, + }); + }; + Plugin.prototype.getServerInfo = function() { return document.createElement('gr-rest-api-interface').getConfig(); }; @@ -66,21 +106,82 @@ return this._url.origin + '/plugins/' + this._name + (opt_path || '/'); }; + Plugin.prototype._send = function(method, url, opt_callback, opt_payload) { + return getRestAPI().send(method, url, opt_payload).then(response => { + if (response.status < 200 || response.status >= 300) { + return response.text().then(text => { + if (text) { + return Promise.reject(text); + } else { + return Promise.reject(response.status); + } + }); + } else { + return getRestAPI().getResponseObject(response); + } + }).then(response => { + if (opt_callback) { + opt_callback(response); + } + return response; + }); + }; + + Plugin.prototype.get = function(url, opt_callback) { + return this._send('GET', url, opt_callback); + }, + + Plugin.prototype.post = function(url, payload, opt_callback) { + return this._send('POST', url, opt_callback, payload); + }, + Plugin.prototype.changeActions = function() { return new GrChangeActionsInterface(Plugin._sharedAPIElement.getElement( Plugin._sharedAPIElement.Element.CHANGE_ACTIONS)); }; Plugin.prototype.changeReply = function() { - return new GrChangeReplyInterface(Plugin._sharedAPIElement.getElement( - Plugin._sharedAPIElement.Element.REPLY_DIALOG)); + return new GrChangeReplyInterface(this, + Plugin._sharedAPIElement.getElement( + Plugin._sharedAPIElement.Element.REPLY_DIALOG)); }; - var Gerrit = window.Gerrit || {}; + Plugin.prototype._getGeneratedHookName = function(endpointName) { + if (!this._generatedHookNames[endpointName]) { + this._generatedHookNames[endpointName] = + (this.getPluginName() || 'test') + '-autogenerated-' + endpointName; + } + return this._generatedHookNames[endpointName]; + }; + + Plugin.prototype.getDomHook = function(endpointName) { + const hookName = this._getGeneratedHookName(endpointName); + if (!this._hooks[hookName]) { + this._hooks[hookName] = new Promise((resolve, reject) => { + Polymer({ + is: hookName, + properties: { + plugin: Object, + content: Object, + }, + attached() { + resolve(this); + }, + }); + this.registerCustomComponent(endpointName, hookName); + }); + } + return this._hooks[hookName]; + }; + + const Gerrit = window.Gerrit || {}; // Number of plugins to initialize, -1 means 'not yet known'. Gerrit._pluginsPending = -1; + // Hash of custom components to be instantiated for extension endpoints. + Gerrit._endpoints = {}; + Gerrit.getPluginName = function() { console.warn('Gerrit.getPluginName is not supported in PolyGerrit.', 'Please use self.getPluginName() instead.'); @@ -88,12 +189,13 @@ Gerrit.css = function(rulesStr) { if (!Gerrit._customStyleSheet) { - var styleEl = document.createElement('style'); + const styleEl = document.createElement('style'); document.head.appendChild(styleEl); Gerrit._customStyleSheet = styleEl.sheet; } - var name = '__pg_js_api_class_' + Gerrit._customStyleSheet.cssRules.length; + const name = '__pg_js_api_class_' + + Gerrit._customStyleSheet.cssRules.length; Gerrit._customStyleSheet.insertRule('.' + name + '{' + rulesStr + '}', 0); return name; }; @@ -107,8 +209,9 @@ } // TODO(andybons): Polyfill currentScript for IE10/11 (edge supports it). - var src = opt_src || (document.currentScript && document.currentScript.src); - var plugin = new Plugin(src); + const src = opt_src || (document.currentScript && + document.currentScript.src || document.currentScript.baseURI); + const plugin = new Plugin(src); try { callback(plugin); } catch (e) { @@ -140,7 +243,7 @@ if (Gerrit._arePluginsLoaded()) { Gerrit._allPluginsPromise = Promise.resolve(); } else { - Gerrit._allPluginsPromise = new Promise(function(resolve) { + Gerrit._allPluginsPromise = new Promise(resolve => { Gerrit._resolveAllPluginsLoaded = resolve; }); } @@ -166,5 +269,67 @@ return Gerrit._pluginsPending === 0; }; + /** + * Get detailed information about modules registered with an extension + * endpoint. + * @param {string} name Endpoint name. + * @param {?{ + * type: (string|undefined), + * moduleName: (string|undefined) + * }} opt_options + * @return {{ + * moduleName: string, + * plugin: Plugin, + * pluginUrl: String, + * type: EndpointType, + * }} + */ + Gerrit._getEndpointDetails = function(name, opt_options) { + const type = opt_options && opt_options.type; + const moduleName = opt_options && opt_options.moduleName; + if (!Gerrit._endpoints[name]) { + return []; + } + return Gerrit._endpoints[name] + .filter(item => (!type || item.type === type) && + (!moduleName || moduleName == item.moduleName)); + }; + + /** + * Get detailed module names for instantiating at the endpoint + * @param {string} name Endpoint name. + * @param {?{ + * type: (string|undefined), + * moduleName: (string|undefined) + * }} opt_options + * @return {!Array<string>} + */ + Gerrit._getModulesForEndoint = function(name, opt_options) { + const modulesData = Gerrit._getEndpointDetails(name, opt_options); + if (!modulesData.length) { + return []; + } + return modulesData.map(m => m.moduleName); + }; + + /** + * Get .html plugin URLs with element and module definitions. + * @param {string} name Endpoint name. + * @param {?{ + * type: (string|undefined), + * moduleName: (string|undefined) + * }} opt_options + * @return {!Array<!URL>} + */ + Gerrit._getPluginsForEndpoint = function(name, opt_options) { + const modulesData = + Gerrit._getEndpointDetails(name, opt_options).filter( + data => data.pluginUrl.pathname.indexOf('.html') !== -1); + if (!modulesData.length) { + return []; + } + return Array.from(new Set(modulesData.map(m => m.pluginUrl))); + }; + window.Gerrit = Gerrit; })(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html index 5828e7b..c4f7ee0 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.html
@@ -16,9 +16,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../gr-button/gr-button.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + <dom-module id="gr-linked-chip"> <template> - <style> + <style include="shared-styles"> :host { display: block; overflow: hidden;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js index c6a5e4e..bfba1c4 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
@@ -30,11 +30,11 @@ }, }, - _getBackgroundClass: function(transparent) { + _getBackgroundClass(transparent) { return transparent ? 'transparentBackground' : ''; }, - _handleRemoveTap: function(e) { + _handleRemoveTap(e) { e.preventDefault(); this.fire('remove'); },
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html index eefc79d..d707d10 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../bower_components/iron-test-helpers/mock-interactions.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-linked-chip.html"> <script>void(0);</script> @@ -34,21 +34,21 @@ </test-fixture> <script> - suite('gr-linked-chip tests', function() { - var element; - var sandbox; + suite('gr-linked-chip tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { element = fixture('basic'); sandbox = sinon.sandbox.create(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('remove fired', function() { - var spy = sandbox.spy(); + test('remove fired', () => { + const spy = sandbox.spy(); element.addEventListener('remove', spy); flushAsynchronousOperations(); MockInteractions.tap(element.$.remove);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html index 79db969..a0d9233 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.html
@@ -15,11 +15,13 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + <script src="ba-linkify.js"></script> <script src="link-text-parser.js"></script> <dom-module id="gr-linked-text"> <template> - <style> + <style include="shared-styles"> :host { display: block; }
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html index 807278d..0042049 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -20,6 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> <link rel="import" href="gr-linked-text.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html new file mode 100644 index 0000000..b471d55 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.html
@@ -0,0 +1,72 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<link rel="import" href="./gr-styled-table.html"> +<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> +<link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> + +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-list-view"> + <template> + <style include="shared-styles"> + #filterContainer { + margin: 1em; + } + #filter { + font-size: 1em; + max-width: 25em; + } + a { + color: var(--default-text-color); + text-decoration: none; + } + a:hover { + text-decoration: underline; + } + nav { + padding: .5em 0; + text-align: center; + } + nav a { + display: inline-block; + } + nav a:first-of-type { + margin-right: .5em; + } + </style> + <div id="filterContainer"> + <label>Filter:</label> + <input is="iron-input" + type="text" + id="filter" + bind-value="{{_filter}}"> + </div> + <gr-styled-table> + <content></content> + </gr-styled-table> + <nav> + <a id="prevArrow" + href$="[[_computeNavLink(offset, -1, itemsPerPage, filter)]]" + hidden$="[[_hidePrevArrow(offset)]]" hidden>← Prev</a> + <a id="nextArrow" + href$="[[_computeNavLink(offset, 1, itemsPerPage, filter)]]" + hidden$="[[_hideNextArrow(loading, items)]]" hidden> + Next →</a> + </nav> + </template> + <script src="gr-list-view.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js new file mode 100644 index 0000000..4a45352 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -0,0 +1,84 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + const REQUEST_DEBOUNCE_INTERVAL_MS = 200; + + Polymer({ + is: 'gr-list-view', + + properties: { + items: Array, + itemsPerPage: Number, + _filter: { + type: String, + observer: '_filterChanged', + }, + offset: Number, + loading: Boolean, + path: String, + }, + + behaviors: [ + Gerrit.BaseUrlBehavior, + Gerrit.URLEncodingBehavior, + ], + + listeners: { + 'next-page': '_handleNextPage', + 'previous-page': '_handlePreviousPage', + }, + + detached() { + this.cancelDebouncer('reload'); + }, + + _filterChanged(filter) { + this.debounce('reload', () => { + if (filter) { + return page.show(`${this.path}/q/filter:` + + this.encodeURL(filter, false)); + } + page.show(this.path); + }, REQUEST_DEBOUNCE_INTERVAL_MS); + }, + + _computeNavLink(offset, direction, itemsPerPage, filter) { + // Offset could be a string when passed from the router. + offset = +(offset || 0); + const newOffset = Math.max(0, offset + (itemsPerPage * direction)); + let href = this.getBaseUrl() + this.path; + if (filter) { + href += '/q/filter:' + filter; + } + if (newOffset > 0) { + href += ',' + newOffset; + } + return href; + }, + + _hidePrevArrow(offset) { + return offset === 0; + }, + + _hideNextArrow(loading, items) { + let lastPage = false; + if (items.length < this.itemsPerPage + 1) { + lastPage = true; + } + return loading || lastPage || !items || !items.length; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html new file mode 100644 index 0000000..e4dc288 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
@@ -0,0 +1,111 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-list-view</title> +<script src="../../../bower_components/page/page.js"></script> +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-list-view.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-list-view></gr-list-view> + </template> +</test-fixture> + +<script> + suite('gr-list-view tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('_computeNavLink', () => { + const offset = 25; + const projectsPerPage = 25; + const filter = 'test'; + element.path = '/admin/projects'; + + sandbox.stub(element, 'getBaseUrl', () => ''); + + assert.equal( + element._computeNavLink(offset, 1, projectsPerPage, filter), + '/admin/projects/q/filter:test,50'); + + assert.equal( + element._computeNavLink(offset, -1, projectsPerPage, filter), + '/admin/projects/q/filter:test'); + + assert.equal( + element._computeNavLink(offset, 1, projectsPerPage, null), + '/admin/projects,50'); + + assert.equal( + element._computeNavLink(offset, -1, projectsPerPage, null), + '/admin/projects'); + }); + + test('_onValueChange', done => { + element.path = '/admin/projects'; + sandbox.stub(page, 'show', url => { + assert.equal(url, '/admin/projects/q/filter:test'); + done(); + }); + element._filterChanged('test'); + }); + + test('next button', done => { + element.itemsPerPage = 25; + projects = new Array(26); + + flush(() => { + let loading; + assert.isFalse(element._hideNextArrow(loading, projects)); + loading = true; + assert.isTrue(element._hideNextArrow(loading, projects)); + loading = false; + assert.isFalse(element._hideNextArrow(loading, projects)); + element._projects = []; + assert.isTrue(element._hideNextArrow(loading, element._projects)); + projects = new Array(4); + assert.isTrue(element._hideNextArrow(loading, projects)); + done(); + }); + }); + + test('prev button', () => { + flush(() => { + let offset = 0; + assert.isTrue(element._hidePrevArrow(offset)); + offset = 5; + assert.isFalse(element._hidePrevArrow(offset)); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-styled-table.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-styled-table.html new file mode 100644 index 0000000..273b005f --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-styled-table.html
@@ -0,0 +1,77 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-styled-table"> + <template> + <style include="shared-styles"> + ::content { + display: flex; + flex-direction: column; + } + ::content .loading { + display: none; + } + ::content #list { + border-collapse: collapse; + width: 100%; + } + ::content #list tr.table { + border-bottom: 1px solid #eee; + } + ::content #list td { + flex-shrink: 0; + padding: .3em .5em; + } + ::content #list th { + background-color: #ddd; + border-bottom: 1px solid #eee; + font-weight: bold; + padding: .3em .5em; + text-align: left; + } + ::content #list a { + color: var(--default-text-color); + text-decoration: none; + } + ::content #list a:hover { + text-decoration: underline; + } + ::content #list .description { + width: 70%; + } + ::content #list .loadingMsg { + color: #666; + display: block; + padding: 1em var(--default-horizontal-margin); + } + ::content #list .loadingMsg:not(.loading) { + display: none; + } + </style> + <content></content> + </template> + <script> + (function() { + 'use strict'; + Polymer({ + is: 'gr-styled-table', + }); + })(); + </script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html index 9aa80b5..419d2f7 100644 --- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html +++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.html
@@ -16,10 +16,11 @@ <link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/iron-overlay-behavior/iron-overlay-behavior.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-overlay"> <template> - <style> + <style include="shared-styles"> :host { background: #fff; box-shadow: rgba(0, 0, 0, 0.3) 0 1px 3px;
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js index 9f271ed..8db8004 100644 --- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js +++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -14,8 +14,8 @@ (function() { 'use strict'; - var AWAIT_MAX_ITERS = 10; - var AWAIT_STEP = 5; + const AWAIT_MAX_ITERS = 10; + const AWAIT_STEP = 5; Polymer({ is: 'gr-overlay', @@ -24,17 +24,17 @@ Polymer.IronOverlayBehavior, ], - open: function() { - return new Promise(function(resolve) { - Polymer.IronOverlayBehaviorImpl.open.apply(this, arguments); + open(...args) { + return new Promise(resolve => { + Polymer.IronOverlayBehaviorImpl.open.apply(this, args); this._awaitOpen(resolve); - }.bind(this)); + }); }, /** * Override the focus stops that iron-overlay-behavior tries to find. */ - setFocusStops: function(stops) { + setFocusStops(stops) { this.__firstFocusableNode = stops.start; this.__lastFocusableNode = stops.end; }, @@ -43,21 +43,21 @@ * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually * opening. Eventually replace with a direct way to listen to the overlay. */ - _awaitOpen: function(fn) { - var iters = 0; - var step = function() { - this.async(function() { + _awaitOpen(fn) { + let iters = 0; + const step = () => { + this.async(() => { if (this.style.display !== 'none') { fn.call(this); } else if (iters++ < AWAIT_MAX_ITERS) { step.call(this); } - }.bind(this), AWAIT_STEP); - }.bind(this); + }, AWAIT_STEP); + }; step.call(this); }, - _id: function() { + _id() { return this.getAttribute('id') || 'global'; }, });
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html new file mode 100644 index 0000000..3f954b4 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.html
@@ -0,0 +1,85 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-page-nav"> + <template> + <style include="shared-styles"> + #nav { + background-color: #f5f5f5; + border: 1px solid #eee; + border-top: none; + height: 100%; + position: absolute; + top: 0; + width: 14em; + } + #nav.pinned { + position: fixed; + } + #nav ::content ul { + padding: 1em 0; + } + #nav ::content li { + border-bottom: 1px solid transparent; + border-top: 1px solid transparent; + padding: 0 2em; + } + #nav ::content li a { + word-break: break-all; + } + #nav ::content .subsectionItem { + padding-left: 3em; + } + #nav ::content .hideSubsection { + display: none; + } + #nav ::content li.sectionTitle { + padding: 0 2em 0 1.5em; + } + #nav ::content li.sectionTitle:not(:first-child) { + margin-top: 1em; + } + #nav ::content .title { + display: flex; + font-weight: bold; + margin: .4em 0; + } + #nav ::content .selected { + background-color: #fff; + border-bottom: 1px dotted #808080; + border-top: 1px dotted #808080; + font-weight: bold; + } + #nav ::content a { + color: black; + display: inline-block; + margin: .4em 0; + } + @media only screen and (max-width: 53em) { + #nav { + display: none; + } + } + </style> + <nav id="nav"> + <content></content> + </nav> + </template> + <script src="gr-page-nav.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js new file mode 100644 index 0000000..6d7f979 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
@@ -0,0 +1,63 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-page-nav', + + properties: { + _headerHeight: Number, + }, + + attached() { + this.listen(window, 'scroll', '_handleBodyScroll'); + }, + + detached() { + this.unlisten(window, 'scroll', '_handleBodyScroll'); + }, + + _handleBodyScroll() { + if (this._headerHeight === undefined) { + let top = this._getOffsetTop(this); + // Don't want to include the element that wraps around the nav, start + // with its parent. + for (let offsetParent = this._getOffsetParent(this.offsetParent); + offsetParent; + offsetParent = this._getOffsetParent(offsetParent)) { + top += this._getOffsetTop(offsetParent); + } + this._headerHeight = top; + } + + this.$.nav.classList.toggle('pinned', + this._getScrollY() >= this._headerHeight); + }, + + /* Functions used for test purposes */ + _getOffsetParent(element) { + if (!element.offsetParent) { return ''; } + return element.offsetParent; + }, + + _getOffsetTop(element) { + return element.offsetTop; + }, + + _getScrollY() { + return window.scrollY; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html new file mode 100644 index 0000000..7e426d7 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
@@ -0,0 +1,89 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-page-nav</title> + +<script src="../../../bower_components/page/page.js"></script> +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="../../../test/common-test-setup.html"/> + +<link rel="import" href="gr-page-nav.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-page-nav> + <ul> + <li>item</li> + </ul> + </gr-page-nav> + </template> +</test-fixture> + +<script> + suite('gr-page-nav tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + flushAsynchronousOperations(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('header is not pinned just below top', () => { + sandbox.stub(element, '_getOffsetParent', () => 0); + sandbox.stub(element, '_getOffsetTop', () => 10); + sandbox.stub(element, '_getScrollY', () => 5); + element._handleBodyScroll(); + assert.isFalse(element.$.nav.classList.contains('pinned')); + }); + + test('header is pinned when scroll down the page', () => { + sandbox.stub(element, '_getOffsetParent', () => 0); + sandbox.stub(element, '_getOffsetTop', () => 10); + sandbox.stub(element, '_getScrollY', () => 25); + window.scrollY = 100; + element._handleBodyScroll(); + assert.isTrue(element.$.nav.classList.contains('pinned')); + }); + + test('header is not pinned just below top with header set', () => { + element._headerHeight = 20; + sandbox.stub(element, '_getScrollY', () => 15); + window.scrollY = 100; + element._handleBodyScroll(); + assert.isFalse(element.$.nav.classList.contains('pinned')); + }); + + test('header is pinned when scroll down the page with header set', () => { + element._headerHeight = 20; + sandbox.stub(element, '_getScrollY', () => 25); + window.scrollY = 100; + element._handleBodyScroll(); + assert.isTrue(element.$.nav.classList.contains('pinned')); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.html b/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.html index 2c8be31a..15f44cf 100644 --- a/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.html +++ b/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.html
@@ -16,10 +16,11 @@ <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-placeholder"> <template> - <style> + <style include="shared-styles"> main { margin: 2em auto; max-width: 46em;
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth.js new file mode 100644 index 0000000..f4e66a3 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth.js
@@ -0,0 +1,194 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function(window) { + 'use strict'; + + // Prevent redefinition. + if (window.GrGapiAuth) { return; } + + const EMAIL_SCOPE = 'email'; + + function GrGapiAuth() {} + + GrGapiAuth._loadGapiPromise = null; + GrGapiAuth._setupPromise = null; + GrGapiAuth._refreshTokenPromise = null; + GrGapiAuth._sharedAuthToken = null; + GrGapiAuth._oauthClientId = null; + GrGapiAuth._oauthEmail = null; + + GrGapiAuth.prototype.fetch = function(url, options) { + options = Object.assign({}, options); + return this._getAccessToken().then( + token => window.FASTER_GERRIT_CORS ? + this._fasterGerritCors(url, options, token) : + this._defaultFetch(url, options, token) + ); + }; + + GrGapiAuth.prototype._defaultFetch = function(url, options, token) { + if (token) { + options.headers = options.headers || new Headers(); + options.headers.append('Authorization', `Bearer ${token}`); + if (!url.startsWith('/a/')) { + url = '/a' + url; + } + } + return fetch(url, options); + }; + + GrGapiAuth.prototype._fasterGerritCors = function(url, options, token) { + const method = options.method || 'GET'; + if (method === 'GET') { + return fetch(url, options); + } + const params = []; + if (token) { + params.push(`access_token=${token}`); + } + const contentType = options.headers && options.headers.get('Content-Type'); + if (contentType) { + options.headers.set('Content-Type', 'text/plain'); + params.push(`$ct=${encodeURIComponent(contentType)}`); + } + params.push(`$m=${method}`); + url = url + (url.indexOf('?') === -1 ? '?' : '') + params.join('&'); + options.method = 'POST'; + return fetch(url, options); + }; + + GrGapiAuth.prototype._getAccessToken = function() { + if (this._isTokenValid(GrGapiAuth._sharedAuthToken)) { + return Promise.resolve(GrGapiAuth._sharedAuthToken.access_token); + } + if (!GrGapiAuth._refreshTokenPromise) { + GrGapiAuth._refreshTokenPromise = this._loadGapi() + .then(() => this._configureOAuthLibrary()) + .then(() => this._refreshToken()) + .then(token => { + GrGapiAuth._sharedAuthToken = token; + GrGapiAuth._refreshTokenPromise = null; + return this._getAccessToken(); + }).catch(err => { + console.error(err); + }); + } + return GrGapiAuth._refreshTokenPromise; + }; + + GrGapiAuth.prototype._isTokenValid = function(token) { + if (!token) { return false; } + if (!token.access_token || !token.expires_at) { return false; } + + const expiration = new Date(parseInt(token.expires_at, 10) * 1000); + if (Date.now() >= expiration) { return false; } + + return true; + }; + + GrGapiAuth.prototype._loadGapi = function() { + if (!GrGapiAuth._loadGapiPromise) { + GrGapiAuth._loadGapiPromise = new Promise((resolve, reject) => { + const scriptEl = document.createElement('script'); + scriptEl.defer = true; + scriptEl.async = true; + scriptEl.src = 'https://apis.google.com/js/platform.js'; + scriptEl.onerror = reject; + scriptEl.onload = resolve; + document.body.appendChild(scriptEl); + }); + } + return GrGapiAuth._loadGapiPromise; + }; + + GrGapiAuth.prototype._configureOAuthLibrary = function() { + if (!GrGapiAuth._setupPromise) { + GrGapiAuth._setupPromise = new Promise( + resolve => gapi.load('config_min', resolve) + ) + .then(() => this._getOAuthConfig()) + .then(config => { + if (config.hasOwnProperty('auth_url') && config.auth_url) { + gapi.config.update('oauth-flow/authUrl', config.auth_url); + } + if (config.hasOwnProperty('proxy_url') && config.proxy_url) { + gapi.config.update('oauth-flow/proxyUrl', config.proxy_url); + } + GrGapiAuth._oauthClientId = config.client_id; + GrGapiAuth._oauthEmail = config.email; + + // Loading auth has a side-effect. The URLs should be set before + // loading it. + return new Promise( + resolve => gapi.load('auth', () => gapi.auth.init(resolve)) + ); + }); + } + return GrGapiAuth._setupPromise; + }; + + GrGapiAuth.prototype._refreshToken = function() { + const opts = { + client_id: GrGapiAuth._oauthClientId, + immediate: true, + scope: EMAIL_SCOPE, + login_hint: GrGapiAuth._oauthEmail, + }; + return new Promise((resolve, reject) => { + gapi.auth.authorize(opts, token => { + if (!token) { + reject('No token returned'); + } else if (token.error) { + reject(token.error); + } else { + resolve(token); + } + }); + }); + }; + + GrGapiAuth.prototype._getOAuthConfig = function() { + const baseUrl = Gerrit.BaseUrlBehavior.getBaseUrl(); + const authConfigURL = baseUrl + '/accounts/self/oauthconfig'; + const opts = { + headers: new Headers({Accept: 'application/json'}), + credentials: 'same-origin', + }; + return fetch(authConfigURL, opts).then(response => { + if (!response.ok) { + console.error(response.statusText); + if (response.body && response.body.then) { + return response.body.then(text => { + return Promise.reject(text); + }); + } + if (response.statusText) { + return Promise.reject(response.statusText); + } else { + return Promise.reject('_getOAuthConfig' + response.status); + } + } + return this._getResponseObject(response); + }); + }; + + GrGapiAuth.prototype._getResponseObject = function(response) { + const JSON_PREFIX = ')]}\''; + return response.text().then(text => { + return JSON.parse(text.substring(JSON_PREFIX.length)); + }); + }, + + window.GrGapiAuth = GrGapiAuth; +})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth_test.html new file mode 100644 index 0000000..70cdd83 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gapi-auth_test.html
@@ -0,0 +1,208 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-gapi-auth</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> + +<script src="gr-gapi-auth.js"></script> + +<script> + suite('gr-rest-api-interface tests', () => { + let auth; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + auth = new GrGapiAuth(); + window.gapi = { + load: sandbox.stub().callsArg(1), + config: { + update: sandbox.stub(), + }, + auth: { + init: sandbox.stub().callsArg(0), + authorize: sandbox.stub(), + }, + }; + sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true})); + }); + + teardown(() => { + delete window.gapi; + sandbox.restore(); + }); + + test('exists', () => { + assert.isOk(auth); + }); + + test('fetch signed in', () => { + sandbox.stub(auth, '_getAccessToken').returns(Promise.resolve('foo')); + return auth.fetch('/url', {bar: 'bar'}).then(() => { + assert.isTrue(auth._getAccessToken.called); + const [url, options] = fetch.lastCall.args; + assert.equal(url, '/a/url'); + assert.equal(options.bar, 'bar'); + assert.equal(options.headers.get('Authorization'), 'Bearer foo'); + }); + }); + + test('fetch not signed in', () => { + sandbox.stub(auth, '_getAccessToken').returns(Promise.resolve()); + return auth.fetch('/url', {bar: 'bar'}).then(() => { + assert.isTrue(auth._getAccessToken.called); + const [url, options] = fetch.lastCall.args; + assert.equal(url, '/url'); + assert.equal(options.bar, 'bar'); + assert.isUndefined(options.headers); + }); + }); + + test('_getAccessToken returns valid shared token', () => { + GrGapiAuth._sharedAuthToken = {access_token: 'foo'}; + sandbox.stub(auth, '_isTokenValid').returns(true); + return auth._getAccessToken().then(token => { + assert.equal(token, 'foo'); + }); + }); + + test('_getAccessToken refreshes token', () => { + const token = {access_token: 'foo'}; + sandbox.stub(auth, '_loadGapi').returns(Promise.resolve()); + sandbox.stub(auth, '_configureOAuthLibrary').returns(Promise.resolve()); + sandbox.stub(auth, '_refreshToken').returns(Promise.resolve(token)); + sandbox.stub(auth, '_isTokenValid').returns(true) + .onFirstCall().returns(false); + return auth._getAccessToken().then(token => { + assert.isTrue(auth._loadGapi.called); + assert.isTrue(auth._configureOAuthLibrary.called); + assert.isTrue(auth._refreshToken.called); + assert.equal(token, 'foo'); + }); + }); + + test('_isTokenValid', () => { + assert.isFalse(auth._isTokenValid()); + 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('_configureOAuthLibrary', () => { + sandbox.stub(auth, '_getOAuthConfig').returns({ + auth_url: 'some_auth_url', + proxy_url: 'some_proxy_url', + client_id: 'some_client_id', + email: 'some_email', + }); + return auth._configureOAuthLibrary().then(() => { + assert.isTrue(gapi.load.calledWith('config_min')); + assert.isTrue(auth._getOAuthConfig.called); + assert.isTrue(gapi.config.update.calledWith( + 'oauth-flow/authUrl', 'some_auth_url')); + assert.isTrue(gapi.config.update.calledWith( + 'oauth-flow/proxyUrl', 'some_proxy_url')); + assert.equal(GrGapiAuth._oauthClientId, 'some_client_id'); + assert.equal(GrGapiAuth._oauthEmail, 'some_email'); + assert.isTrue(gapi.auth.init.called); + assert.isTrue(gapi.load.calledWith('auth')); + }); + }); + + test('_refreshToken no token', () => { + gapi.auth.authorize.callsArgWith(1, null); + return auth._refreshToken().catch(reason => { + assert.equal(reason, 'No token returned'); + }); + }); + + test('_refreshToken error', () => { + gapi.auth.authorize.callsArgWith(1, {error: 'some error'}); + return auth._refreshToken().catch(reason => { + assert.equal(reason, 'some error'); + }); + }); + + test('_refreshToken', () => { + const token = {}; + gapi.auth.authorize.callsArgWith(1, token); + return auth._refreshToken().then(t => { + assert.strictEqual(token, t); + }); + }); + + test('_getOAuthConfig', () => { + const config = {}; + sandbox.stub(auth, '_getResponseObject').returns(config); + return auth._getOAuthConfig().then(c => { + const [url, options] = fetch.lastCall.args; + assert.equal(url, '/accounts/self/oauthconfig'); + assert.equal(options.credentials, 'same-origin'); + assert.equal(options.headers.get('Accept'), 'application/json'); + assert.strictEqual(c, config); + }); + }); + + test('BaseUrlBehavior', () => { + sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('http://foo'); + sandbox.stub(auth, '_getResponseObject').returns({}); + return auth._getOAuthConfig().then(c => { + const [url] = fetch.lastCall.args; + assert.equal(url, 'http://foo/accounts/self/oauthconfig'); + }); + }); + + suite('faster gerrit cors', () => { + setup(() => { + window.FASTER_GERRIT_CORS = true; + }); + + teardown(() => { + delete window.FASTER_GERRIT_CORS; + }); + + test('PUT works', () => { + sandbox.stub(auth, '_getAccessToken').returns(Promise.resolve('foo')); + const originalOptions = { + method: 'PUT', + headers: new Headers({'Content-Type': 'mail/pigeon'}), + }; + return auth.fetch('/url', originalOptions).then(() => { + assert.isTrue(auth._getAccessToken.called); + const [url, options] = fetch.lastCall.args; + assert.include(url, '$ct=mail%2Fpigeon'); + assert.include(url, '$m=PUT'); + assert.include(url, 'access_token=foo'); + assert.equal(options.method, 'POST'); + assert.equal(options.headers.get('Content-Type'), 'text/plain'); + }); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gerrit-auth.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gerrit-auth.js new file mode 100644 index 0000000..b70790d --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-gerrit-auth.js
@@ -0,0 +1,49 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function(window) { + 'use strict'; + + // Prevent redefinition. + if (window.GrGerritAuth) { return; } + + function GrGerritAuth() {} + + GrGerritAuth.prototype._getCookie = function(name) { + const key = name + '='; + let result = ''; + document.cookie.split(';').some(c => { + c = c.trim(); + if (c.startsWith(key)) { + result = c.substring(key.length); + return true; + } + }); + return result; + }; + + GrGerritAuth.prototype.fetch = function(url, opt_options) { + const options = Object.assign({}, opt_options); + if (options.method && options.method !== 'GET') { + const token = this._getCookie('XSRF_TOKEN'); + if (token) { + options.headers = options.headers || new Headers(); + options.headers.append('X-Gerrit-Auth', token); + } + } + options.credentials = 'same-origin'; + return fetch(url, options); + }; + + window.GrGerritAuth = GrGerritAuth; +})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html index 1e5fdaa..753c26e 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -14,13 +14,18 @@ limitations under the License. --> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html"> +<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> +<!-- NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 --> <script src="../../../bower_components/es6-promise/dist/es6-promise.min.js"></script> <script src="../../../bower_components/fetch/fetch.js"></script> <dom-module id="gr-rest-api-interface"> - <script src="gr-rest-api-interface.js"></script> + <!-- NB: Order is important, because of namespaced classes. --> + <script src="gr-gerrit-auth.js"></script> + <script src="gr-gapi-auth.js"></script> <script src="gr-reviewer-updates-parser.js"></script> + <script src="gr-rest-api-interface.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js index 512c055..b710cfa 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -14,78 +14,27 @@ (function() { 'use strict'; - var DiffViewMode = { + const DiffViewMode = { SIDE_BY_SIDE: 'SIDE_BY_SIDE', UNIFIED: 'UNIFIED_DIFF', }; - var JSON_PREFIX = ')]}\''; - var MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 900; - var PARENT_PATCH_NUM = 'PARENT'; + const JSON_PREFIX = ')]}\''; + const MAX_PROJECT_RESULTS = 25; + const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 900; + const PARENT_PATCH_NUM = 'PARENT'; - var Requests = { + const Requests = { SEND_DIFF_DRAFT: 'sendDiffDraft', }; - // Must be kept in sync with the ListChangesOption enum and protobuf. - var ListChangesOption = { - LABELS: 0, - DETAILED_LABELS: 8, - - // Return information on the current patch set of the change. - CURRENT_REVISION: 1, - ALL_REVISIONS: 2, - - // If revisions are included, parse the commit object. - CURRENT_COMMIT: 3, - ALL_COMMITS: 4, - - // If a patch set is included, include the files of the patch set. - CURRENT_FILES: 5, - ALL_FILES: 6, - - // If accounts are included, include detailed account info. - DETAILED_ACCOUNTS: 7, - - // Include messages associated with the change. - MESSAGES: 9, - - // Include allowed actions client could perform. - CURRENT_ACTIONS: 10, - - // Set the reviewed boolean for the caller. - REVIEWED: 11, - - // Include download commands for the caller. - DOWNLOAD_COMMANDS: 13, - - // Include patch set weblinks. - WEB_LINKS: 14, - - // Include consistency check results. - CHECK: 15, - - // Include allowed change actions client could perform. - CHANGE_ACTIONS: 16, - - // Include a copy of commit messages including review footers. - COMMIT_FOOTERS: 17, - - // Include push certificate information along with any patch sets. - PUSH_CERTIFICATES: 18, - - // Include change's reviewer updates. - REVIEWER_UPDATES: 19, - - // Set the submittable boolean. - SUBMITTABLE: 20, - }; + let auth = null; Polymer({ is: 'gr-rest-api-interface', behaviors: [ - Gerrit.BaseUrlBehavior, Gerrit.PathListBehavior, + Gerrit.RESTClientBehavior, ], /** @@ -115,20 +64,13 @@ }, }, - fetchJSON: function(url, opt_errFn, opt_cancelCondition, opt_params, - opt_opts) { - opt_opts = opt_opts || {}; - // Issue 5715, This can be reverted back once - // iOS 10.3 and mac os 10.12.4 has the fetch api fix. - var fetchOptions = { - credentials: 'same-origin' - }; - if (opt_opts.headers !== undefined) { - fetchOptions['headers'] = opt_opts.headers; - } + created() { + auth = window.USE_GAPI_AUTH ? new GrGapiAuth() : new GrGerritAuth(); + }, - var urlWithParams = this._urlWithParams(url, opt_params); - return fetch(urlWithParams, fetchOptions).then(function(response) { + fetchJSON(url, opt_errFn, opt_cancelCondition, opt_params) { + const urlWithParams = this._urlWithParams(url, opt_params); + return auth.fetch(urlWithParams).then(response => { if (opt_cancelCondition && opt_cancelCondition()) { response.body.cancel(); return; @@ -139,12 +81,12 @@ opt_errFn.call(null, response); return; } - this.fire('server-error', {response: response}); + this.fire('server-error', {response}); return; } return this.getResponseObject(response); - }.bind(this)).catch(function(err) { + }).catch(err => { if (opt_errFn) { opt_errFn.call(null, null, err); } else { @@ -152,31 +94,28 @@ throw err; } throw err; - }.bind(this)); + }); }, - _urlWithParams: function(url, opt_params) { + _urlWithParams(url, opt_params) { if (!opt_params) { return this.getBaseUrl() + url; } - var params = []; - for (var p in opt_params) { + const params = []; + for (const p in opt_params) { if (opt_params[p] == null) { params.push(encodeURIComponent(p)); continue; } - var values = [].concat(opt_params[p]); - for (var i = 0; i < values.length; i++) { - params.push( - encodeURIComponent(p) + '=' + - encodeURIComponent(values[i])); + for (const value of [].concat(opt_params[p])) { + params.push(`${encodeURIComponent(p)}=${encodeURIComponent(value)}`); } } return this.getBaseUrl() + url + '?' + params.join('&'); }, - getResponseObject: function(response) { - return response.text().then(function(text) { - var result; + getResponseObject(response) { + return response.text().then(text => { + let result; try { result = JSON.parse(text.substring(JSON_PREFIX.length)); } catch (_) { @@ -186,21 +125,39 @@ }); }, - getConfig: function() { + getConfig() { return this._fetchSharedCacheURL('/config/server/info'); }, - getProjectConfig: function(project) { + getProjectConfig(project) { return this._fetchSharedCacheURL( '/projects/' + encodeURIComponent(project) + '/config'); }, - getVersion: function() { + getProjectAccess(project) { + return this._fetchSharedCacheURL( + '/access/?project=' + encodeURIComponent(project)); + }, + + saveProjectConfig(project, config, opt_errFn, opt_ctx) { + return this.send('PUT', `/projects/${project}/config`, config, opt_errFn, + opt_ctx); + }, + + createProject(config, opt_errFn, opt_ctx) { + if (!config.name) { + return ''; + } + return this.send('PUT', `/projects/${config.name}`, config, opt_errFn, + opt_ctx); + }, + + getVersion() { return this._fetchSharedCacheURL('/config/server/version'); }, - getDiffPreferences: function() { - return this.getLoggedIn().then(function(loggedIn) { + getDiffPreferences() { + return this.getLoggedIn().then(loggedIn => { if (loggedIn) { return this._fetchSharedCacheURL('/accounts/self/preferences.diff'); } @@ -224,10 +181,10 @@ tab_size: 8, theme: 'DEFAULT', }); - }.bind(this)); + }); }, - savePreferences: function(prefs, opt_errFn, opt_ctx) { + savePreferences(prefs, opt_errFn, opt_ctx) { // Note (Issue 5142): normalize the download scheme with lower case before // saving. if (prefs.download_scheme) { @@ -238,127 +195,143 @@ opt_ctx); }, - saveDiffPreferences: function(prefs, opt_errFn, opt_ctx) { + saveDiffPreferences(prefs, opt_errFn, opt_ctx) { // Invalidate the cache. this._cache['/accounts/self/preferences.diff'] = undefined; return this.send('PUT', '/accounts/self/preferences.diff', prefs, opt_errFn, opt_ctx); }, - getAccount: function() { - return this._fetchSharedCacheURL('/accounts/self/detail', function(resp) { + getAccount() { + return this._fetchSharedCacheURL('/accounts/self/detail', resp => { if (resp.status === 403) { this._cache['/accounts/self/detail'] = null; } - }.bind(this)); + }); }, - getAccountEmails: function() { + getAccountEmails() { return this._fetchSharedCacheURL('/accounts/self/emails'); }, - addAccountEmail: function(email, opt_errFn, opt_ctx) { + addAccountEmail(email, opt_errFn, opt_ctx) { return this.send('PUT', '/accounts/self/emails/' + encodeURIComponent(email), null, opt_errFn, opt_ctx); }, - deleteAccountEmail: function(email, opt_errFn, opt_ctx) { + deleteAccountEmail(email, opt_errFn, opt_ctx) { return this.send('DELETE', '/accounts/self/emails/' + encodeURIComponent(email), null, opt_errFn, opt_ctx); }, - setPreferredAccountEmail: function(email, opt_errFn, opt_ctx) { + setPreferredAccountEmail(email, opt_errFn, opt_ctx) { return this.send('PUT', '/accounts/self/emails/' + encodeURIComponent(email) + '/preferred', null, - opt_errFn, opt_ctx).then(function() { - // If result of getAccountEmails is in cache, update it in the cache - // so we don't have to invalidate it. - var cachedEmails = this._cache['/accounts/self/emails']; - if (cachedEmails) { - var emails = cachedEmails.map(function(entry) { - if (entry.email === email) { - return {email: email, preferred: true}; - } else { - return {email: email}; + opt_errFn, opt_ctx).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['/accounts/self/emails']; + if (cachedEmails) { + const emails = cachedEmails.map(entry => { + if (entry.email === email) { + return {email, preferred: true}; + } else { + return {email}; + } + }); + this._cache['/accounts/self/emails'] = emails; } }); - this._cache['/accounts/self/emails'] = emails; - } - }.bind(this)); }, - setAccountName: function(name, opt_errFn, opt_ctx) { - return this.send('PUT', '/accounts/self/name', {name: name}, opt_errFn, - opt_ctx).then(function(response) { + setAccountName(name, opt_errFn, opt_ctx) { + return this.send('PUT', '/accounts/self/name', {name}, opt_errFn, + opt_ctx).then(response => { // If result of getAccount is in cache, update it in the cache // so we don't have to invalidate it. - var cachedAccount = this._cache['/accounts/self/detail']; + const cachedAccount = this._cache['/accounts/self/detail']; if (cachedAccount) { - return this.getResponseObject(response).then(function(newName) { + return this.getResponseObject(response).then(newName => { // Replace object in cache with new object to force UI updates. // TODO(logan): Polyfill for Object.assign in IE this._cache['/accounts/self/detail'] = Object.assign( {}, cachedAccount, {name: newName}); - }.bind(this)); + }); } - }.bind(this)); + }); }, - setAccountStatus: function(status, opt_errFn, opt_ctx) { - return this.send('PUT', '/accounts/self/status', {status: status}, - opt_errFn, opt_ctx).then(function(response) { + setAccountStatus(status, opt_errFn, opt_ctx) { + return this.send('PUT', '/accounts/self/status', {status}, + opt_errFn, opt_ctx).then(response => { // If result of getAccount is in cache, update it in the cache // so we don't have to invalidate it. - var cachedAccount = this._cache['/accounts/self/detail']; + const cachedAccount = this._cache['/accounts/self/detail']; if (cachedAccount) { - return this.getResponseObject(response).then(function(newStatus) { + return this.getResponseObject(response).then(newStatus => { // Replace object in cache with new object to force UI updates. // TODO(logan): Polyfill for Object.assign in IE this._cache['/accounts/self/detail'] = Object.assign( {}, cachedAccount, {status: newStatus}); - }.bind(this)); + }); } - }.bind(this)); + }); }, - getAccountGroups: function() { + getAccountGroups() { return this._fetchSharedCacheURL('/accounts/self/groups'); }, - getAccountCapabilities: function(opt_params) { - var queryString = ''; + getAccountAgreements() { + return this._fetchSharedCacheURL('/accounts/self/agreements'); + }, + + getAccountCapabilities(opt_params) { + let queryString = ''; if (opt_params) { queryString = '?q=' + opt_params - .map(function(param) { return encodeURIComponent(param); }) + .map(param => { return encodeURIComponent(param); }) .join('&q='); } return this._fetchSharedCacheURL('/accounts/self/capabilities' + queryString); }, - getLoggedIn: function() { - return this.getAccount().then(function(account) { + getLoggedIn() { + return this.getAccount().then(account => { return account != null; }); }, - checkCredentials: function() { + getIsAdmin() { + return this.getLoggedIn().then(isLoggedIn => { + if (isLoggedIn) { + return this.getAccountCapabilities(); + } else { + return Promise.resolve(); + } + }).then(capabilities => { + return capabilities && capabilities.administrateServer; + }); + }, + + checkCredentials() { // Skip the REST response cache. return this.fetchJSON('/accounts/self/detail'); }, - getPreferences: function() { - return this.getLoggedIn().then(function(loggedIn) { + getPreferences() { + return this.getLoggedIn().then(loggedIn => { if (loggedIn) { return this._fetchSharedCacheURL('/accounts/self/preferences').then( - function(res) { - if (this._isNarrowScreen()) { - res.default_diff_view = DiffViewMode.UNIFIED; - } else { - res.default_diff_view = res.diff_view; - } - return Promise.resolve(res); - }.bind(this)); + res => { + if (this._isNarrowScreen()) { + res.default_diff_view = DiffViewMode.UNIFIED; + } else { + res.default_diff_view = res.diff_view; + } + return Promise.resolve(res); + }); } return Promise.resolve({ @@ -367,27 +340,27 @@ DiffViewMode.UNIFIED : DiffViewMode.SIDE_BY_SIDE, diff_view: 'SIDE_BY_SIDE', }); - }.bind(this)); + }); }, - getWatchedProjects: function() { + getWatchedProjects() { return this._fetchSharedCacheURL('/accounts/self/watched.projects'); }, - saveWatchedProjects: function(projects, opt_errFn, opt_ctx) { + saveWatchedProjects(projects, opt_errFn, opt_ctx) { return this.send('POST', '/accounts/self/watched.projects', projects, opt_errFn, opt_ctx) - .then(function(response) { + .then(response => { return this.getResponseObject(response); - }.bind(this)); + }); }, - deleteWatchedProjects: function(projects, opt_errFn, opt_ctx) { + deleteWatchedProjects(projects, opt_errFn, opt_ctx) { return this.send('POST', '/accounts/self/watched.projects:delete', projects, opt_errFn, opt_ctx); }, - _fetchSharedCacheURL: function(url, opt_errFn) { + _fetchSharedCacheURL(url, opt_errFn) { if (this._sharedFetchPromises[url]) { return this._sharedFetchPromises[url]; } @@ -396,89 +369,71 @@ return Promise.resolve(this._cache[url]); } this._sharedFetchPromises[url] = this.fetchJSON(url, opt_errFn).then( - function(response) { - if (response !== undefined) { - this._cache[url] = response; - } - this._sharedFetchPromises[url] = undefined; - return response; - }.bind(this)).catch(function(err) { - this._sharedFetchPromises[url] = undefined; - throw err; - }.bind(this)); + response => { + if (response !== undefined) { + this._cache[url] = response; + } + this._sharedFetchPromises[url] = undefined; + return response; + }).catch(err => { + this._sharedFetchPromises[url] = undefined; + throw err; + }); return this._sharedFetchPromises[url]; }, - _isNarrowScreen: function() { + _isNarrowScreen() { return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX; }, - getChanges: function(changesPerPage, opt_query, opt_offset) { - var options = this._listChangesOptionsToHex( - ListChangesOption.LABELS, - ListChangesOption.DETAILED_ACCOUNTS + getChanges(opt_changesPerPage, opt_query, opt_offset, opt_options) { + const options = opt_options || this.listChangesOptionsToHex( + this.ListChangesOption.LABELS, + this.ListChangesOption.DETAILED_ACCOUNTS ); // Issue 4524: respect legacy token with max sortkey. if (opt_offset === 'n,z') { opt_offset = 0; } - var params = { - n: changesPerPage, + const params = { O: options, S: opt_offset || 0, }; + if (opt_changesPerPage) { params.n = opt_changesPerPage; } if (opt_query && opt_query.length > 0) { params.q = opt_query; } return this.fetchJSON('/changes/', null, null, params); }, - getDashboardChanges: function() { - var options = this._listChangesOptionsToHex( - ListChangesOption.LABELS, - ListChangesOption.DETAILED_ACCOUNTS, - ListChangesOption.REVIEWED - ); - var params = { - O: options, - q: [ - 'is:open owner:self', - 'is:open ((reviewer:self -owner:self -star:ignore) OR assignee:self)', - 'is:closed (owner:self OR reviewer:self OR assignee:self) -age:4w ' + - 'limit:10', - ], - }; - return this.fetchJSON('/changes/', null, null, params); - }, - - getChangeActionURL: function(changeNum, opt_patchNum, endpoint) { + getChangeActionURL(changeNum, opt_patchNum, endpoint) { return this._changeBaseURL(changeNum, opt_patchNum) + endpoint; }, - getChangeDetail: function(changeNum, opt_errFn, opt_cancelCondition) { - var options = this._listChangesOptionsToHex( - ListChangesOption.ALL_REVISIONS, - ListChangesOption.CHANGE_ACTIONS, - ListChangesOption.CURRENT_ACTIONS, - ListChangesOption.CURRENT_COMMIT, - ListChangesOption.DOWNLOAD_COMMANDS, - ListChangesOption.SUBMITTABLE, - ListChangesOption.WEB_LINKS + getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) { + const options = this.listChangesOptionsToHex( + this.ListChangesOption.ALL_REVISIONS, + this.ListChangesOption.CHANGE_ACTIONS, + this.ListChangesOption.CURRENT_ACTIONS, + this.ListChangesOption.CURRENT_COMMIT, + this.ListChangesOption.DOWNLOAD_COMMANDS, + this.ListChangesOption.SUBMITTABLE, + this.ListChangesOption.WEB_LINKS ); return this._getChangeDetail( changeNum, options, opt_errFn, opt_cancelCondition) - .then(GrReviewerUpdatesParser.parse); + .then(GrReviewerUpdatesParser.parse); }, - getDiffChangeDetail: function(changeNum, opt_errFn, opt_cancelCondition) { - var options = this._listChangesOptionsToHex( - ListChangesOption.ALL_REVISIONS + getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) { + const options = this.listChangesOptionsToHex( + this.ListChangesOption.ALL_REVISIONS ); return this._getChangeDetail(changeNum, options, opt_errFn, opt_cancelCondition); }, - _getChangeDetail: function(changeNum, options, opt_errFn, + _getChangeDetail(changeNum, options, opt_errFn, opt_cancelCondition) { return this.fetchJSON( this.getChangeActionURL(changeNum, null, '/detail'), @@ -487,13 +442,13 @@ {O: options}); }, - getChangeCommitInfo: function(changeNum, patchNum) { + getChangeCommitInfo(changeNum, patchNum) { return this.fetchJSON( this.getChangeActionURL(changeNum, patchNum, '/commit?links')); }, - getChangeFiles: function(changeNum, patchRange) { - var endpoint = '/files'; + getChangeFiles(changeNum, patchRange) { + let endpoint = '/files'; if (patchRange.basePatchNum !== 'PARENT') { endpoint += '?base=' + encodeURIComponent(patchRange.basePatchNum); } @@ -501,23 +456,23 @@ this.getChangeActionURL(changeNum, patchRange.patchNum, endpoint)); }, - getChangeFilesAsSpeciallySortedArray: function(changeNum, patchRange) { + getChangeFilesAsSpeciallySortedArray(changeNum, patchRange) { return this.getChangeFiles(changeNum, patchRange).then( this._normalizeChangeFilesResponse.bind(this)); }, - getChangeFilePathsAsSpeciallySortedArray: function(changeNum, patchRange) { - return this.getChangeFiles(changeNum, patchRange).then(function(files) { + getChangeFilePathsAsSpeciallySortedArray(changeNum, patchRange) { + return this.getChangeFiles(changeNum, patchRange).then(files => { return Object.keys(files).sort(this.specialFilePathCompare); - }.bind(this)); + }); }, - _normalizeChangeFilesResponse: function(response) { + _normalizeChangeFilesResponse(response) { if (!response) { return []; } - var paths = Object.keys(response).sort(this.specialFilePathCompare); - var files = []; - for (var i = 0; i < paths.length; i++) { - var info = response[paths[i]]; + const paths = Object.keys(response).sort(this.specialFilePathCompare); + const files = []; + for (let i = 0; i < paths.length; i++) { + const info = response[paths[i]]; info.__path = paths[i]; info.lines_inserted = info.lines_inserted || 0; info.lines_deleted = info.lines_deleted || 0; @@ -526,64 +481,110 @@ return files; }, - getChangeRevisionActions: function(changeNum, patchNum) { + getChangeRevisionActions(changeNum, patchNum) { return this.fetchJSON( this.getChangeActionURL(changeNum, patchNum, '/actions')).then( - function(revisionActions) { + revisionActions => { // The rebase button on change screen is always enabled. - if (revisionActions.rebase) { - revisionActions.rebase.rebaseOnCurrent = + if (revisionActions.rebase) { + revisionActions.rebase.rebaseOnCurrent = !!revisionActions.rebase.enabled; - revisionActions.rebase.enabled = true; - } - return revisionActions; - }); + revisionActions.rebase.enabled = true; + } + return revisionActions; + }); }, - getChangeSuggestedReviewers: function(changeNum, inputVal, opt_errFn, + getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn, opt_ctx) { - var url = this.getChangeActionURL(changeNum, null, '/suggest_reviewers'); - return this.fetchJSON(url, opt_errFn, opt_ctx, { - n: 10, // Return max 10 results - q: inputVal, - }); + const url = + this.getChangeActionURL(changeNum, null, '/suggest_reviewers'); + const req = {n: 10}; + if (inputVal) { req.q = inputVal; } + return this.fetchJSON(url, opt_errFn, opt_ctx, req); }, - getSuggestedGroups: function(inputVal, opt_n, opt_errFn, opt_ctx) { - var params = {s: inputVal}; + getGroups(filter, groupsPerPage, opt_offset) { + const offset = opt_offset || 0; + filter = filter ? '&m=' + filter : ''; + + return this._fetchSharedCacheURL( + `/groups/?n=${groupsPerPage + 1}&S=${offset}${filter}` + ); + }, + + getProjects(filter, projectsPerPage, opt_offset) { + const offset = opt_offset || 0; + filter = filter ? '&m=' + filter : ''; + + return this._fetchSharedCacheURL( + `/projects/?d&n=${projectsPerPage + 1}&S=${offset}${filter}` + ); + }, + + getProjectBranches(filter, project, projectsBranchesPerPage, opt_offset) { + const offset = opt_offset || 0; + filter = filter ? '&m=' + filter : ''; + + return this._fetchSharedCacheURL( + `/projects/${project}/branches?n=${projectsBranchesPerPage + 1}&s=` + + `${offset}${filter}` + ); + }, + + getProjectTags(filter, project, projectsTagsPerPage, opt_offset) { + const offset = opt_offset || 0; + filter = filter ? '&m=' + filter : ''; + + return this._fetchSharedCacheURL( + `/projects/${project}/tags?n=${projectsTagsPerPage + 1}&s=` + + `${offset}${filter}` + ); + }, + + getPlugins() { + return this._fetchSharedCacheURL('/plugins/?all'); + }, + + getSuggestedGroups(inputVal, opt_n, opt_errFn, opt_ctx) { + const params = {s: inputVal}; if (opt_n) { params.n = opt_n; } return this.fetchJSON('/groups/', opt_errFn, opt_ctx, params); }, - getSuggestedProjects: function(inputVal, opt_n, opt_errFn, opt_ctx) { - var params = {p: inputVal}; + getSuggestedProjects(inputVal, opt_n, opt_errFn, opt_ctx) { + const params = { + m: inputVal, + n: MAX_PROJECT_RESULTS, + type: 'ALL', + }; if (opt_n) { params.n = opt_n; } return this.fetchJSON('/projects/', opt_errFn, opt_ctx, params); }, - getSuggestedAccounts: function(inputVal, opt_n, opt_errFn, opt_ctx) { - var params = {q: inputVal, suggest: null}; + getSuggestedAccounts(inputVal, opt_n, opt_errFn, opt_ctx) { + const params = {q: inputVal, suggest: null}; if (opt_n) { params.n = opt_n; } return this.fetchJSON('/accounts/', opt_errFn, opt_ctx, params); }, - addChangeReviewer: function(changeNum, reviewerID) { + addChangeReviewer(changeNum, reviewerID) { return this._sendChangeReviewerRequest('POST', changeNum, reviewerID); }, - removeChangeReviewer: function(changeNum, reviewerID) { + removeChangeReviewer(changeNum, reviewerID) { return this._sendChangeReviewerRequest('DELETE', changeNum, reviewerID); }, - _sendChangeReviewerRequest: function(method, changeNum, reviewerID) { - var url = this.getChangeActionURL(changeNum, null, '/reviewers'); - var body; + _sendChangeReviewerRequest(method, changeNum, reviewerID) { + let url = this.getChangeActionURL(changeNum, null, '/reviewers'); + let body; switch (method) { case 'POST': body = {reviewer: reviewerID}; break; case 'DELETE': - url += '/' + reviewerID; + url += '/' + encodeURIComponent(reviewerID); break; default: throw Error('Unsupported HTTP method: ' + method); @@ -592,124 +593,122 @@ return this.send(method, url, body); }, - getRelatedChanges: function(changeNum, patchNum) { + getRelatedChanges(changeNum, patchNum) { return this.fetchJSON( this.getChangeActionURL(changeNum, patchNum, '/related')); }, - getChangesSubmittedTogether: function(changeNum) { + getChangesSubmittedTogether(changeNum) { return this.fetchJSON( this.getChangeActionURL(changeNum, null, '/submitted_together')); }, - getChangeConflicts: function(changeNum) { - var options = this._listChangesOptionsToHex( - ListChangesOption.CURRENT_REVISION, - ListChangesOption.CURRENT_COMMIT + getChangeConflicts(changeNum) { + const options = this.listChangesOptionsToHex( + this.ListChangesOption.CURRENT_REVISION, + this.ListChangesOption.CURRENT_COMMIT ); - var params = { + const params = { O: options, q: 'status:open is:mergeable conflicts:' + changeNum, }; return this.fetchJSON('/changes/', null, null, params); }, - getChangeCherryPicks: function(project, changeID, changeNum) { - var options = this._listChangesOptionsToHex( - ListChangesOption.CURRENT_REVISION, - ListChangesOption.CURRENT_COMMIT + getChangeCherryPicks(project, changeID, changeNum) { + const options = this.listChangesOptionsToHex( + this.ListChangesOption.CURRENT_REVISION, + this.ListChangesOption.CURRENT_COMMIT ); - var query = [ + const query = [ 'project:' + project, 'change:' + changeID, '-change:' + changeNum, '-is:abandoned', ].join(' '); - var params = { + const params = { O: options, q: query, }; return this.fetchJSON('/changes/', null, null, params); }, - getChangesWithSameTopic: function(topic) { - var options = this._listChangesOptionsToHex( - ListChangesOption.LABELS, - ListChangesOption.CURRENT_REVISION, - ListChangesOption.CURRENT_COMMIT, - ListChangesOption.DETAILED_LABELS + getChangesWithSameTopic(topic) { + const options = this.listChangesOptionsToHex( + this.ListChangesOption.LABELS, + this.ListChangesOption.CURRENT_REVISION, + this.ListChangesOption.CURRENT_COMMIT, + this.ListChangesOption.DETAILED_LABELS ); - var params = { + const params = { O: options, q: 'status:open topic:' + topic, }; return this.fetchJSON('/changes/', null, null, params); }, - getReviewedFiles: function(changeNum, patchNum) { + getReviewedFiles(changeNum, patchNum) { return this.fetchJSON( this.getChangeActionURL(changeNum, patchNum, '/files?reviewed')); }, - saveFileReviewed: function(changeNum, patchNum, path, reviewed, opt_errFn, - opt_ctx) { - var method = reviewed ? 'PUT' : 'DELETE'; - var url = this.getChangeActionURL(changeNum, patchNum, + saveFileReviewed(changeNum, patchNum, path, reviewed, opt_errFn, opt_ctx) { + const method = reviewed ? 'PUT' : 'DELETE'; + const url = this.getChangeActionURL(changeNum, patchNum, '/files/' + encodeURIComponent(path) + '/reviewed'); return this.send(method, url, null, opt_errFn, opt_ctx); }, - saveChangeReview: function(changeNum, patchNum, review, opt_errFn, - opt_ctx) { - var url = this.getChangeActionURL(changeNum, patchNum, '/review'); + saveChangeReview(changeNum, patchNum, review, opt_errFn, opt_ctx) { + const url = this.getChangeActionURL(changeNum, patchNum, '/review'); return this.send('POST', url, review, opt_errFn, opt_ctx); }, - getFileInChangeEdit: function(changeNum, path) { + getFileInChangeEdit(changeNum, path) { return this.send('GET', this.getChangeActionURL(changeNum, null, '/edit/' + encodeURIComponent(path) )); }, - rebaseChangeEdit: function(changeNum) { + rebaseChangeEdit(changeNum) { return this.send('POST', this.getChangeActionURL(changeNum, null, '/edit:rebase' )); }, - deleteChangeEdit: function(changeNum) { + deleteChangeEdit(changeNum) { return this.send('DELETE', this.getChangeActionURL(changeNum, null, '/edit' )); }, - restoreFileInChangeEdit: function(changeNum, restore_path) { + restoreFileInChangeEdit(changeNum, restore_path) { return this.send('POST', this.getChangeActionURL(changeNum, null, '/edit'), - {restore_path: restore_path} + {restore_path} ); }, - renameFileInChangeEdit: function(changeNum, old_path, new_path) { + renameFileInChangeEdit(changeNum, old_path, new_path) { return this.send('POST', this.getChangeActionURL(changeNum, null, '/edit'), - {old_path: old_path}, - {new_path: new_path} + {old_path}, + {new_path} ); }, - deleteFileInChangeEdit: function(changeNum, path) { + deleteFileInChangeEdit(changeNum, path) { return this.send('DELETE', this.getChangeActionURL(changeNum, null, '/edit/' + encodeURIComponent(path) )); }, - saveChangeEdit: function(changeNum, path, contents) { + saveChangeEdit(changeNum, path, contents) { return this.send('PUT', this.getChangeActionURL(changeNum, null, '/edit/' + encodeURIComponent(path) @@ -718,62 +717,63 @@ ); }, - saveChangeCommitMessageEdit: function(changeNum, message) { - var url = this.getChangeActionURL(changeNum, null, '/edit:message'); - return this.send('PUT', url, {message: message}); + // Deprecated, prefer to use putChangeCommitMessage instead. + saveChangeCommitMessageEdit(changeNum, message) { + const url = this.getChangeActionURL(changeNum, null, '/edit:message'); + return this.send('PUT', url, {message}); }, - publishChangeEdit: function(changeNum) { + publishChangeEdit(changeNum) { return this.send('POST', this.getChangeActionURL(changeNum, null, '/edit:publish')); }, - saveChangeStarred: function(changeNum, starred) { - var url = '/accounts/self/starred.changes/' + changeNum; - var method = starred ? 'PUT' : 'DELETE'; + putChangeCommitMessage(changeNum, message) { + const url = this.getChangeActionURL(changeNum, null, '/message'); + return this.send('PUT', url, {message}); + }, + + saveChangeStarred(changeNum, starred) { + const url = '/accounts/self/starred.changes/' + changeNum; + const method = starred ? 'PUT' : 'DELETE'; return this.send(method, url); }, - send: function(method, url, opt_body, opt_errFn, opt_ctx, opt_contentType) { - var headers = new Headers({ - 'X-Gerrit-Auth': this._getCookie('XSRF_TOKEN'), - }); - var options = { - method: method, - headers: headers, - credentials: 'same-origin', - }; + send(method, url, opt_body, opt_errFn, opt_ctx, opt_contentType) { + const options = {method}; if (opt_body) { - headers.append('Content-Type', opt_contentType || 'application/json'); + options.headers = new Headers({ + 'Content-Type': opt_contentType || 'application/json', + }); if (typeof opt_body !== 'string') { opt_body = JSON.stringify(opt_body); } options.body = opt_body; } - return fetch(this.getBaseUrl() + url, options).then(function(response) { + return auth.fetch(this.getBaseUrl() + url, options).then(response => { if (!response.ok) { if (opt_errFn) { opt_errFn.call(opt_ctx || null, response); return undefined; } - this.fire('server-error', {response: response}); + this.fire('server-error', {response}); } return response; - }.bind(this)).catch(function(err) { + }).catch(err => { this.fire('network-error', {error: err}); if (opt_errFn) { opt_errFn.call(opt_ctx, null, err); } else { throw err; } - }.bind(this)); + }); }, - getDiff: function(changeNum, basePatchNum, patchNum, path, + getDiff(changeNum, basePatchNum, patchNum, path, opt_errFn, opt_cancelCondition) { - var url = this._getDiffFetchURL(changeNum, patchNum, path); - var params = { + const url = this._getDiffFetchURL(changeNum, patchNum, path); + const params = { context: 'ALL', intraline: null, whitespace: 'IGNORE_NONE', @@ -785,32 +785,29 @@ return this.fetchJSON(url, opt_errFn, opt_cancelCondition, params); }, - _getDiffFetchURL: function(changeNum, patchNum, path) { + _getDiffFetchURL(changeNum, patchNum, path) { return this._changeBaseURL(changeNum, patchNum) + '/files/' + encodeURIComponent(path) + '/diff'; }, - getDiffComments: function(changeNum, opt_basePatchNum, opt_patchNum, - opt_path) { + getDiffComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { return this._getDiffComments(changeNum, '/comments', opt_basePatchNum, opt_patchNum, opt_path); }, - getDiffRobotComments: function(changeNum, basePatchNum, patchNum, - opt_path) { + getDiffRobotComments(changeNum, basePatchNum, patchNum, opt_path) { return this._getDiffComments(changeNum, '/robotcomments', basePatchNum, patchNum, opt_path); }, - getDiffDrafts: function(changeNum, opt_basePatchNum, opt_patchNum, - opt_path) { + getDiffDrafts(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { return this._getDiffComments(changeNum, '/drafts', opt_basePatchNum, opt_patchNum, opt_path); }, - _setRange: function(comments, comment) { + _setRange(comments, comment) { if (comment.in_reply_to && !comment.range) { - for (var i = 0; i < comments.length; i++) { + for (let i = 0; i < comments.length; i++) { if (comments[i].id === comment.in_reply_to) { comment.range = comments[i].range; break; @@ -820,18 +817,18 @@ return comment; }, - _setRanges: function(comments) { + _setRanges(comments) { comments = comments || []; - comments.sort(function(a, b) { + comments.sort((a, b) => { return util.parseDate(a.updated) - util.parseDate(b.updated); }); - comments.forEach(function(comment) { + for (const comment of comments) { this._setRange(comments, comment); - }.bind(this)); + } return comments; }, - _getDiffComments: function(changeNum, endpoint, opt_basePatchNum, + _getDiffComments(changeNum, endpoint, opt_basePatchNum, opt_patchNum, opt_path) { if (!opt_basePatchNum && !opt_patchNum && !opt_path) { return this.fetchJSON( @@ -842,12 +839,12 @@ function withoutParent(c) { return c.side != PARENT_PATCH_NUM; } function setPath(c) { c.path = opt_path; } - var promises = []; - var comments; - var baseComments; - var url = + const promises = []; + let comments; + let baseComments; + const url = this._getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum); - promises.push(this.fetchJSON(url).then(function(response) { + promises.push(this.fetchJSON(url).then(response => { comments = response[opt_path] || []; // TODO(kaspern): Implement this on in the backend so this can be @@ -864,50 +861,50 @@ comments = comments.filter(withoutParent); comments.forEach(setPath); - }.bind(this))); + })); if (opt_basePatchNum != PARENT_PATCH_NUM) { - var baseURL = this._getDiffCommentsFetchURL(changeNum, endpoint, + const baseURL = this._getDiffCommentsFetchURL(changeNum, endpoint, opt_basePatchNum); - promises.push(this.fetchJSON(baseURL).then(function(response) { + promises.push(this.fetchJSON(baseURL).then(response => { baseComments = (response[opt_path] || []).filter(withoutParent); baseComments = this._setRanges(baseComments); baseComments.forEach(setPath); - }.bind(this))); + })); } - return Promise.all(promises).then(function() { + return Promise.all(promises).then(() => { return Promise.resolve({ - baseComments: baseComments, - comments: comments, + baseComments, + comments, }); }); }, - _getDiffCommentsFetchURL: function(changeNum, endpoint, opt_patchNum) { + _getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum) { return this._changeBaseURL(changeNum, opt_patchNum) + endpoint; }, - saveDiffDraft: function(changeNum, patchNum, draft) { + saveDiffDraft(changeNum, patchNum, draft) { return this._sendDiffDraftRequest('PUT', changeNum, patchNum, draft); }, - deleteDiffDraft: function(changeNum, patchNum, draft) { + deleteDiffDraft(changeNum, patchNum, draft) { return this._sendDiffDraftRequest('DELETE', changeNum, patchNum, draft); }, - hasPendingDiffDrafts: function() { + hasPendingDiffDrafts() { return !!this._pendingRequests[Requests.SEND_DIFF_DRAFT]; }, - _sendDiffDraftRequest: function(method, changeNum, patchNum, draft) { - var url = this.getChangeActionURL(changeNum, patchNum, '/drafts'); + _sendDiffDraftRequest(method, changeNum, patchNum, draft) { + let url = this.getChangeActionURL(changeNum, patchNum, '/drafts'); if (draft.id) { url += '/' + draft.id; } - var body; + let body; if (method === 'PUT') { body = draft; } @@ -917,93 +914,49 @@ } this._pendingRequests[Requests.SEND_DIFF_DRAFT]++; - return this.send(method, url, body).then(function(res) { + return this.send(method, url, body).then(res => { this._pendingRequests[Requests.SEND_DIFF_DRAFT]--; return res; - }.bind(this)); + }); }, - _changeBaseURL: function(changeNum, opt_patchNum) { - var v = '/changes/' + changeNum; - if (opt_patchNum) { - v += '/revisions/' + opt_patchNum; - } - return v; - }, - - // Derived from - // gerrit-extension-api/src/main/j/c/g/gerrit/extensions/client/ListChangesOption.java - _listChangesOptionsToHex: function() { - var v = 0; - for (var i = 0; i < arguments.length; i++) { - v |= 1 << arguments[i]; - } - return v.toString(16); - }, - - _getCookie: function(name) { - var key = name + '='; - var cookies = document.cookie.split(';'); - for (var i = 0; i < cookies.length; i++) { - var c = cookies[i]; - while (c.charAt(0) == ' ') { - c = c.substring(1); - } - if (c.indexOf(key) == 0) { - return c.substring(key.length, c.length); - } - } - return ''; - }, - - getCommitInfo: function(project, commit) { + getCommitInfo(project, commit) { return this.fetchJSON( '/projects/' + encodeURIComponent(project) + '/commits/' + encodeURIComponent(commit)); }, - _fetchB64File: function(url) { - return fetch(this.getBaseUrl() + url, {credentials: 'same-origin'}).then(function(response) { - var type = response.headers.get('X-FYI-Content-Type'); - return response.text() - .then(function(text) { - return {body: text, type: type}; + _fetchB64File(url) { + return auth.fetch(this.getBaseUrl() + url) + .then(response => { + if (!response.ok) { return Promise.reject(response.statusText); } + const type = response.headers.get('X-FYI-Content-Type'); + return response.text() + .then(text => { + return {body: text, type}; + }); }); - }); }, - getChangeFileContents: function(changeId, patchNum, path) { + getChangeFileContents(changeId, patchNum, path, opt_parentIndex) { + const parent = typeof opt_parentIndex === 'number' ? + '?parent=' + opt_parentIndex : ''; return this._fetchB64File( '/changes/' + encodeURIComponent(changeId) + '/revisions/' + encodeURIComponent(patchNum) + '/files/' + encodeURIComponent(path) + - '/content'); + '/content' + parent); }, - getCommitFileContents: function(projectName, commit, path) { - return this._fetchB64File( - '/projects/' + encodeURIComponent(projectName) + - '/commits/' + encodeURIComponent(commit) + - '/files/' + encodeURIComponent(path) + - '/content'); - }, + getImagesForDiff(changeNum, diff, patchRange) { + let promiseA; + let promiseB; - getImagesForDiff: function(project, commit, changeNum, diff, patchRange) { - var promiseA; - var promiseB; - - if (diff.meta_a && diff.meta_a.content_type.indexOf('image/') === 0) { + if (diff.meta_a && diff.meta_a.content_type.startsWith('image/')) { if (patchRange.basePatchNum === 'PARENT') { - // Need the commit info know the parent SHA. - promiseA = this.getCommitInfo(project, commit).then(function(info) { - if (info.parents.length !== 1) { - return Promise.reject('Change commit has multiple parents.'); - } - var parent = info.parents[0].commit; - return this.getCommitFileContents(project, parent, - diff.meta_a.name); - }.bind(this)); - + // Note: we only attempt to get the image from the first parent. + promiseA = this.getChangeFileContents(changeNum, patchRange.patchNum, + diff.meta_a.name, 1); } else { promiseA = this.getChangeFileContents(changeNum, patchRange.basePatchNum, diff.meta_a.name); @@ -1012,81 +965,90 @@ promiseA = Promise.resolve(null); } - if (diff.meta_b && diff.meta_b.content_type.indexOf('image/') === 0) { + if (diff.meta_b && diff.meta_b.content_type.startsWith('image/')) { promiseB = this.getChangeFileContents(changeNum, patchRange.patchNum, diff.meta_b.name); } else { promiseB = Promise.resolve(null); } - return Promise.all([promiseA, promiseB]) - .then(function(results) { - var baseImage = results[0]; - var revisionImage = results[1]; + return Promise.all([promiseA, promiseB]).then(results => { + const baseImage = results[0]; + const revisionImage = results[1]; - // Sometimes the server doesn't send back the content type. - if (baseImage) { - baseImage._expectedType = diff.meta_a.content_type; - } - if (revisionImage) { - revisionImage._expectedType = diff.meta_b.content_type; - } + // Sometimes the server doesn't send back the content type. + if (baseImage) { + baseImage._expectedType = diff.meta_a.content_type; + baseImage._name = diff.meta_a.name; + } + if (revisionImage) { + revisionImage._expectedType = diff.meta_b.content_type; + revisionImage._name = diff.meta_b.name; + } - return {baseImage: baseImage, revisionImage: revisionImage}; - }.bind(this)); + return {baseImage, revisionImage}; + }); }, - setChangeTopic: function(changeNum, topic) { + _changeBaseURL(changeNum, opt_patchNum) { + let v = '/changes/' + changeNum; + if (opt_patchNum) { + v += '/revisions/' + opt_patchNum; + } + return v; + }, + + setChangeTopic(changeNum, topic) { return this.send('PUT', '/changes/' + encodeURIComponent(changeNum) + - '/topic', {topic: topic}); + '/topic', {topic}).then(this.getResponseObject); }, - deleteAccountHttpPassword: function() { + deleteAccountHttpPassword() { return this.send('DELETE', '/accounts/self/password.http'); }, - generateAccountHttpPassword: function() { + generateAccountHttpPassword() { return this.send('PUT', '/accounts/self/password.http', {generate: true}) .then(this.getResponseObject); }, - getAccountSSHKeys: function() { + getAccountSSHKeys() { return this._fetchSharedCacheURL('/accounts/self/sshkeys'); }, - addAccountSSHKey: function(key) { + addAccountSSHKey(key) { return this.send('POST', '/accounts/self/sshkeys', key, null, null, 'plain/text') - .then(function(response) { + .then(response => { if (response.status < 200 && response.status >= 300) { return Promise.reject(); } return this.getResponseObject(response); - }.bind(this)) - .then(function(obj) { + }) + .then(obj => { if (!obj.valid) { return Promise.reject(); } return obj; }); }, - deleteAccountSSHKey: function(id) { + deleteAccountSSHKey(id) { return this.send('DELETE', '/accounts/self/sshkeys/' + id); }, - deleteVote: function(changeID, account, label) { + deleteVote(changeID, account, label) { return this.send('DELETE', '/changes/' + changeID + '/reviewers/' + account + '/votes/' + encodeURIComponent(label)); }, - setDescription: function(changeNum, patchNum, desc) { + setDescription(changeNum, patchNum, desc) { return this.send('PUT', this.getChangeActionURL(changeNum, patchNum, '/description'), {description: desc}); }, - confirmEmail: function(token) { - return this.send('PUT', '/config/server/email.confirm', {token: token}) - .then(function(response) { + confirmEmail(token) { + return this.send('PUT', '/config/server/email.confirm', {token}) + .then(response => { if (response.status === 204) { return 'Email confirmed successfully.'; } @@ -1094,22 +1056,48 @@ }); }, - setAssignee: function(changeNum, assignee) { + setAssignee(changeNum, assignee) { return this.send('PUT', this.getChangeActionURL(changeNum, null, '/assignee'), - {assignee: assignee}); + {assignee}); }, - deleteAssignee: function(changeNum) { + deleteAssignee(changeNum) { return this.send('DELETE', this.getChangeActionURL(changeNum, null, '/assignee')); }, - probePath: function(path) { + probePath(path) { return fetch(new Request(path, {method: 'HEAD'})) - .then(function(response) { - return response.ok; - }); + .then(response => { + return response.ok; + }); + }, + + startWorkInProgress(changeNum, opt_message) { + const payload = {}; + if (opt_message) { + payload.message = opt_message; + } + const url = this.getChangeActionURL(changeNum, null, '/wip'); + return this.send('POST', url, payload) + .then(response => { + if (response.status === 204) { + return 'Change marked as Work In Progress.'; + } + }); + }, + + startReview(changeNum, review) { + return this.send( + 'POST', this.getChangeActionURL(changeNum, null, '/ready'), review); + }, + + deleteComment(changeNum, patchNum, commentID, reason) { + const url = this.changeBaseURL(changeNum, patchNum) + + '/comments/' + commentID + '/delete'; + return this.send('POST', url, {reason}).then(response => + this.getResponseObject(response)); }, }); })();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html index 0ff162d..b185933 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -20,9 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> <link rel="import" href="gr-rest-api-interface.html"> <script>void(0);</script> @@ -34,63 +34,63 @@ </test-fixture> <script> - suite('gr-rest-api-interface tests', function() { - var element; - var sandbox; + suite('gr-rest-api-interface tests', () => { + let element; + let sandbox; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); element = fixture('basic'); - var testJSON = ')]}\'\n{"hello": "bonjour"}'; + const testJSON = ')]}\'\n{"hello": "bonjour"}'; sandbox.stub(window, 'fetch').returns(Promise.resolve({ ok: true, - text: function() { + text() { return Promise.resolve(testJSON); }, })); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('JSON prefix is properly removed', function(done) { - element.fetchJSON('/dummy/url').then(function(obj) { + test('JSON prefix is properly removed', done => { + element.fetchJSON('/dummy/url').then(obj => { assert.deepEqual(obj, {hello: 'bonjour'}); done(); }); }); - test('cached results', function(done) { - var n = 0; - sandbox.stub(element, 'fetchJSON', function() { + test('cached results', done => { + let n = 0; + sandbox.stub(element, 'fetchJSON', () => { return Promise.resolve(++n); }); - var promises = []; + const promises = []; promises.push(element._fetchSharedCacheURL('/foo')); promises.push(element._fetchSharedCacheURL('/foo')); promises.push(element._fetchSharedCacheURL('/foo')); - Promise.all(promises).then(function(results) { + Promise.all(promises).then(results => { assert.deepEqual(results, [1, 1, 1]); - element._fetchSharedCacheURL('/foo').then(function(foo) { + element._fetchSharedCacheURL('/foo').then(foo => { assert.equal(foo, 1); done(); }); }); }); - test('cached promise', function(done) { - var promise = Promise.reject('foo'); + test('cached promise', done => { + const promise = Promise.reject('foo'); element._cache['/foo'] = promise; - element._fetchSharedCacheURL('/foo').catch(function(p) { + element._fetchSharedCacheURL('/foo').catch(p => { assert.equal(p, 'foo'); done(); }); }); - test('params are properly encoded', function() { - var url = element._urlWithParams('/path/', { + test('params are properly encoded', () => { + let url = element._urlWithParams('/path/', { sp: 'hola', gr: 'guten tag', noval: null, @@ -110,23 +110,23 @@ assert.equal(url, '/path/?l=c&l=b&l=a'); }); - test('request callbacks can be canceled', function(done) { - var cancelCalled = false; + test('request callbacks can be canceled', done => { + let cancelCalled = false; window.fetch.returns(Promise.resolve({ body: { - cancel: function() { cancelCalled = true; }, + cancel() { cancelCalled = true; }, }, })); - element.fetchJSON('/dummy/url', null, function() { return true; }).then( - function(obj) { - assert.isUndefined(obj); - assert.isTrue(cancelCalled); - done(); - }); + element.fetchJSON('/dummy/url', null, () => { return true; }).then( + obj => { + assert.isUndefined(obj); + assert.isTrue(cancelCalled); + done(); + }); }); - test('parent diff comments are properly grouped', function(done) { - sandbox.stub(element, 'fetchJSON', function() { + test('parent diff comments are properly grouped', done => { + sandbox.stub(element, 'fetchJSON', () => { return Promise.resolve({ '/COMMIT_MSG': [], 'sieve.go': [ @@ -143,26 +143,26 @@ }); }); element._getDiffComments('42', '', 'PARENT', 1, 'sieve.go').then( - function(obj) { - assert.equal(obj.baseComments.length, 1); - assert.deepEqual(obj.baseComments[0], { - side: 'PARENT', - message: 'how did this work in the first place?', - path: 'sieve.go', - updated: '2017-02-03 22:33:28.000000000', + obj => { + assert.equal(obj.baseComments.length, 1); + assert.deepEqual(obj.baseComments[0], { + side: 'PARENT', + message: 'how did this work in the first place?', + path: 'sieve.go', + updated: '2017-02-03 22:33:28.000000000', + }); + assert.equal(obj.comments.length, 1); + assert.deepEqual(obj.comments[0], { + message: 'this isn’t quite right', + path: 'sieve.go', + updated: '2017-02-03 22:32:28.000000000', + }); + done(); }); - assert.equal(obj.comments.length, 1); - assert.deepEqual(obj.comments[0], { - message: 'this isn’t quite right', - path: 'sieve.go', - updated: '2017-02-03 22:32:28.000000000', - }); - done(); - }); }); - test('_setRange', function() { - var comments = [ + test('_setRange', () => { + const comments = [ { id: 1, side: 'PARENT', @@ -182,7 +182,7 @@ updated: '2017-02-03 22:33:28.000000000', }, ]; - var expectedResult = { + const expectedResult = { id: 2, in_reply_to: 1, message: 'this isn’t quite right', @@ -194,12 +194,12 @@ end_character: 1, }, }; - var comment = comments[1]; + const comment = comments[1]; assert.deepEqual(element._setRange(comments, comment), expectedResult); }); - test('_setRanges', function() { - var comments = [ + test('_setRanges', () => { + const comments = [ { id: 3, in_reply_to: 2, @@ -225,7 +225,7 @@ }, }, ]; - var expectedResult = [ + const expectedResult = [ { id: 1, side: 'PARENT', @@ -266,8 +266,8 @@ assert.deepEqual(element._setRanges(comments), expectedResult); }); - test('differing patch diff comments are properly grouped', function(done) { - sandbox.stub(element, 'fetchJSON', function(url) { + test('differing patch diff comments are properly grouped', done => { + sandbox.stub(element, 'fetchJSON', url => { if (url == '/changes/42/revisions/1') { return Promise.resolve({ '/COMMIT_MSG': [], @@ -305,29 +305,29 @@ } }); element._getDiffComments('42', '', 1, 2, 'sieve.go').then( - function(obj) { - assert.equal(obj.baseComments.length, 1); - assert.deepEqual(obj.baseComments[0], { - message: 'this isn’t quite right', - path: 'sieve.go', - updated: '2017-02-03 22:32:28.000000000', + obj => { + assert.equal(obj.baseComments.length, 1); + assert.deepEqual(obj.baseComments[0], { + message: 'this isn’t quite right', + path: 'sieve.go', + updated: '2017-02-03 22:32:28.000000000', + }); + assert.equal(obj.comments.length, 2); + assert.deepEqual(obj.comments[0], { + message: 'What on earth are you thinking, here?', + path: 'sieve.go', + updated: '2017-02-03 22:32:28.000000000', + }); + assert.deepEqual(obj.comments[1], { + message: '¯\\_(ツ)_/¯', + path: 'sieve.go', + updated: '2017-02-04 22:33:28.000000000', + }); + done(); }); - assert.equal(obj.comments.length, 2); - assert.deepEqual(obj.comments[0], { - message: 'What on earth are you thinking, here?', - path: 'sieve.go', - updated: '2017-02-03 22:32:28.000000000', - }); - assert.deepEqual(obj.comments[1], { - message: '¯\\_(ツ)_/¯', - path: 'sieve.go', - updated: '2017-02-04 22:33:28.000000000', - }); - done(); - }); }); - test('special file path sorting', function() { + test('special file path sorting', () => { assert.deepEqual( ['.b', '/COMMIT_MSG', '.a', 'file'].sort( element.specialFilePathCompare), @@ -354,13 +354,13 @@ ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']); // Regression test for Issue 4448. - assert.deepEqual([ - 'minidump/minidump_memory_writer.cc', - 'minidump/minidump_memory_writer.h', - 'minidump/minidump_thread_writer.cc', - 'minidump/minidump_thread_writer.h', - ] - .sort(element.specialFilePathCompare), + assert.deepEqual( + [ + 'minidump/minidump_memory_writer.cc', + 'minidump/minidump_memory_writer.h', + 'minidump/minidump_thread_writer.cc', + 'minidump/minidump_thread_writer.h', + ].sort(element.specialFilePathCompare), [ 'minidump/minidump_memory_writer.h', 'minidump/minidump_memory_writer.cc', @@ -369,102 +369,102 @@ ]); // Regression test for Issue 4545. - assert.deepEqual([ - 'task_test.go', - 'task.go', - ] - .sort(element.specialFilePathCompare), + assert.deepEqual( + [ + 'task_test.go', + 'task.go', + ].sort(element.specialFilePathCompare), [ 'task.go', 'task_test.go', ]); }); - suite('rebase action', function() { - var resolveFetchJSON; - setup(function() { + suite('rebase action', () => { + let resolveFetchJSON; + setup(() => { sandbox.stub(element, 'fetchJSON').returns( - new Promise(function(resolve) { + new Promise(resolve => { resolveFetchJSON = resolve; })); }); - test('no rebase on current', function(done) { + test('no rebase on current', done => { element.getChangeRevisionActions('42', '1337').then( - function(response) { - assert.isTrue(response.rebase.enabled); - assert.isFalse(response.rebase.rebaseOnCurrent); - done(); - }); + response => { + assert.isTrue(response.rebase.enabled); + assert.isFalse(response.rebase.rebaseOnCurrent); + done(); + }); resolveFetchJSON({rebase: {}}); }); - test('rebase on current', function(done) { + test('rebase on current', done => { element.getChangeRevisionActions('42', '1337').then( - function(response) { - assert.isTrue(response.rebase.enabled); - assert.isTrue(response.rebase.rebaseOnCurrent); - done(); - }); + response => { + assert.isTrue(response.rebase.enabled); + assert.isTrue(response.rebase.rebaseOnCurrent); + done(); + }); resolveFetchJSON({rebase: {enabled: true}}); }); }); - test('server error', function(done) { - var getResponseObjectStub = sandbox.stub(element, 'getResponseObject'); + test('server error', done => { + const getResponseObjectStub = sandbox.stub(element, 'getResponseObject'); window.fetch.returns(Promise.resolve({ok: false})); - var serverErrorEventPromise = new Promise(function(resolve) { - element.addEventListener('server-error', function() { resolve(); }); + const serverErrorEventPromise = new Promise(resolve => { + element.addEventListener('server-error', () => { resolve(); }); }); element.fetchJSON().then( - function(response) { + response => { assert.isUndefined(response); assert.isTrue(getResponseObjectStub.notCalled); - serverErrorEventPromise.then(function() { + serverErrorEventPromise.then(() => { done(); }); }); }); - test('checkCredentials', function(done) { - var responses = [ + test('checkCredentials', done => { + const responses = [ { ok: false, status: 403, - text: function() { return Promise.resolve(); }, + text() { return Promise.resolve(); }, }, { ok: true, status: 200, - text: function() { return Promise.resolve(')]}\'{}'); }, + text() { return Promise.resolve(')]}\'{}'); }, }, ]; window.fetch.restore(); - sandbox.stub(window, 'fetch', function(url) { + sandbox.stub(window, 'fetch', url => { if (url === '/accounts/self/detail') { return Promise.resolve(responses.shift()); } }); - element.getLoggedIn().then(function(account) { + element.getLoggedIn().then(account => { assert.isNotOk(account); - element.checkCredentials().then(function(account) { + element.checkCredentials().then(account => { assert.isOk(account); done(); }); }); }); - test('legacy n,z key in change url is replaced', function() { - var stub = sandbox.stub(element, 'fetchJSON'); + test('legacy n,z key in change url is replaced', () => { + const stub = sandbox.stub(element, 'fetchJSON'); element.getChanges(1, null, 'n,z'); assert.equal(stub.args[0][3].S, 0); }); - test('saveDiffPreferences invalidates cache line', function() { - var cacheKey = '/accounts/self/preferences.diff'; + test('saveDiffPreferences invalidates cache line', () => { + const cacheKey = '/accounts/self/preferences.diff'; sandbox.stub(element, 'send'); element._cache[cacheKey] = {tab_size: 4}; element.saveDiffPreferences({tab_size: 8}); @@ -472,108 +472,106 @@ assert.notOk(element._cache[cacheKey]); }); - var preferenceSetup = function(testJSON, loggedIn, smallScreen) { - sandbox.stub(element, 'getLoggedIn', function() { + const preferenceSetup = function(testJSON, loggedIn, smallScreen) { + sandbox.stub(element, 'getLoggedIn', () => { return Promise.resolve(loggedIn); }); - sandbox.stub(element, '_isNarrowScreen', function() { + sandbox.stub(element, '_isNarrowScreen', () => { return smallScreen; }); - sandbox.stub(element, '_fetchSharedCacheURL', function() { + sandbox.stub(element, '_fetchSharedCacheURL', () => { return Promise.resolve(testJSON); }); }; test('getPreferences returns correctly on small screens logged in', - function(done) { + done => { + const testJSON = {diff_view: 'SIDE_BY_SIDE'}; + const loggedIn = true; + const smallScreen = true; - var testJSON = {diff_view: 'SIDE_BY_SIDE'}; - var loggedIn = true; - var smallScreen = true; + preferenceSetup(testJSON, loggedIn, smallScreen); - preferenceSetup(testJSON, loggedIn, smallScreen); - - element.getPreferences().then(function(obj) { - assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); - assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); - done(); - }); - }); + element.getPreferences().then(obj => { + assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); + assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); + done(); + }); + }); test('getPreferences returns correctly on small screens not logged in', - function(done) { + done => { + const testJSON = {diff_view: 'SIDE_BY_SIDE'}; + const loggedIn = false; + const smallScreen = true; - var testJSON = {diff_view: 'SIDE_BY_SIDE'}; - var loggedIn = false; - var smallScreen = true; - - preferenceSetup(testJSON, loggedIn, smallScreen); - element.getPreferences().then(function(obj) { - assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); - assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); - done(); - }); - }); + preferenceSetup(testJSON, loggedIn, smallScreen); + element.getPreferences().then(obj => { + assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); + assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); + done(); + }); + }); test('getPreferences returns correctly on larger screens logged in', - function(done) { - var testJSON = {diff_view: 'UNIFIED_DIFF'}; - var loggedIn = true; - var smallScreen = false; + done => { + const testJSON = {diff_view: 'UNIFIED_DIFF'}; + const loggedIn = true; + const smallScreen = false; - preferenceSetup(testJSON, loggedIn, smallScreen); + preferenceSetup(testJSON, loggedIn, smallScreen); - element.getPreferences().then(function(obj) { - assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); - assert.equal(obj.diff_view, 'UNIFIED_DIFF'); - done(); - }); - }); + element.getPreferences().then(obj => { + assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); + assert.equal(obj.diff_view, 'UNIFIED_DIFF'); + done(); + }); + }); test('getPreferences returns correctly on larger screens not logged in', - function(done) { - var testJSON = {diff_view: 'UNIFIED_DIFF'}; - var loggedIn = false; - var smallScreen = false; + done => { + const testJSON = {diff_view: 'UNIFIED_DIFF'}; + const loggedIn = false; + const smallScreen = false; - preferenceSetup(testJSON, loggedIn, smallScreen); + preferenceSetup(testJSON, loggedIn, smallScreen); - element.getPreferences().then(function(obj) { - assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE'); - assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); - done(); - }); - }); + element.getPreferences().then(obj => { + assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE'); + assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); + done(); + }); + }); - test('savPreferences normalizes download scheme', function() { + test('savPreferences normalizes download scheme', () => { sandbox.stub(element, 'send'); element.savePreferences({download_scheme: 'HTTP'}); assert.isTrue(element.send.called); assert.equal(element.send.lastCall.args[2].download_scheme, 'http'); }); - test('confirmEmail', function() { + test('confirmEmail', () => { sandbox.spy(element, 'send'); element.confirmEmail('foo'); assert.isTrue(element.send.calledWith( 'PUT', '/config/server/email.confirm', {token: 'foo'})); }); - test('GrReviewerUpdatesParser.parse is used', function() { + test('GrReviewerUpdatesParser.parse is used', () => { sandbox.stub(GrReviewerUpdatesParser, 'parse').returns( Promise.resolve('foo')); - return element.getChangeDetail(42).then(function(result) { + return element.getChangeDetail(42).then(result => { assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce); assert.equal(result, 'foo'); }); }); - test('setAccountStatus', function(done) { + test('setAccountStatus', done => { sandbox.stub(element, 'send').returns(Promise.resolve('OOO')); sandbox.stub(element, 'getResponseObject') .returns(Promise.resolve('OOO')); element._cache['/accounts/self/detail'] = {}; - element.setAccountStatus('OOO').then(function() { + element.setAccountStatus('OOO').then(() => { assert.isTrue(element.send.calledWith('PUT', '/accounts/self/status', {status: 'OOO'})); assert.deepEqual(element._cache['/accounts/self/detail'], @@ -582,24 +580,24 @@ }); }); - test('_sendDiffDraft pending requests tracked', function(done) { - sandbox.stub(element, 'send', function() { + test('_sendDiffDraft pending requests tracked', done => { + sandbox.stub(element, 'send', () => { assert.equal(element._pendingRequests.sendDiffDraft, 1); return Promise.resolve([]); }); - element.saveDiffDraft('', 1, 1).then(function() { + element.saveDiffDraft('', 1, 1).then(() => { assert.equal(element._pendingRequests.sendDiffDraft, 0); - element.deleteDiffDraft('', 1, 1).then(function() { + element.deleteDiffDraft('', 1, 1).then(() => { assert.equal(element._pendingRequests.sendDiffDraft, 0); done(); }); }); }); - test('saveChangeEdit', function(done) { - var change_num = '1'; - var file_name = 'index.php'; - var file_contents = '<?php'; + test('saveChangeEdit', done => { + const change_num = '1'; + const file_name = 'index.php'; + const file_contents = '<?php'; sandbox.stub(element, 'send').returns( Promise.resolve([change_num, file_name, file_contents]) ); @@ -607,7 +605,7 @@ .returns(Promise.resolve([change_num, file_name, file_contents])); element._cache['/changes/' + change_num + '/edit/' + file_name] = {}; element.saveChangeEdit(change_num, file_name, file_contents).then( - function() { + () => { assert.isTrue(element.send.calledWith('PUT', '/changes/' + change_num + '/edit/' + file_name, file_contents)); @@ -615,5 +613,83 @@ } ); }); + + test('putChangeCommitMessage', done => { + const change_num = '1'; + const message = 'this is a commit message'; + sandbox.stub(element, 'send').returns( + Promise.resolve([change_num, message]) + ); + sandbox.stub(element, 'getResponseObject') + .returns(Promise.resolve([change_num, message])); + element._cache['/changes/' + change_num + '/message'] = {}; + element.putChangeCommitMessage(change_num, message).then( + () => { + assert.isTrue(element.send.calledWith('PUT', + '/changes/' + change_num + '/message', {message})); + done(); + } + ); + }); + + test('startWorkInProgress', () => { + sandbox.stub(element, 'send').returns(Promise.resolve('ok')); + element.startWorkInProgress('42'); + assert.isTrue(element.send.calledWith( + 'POST', '/changes/42/wip', {})); + element.startWorkInProgress('42', 'revising...'); + assert.isTrue(element.send.calledWith( + 'POST', '/changes/42/wip', {message: 'revising...'})); + }); + + test('startReview', () => { + sandbox.stub(element, 'send').returns(Promise.resolve({})); + element.startReview('42', {message: 'Please review.'}); + assert.isTrue(element.send.calledWith( + 'POST', '/changes/42/ready', {message: 'Please review.'})); + }); + + test('deleteComment', done => { + sandbox.stub(element, 'send').returns(Promise.resolve()); + sandbox.stub(element, 'getResponseObject').returns('some response'); + element.deleteComment('foo', 'bar', '01234', 'removal reason') + .then(response => { + assert.equal(response, 'some response'); + done(); + }); + assert.isTrue(element.send.calledWith( + 'POST', '/changes/foo/revisions/bar/comments/01234/delete', + {reason: 'removal reason'})); + }); + + test('getProjects', () => { + sandbox.stub(element, '_fetchSharedCacheURL'); + element.getProjects('test', 25); + assert.isTrue(element._fetchSharedCacheURL.lastCall + .calledWithExactly('/projects/?d&n=26&S=0&m=test')); + + element.getProjects(null, 25); + assert.isTrue(element._fetchSharedCacheURL.lastCall + .calledWithExactly('/projects/?d&n=26&S=0')); + + element.getProjects('test', 25, 25); + assert.isTrue(element._fetchSharedCacheURL.lastCall + .calledWithExactly('/projects/?d&n=26&S=25&m=test')); + }); + + test('gerrit auth is used by default', () => { + sandbox.stub(GrGerritAuth.prototype, 'fetch').returns(Promise.resolve()); + element.fetchJSON('foo'); + assert(GrGerritAuth.prototype.fetch.called); + }); + + test('gapi auth is enabled with USE_GAPI_AUTH', () => { + window.USE_GAPI_AUTH = true; + sandbox.stub(GrGapiAuth.prototype, 'fetch').returns(Promise.resolve()); + element = fixture('basic'); + element.fetchJSON('foo'); + assert(GrGapiAuth.prototype.fetch.called); + delete window.USE_GAPI_AUTH; + }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js index 21a6bc6..c119cdf 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js
@@ -21,7 +21,7 @@ // TODO (viktard): Polyfill Object.assign for IE. this.result = Object.assign({}, change); this._lastState = {}; - }; + } GrReviewerUpdatesParser.parse = function(change) { if (!change || @@ -30,13 +30,15 @@ !change.reviewer_updates.length) { return change; } - var parser = new GrReviewerUpdatesParser(change); + const parser = new GrReviewerUpdatesParser(change); parser._filterRemovedMessages(); parser._groupUpdates(); parser._formatUpdates(); + parser._advanceUpdates(); return parser.result; }; + GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS = 500; GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS = 6000; GrReviewerUpdatesParser.prototype.result = null; @@ -49,7 +51,7 @@ * are used. */ GrReviewerUpdatesParser.prototype._filterRemovedMessages = function() { - this.result.messages = this.result.messages.filter(function(message) { + this.result.messages = this.result.messages.filter(message => { return message.tag !== 'autogenerated:gerrit:deleteReviewer'; }); }; @@ -74,10 +76,10 @@ * @param {Object} update instance of ReviewerUpdateInfo */ GrReviewerUpdatesParser.prototype._completeBatch = function(update) { - var items = []; - for (var accountId in this._updateItems) { + const items = []; + for (const accountId in this._updateItems) { if (!this._updateItems.hasOwnProperty(accountId)) continue; - var updateItem = this._updateItems[accountId]; + const updateItem = this._updateItems[accountId]; if (this._lastState[accountId] !== updateItem.state) { this._lastState[accountId] = updateItem.state; items.push(updateItem); @@ -96,14 +98,14 @@ * - Groups with no-change updates are discarded (eg CC -> CC) */ GrReviewerUpdatesParser.prototype._groupUpdates = function() { - var updates = this.result.reviewer_updates; - var newUpdates = updates.reduce(function(newUpdates, update) { + const updates = this.result.reviewer_updates; + const newUpdates = updates.reduce((newUpdates, update) => { if (!this._batch) { this._batch = this._startBatch(update); } - var updateDate = util.parseDate(update.updated).getTime(); - var batchUpdateDate = util.parseDate(this._batch.date).getTime(); - var reviewerId = update.reviewer._account_id.toString(); + const updateDate = util.parseDate(update.updated).getTime(); + const batchUpdateDate = util.parseDate(this._batch.date).getTime(); + const reviewerId = update.reviewer._account_id.toString(); if (updateDate - batchUpdateDate > GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS || update.updated_by._account_id !== this._batch.author._account_id) { @@ -122,7 +124,7 @@ this._updateItems[reviewerId].prev_state = this._lastState[reviewerId]; } return newUpdates; - }.bind(this), []); + }, []); this._completeBatch(); if (this._batch.updates && this._batch.updates.length) { newUpdates.push(this._batch); @@ -157,14 +159,14 @@ * @return {!Object} Hash of arrays of AccountInfo, message as key. */ GrReviewerUpdatesParser.prototype._groupUpdatesByMessage = function(updates) { - return updates.reduce(function(result, item) { - var message = this._getUpdateMessage(item.prev_state, item.state); + return updates.reduce((result, item) => { + const message = this._getUpdateMessage(item.prev_state, item.state); if (!result[message]) { result[message] = []; } result[message].push(item.reviewer); return result; - }.bind(this), {}); + }, {}); }; /** @@ -173,21 +175,48 @@ * @see https://gerrit-review.googlesource.com/c/94490/ */ GrReviewerUpdatesParser.prototype._formatUpdates = function() { - this.result.reviewer_updates.forEach(function(update) { - var grouppedReviewers = this._groupUpdatesByMessage(update.updates); - var newUpdates = []; - for (var message in grouppedReviewers) { + for (const update of this.result.reviewer_updates) { + const grouppedReviewers = this._groupUpdatesByMessage(update.updates); + const newUpdates = []; + for (const message in grouppedReviewers) { if (grouppedReviewers.hasOwnProperty(message)) { newUpdates.push({ - message: message, + message, reviewers: grouppedReviewers[message], }); } } update.updates = newUpdates; - }.bind(this)); + } + }; + + /** + * Moves reviewer updates that are within short time frame of change messages + * back in time so they would come before change messages. + * TODO(viktard): Remove when server-side serves reviewer updates like so. + */ + GrReviewerUpdatesParser.prototype._advanceUpdates = function() { + const updates = this.result.reviewer_updates; + const messages = this.result.messages; + messages.forEach((message, index) => { + const messageDate = util.parseDate(message.date).getTime(); + const nextMessageDate = index === messages.length - 1 ? null : + util.parseDate(messages[index + 1].date).getTime(); + for (const update of updates) { + const date = util.parseDate(update.date).getTime(); + if (date >= messageDate + && (!nextMessageDate || date < nextMessageDate)) { + const timestamp = util.parseDate(update.date).getTime() - + GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS; + update.date = new Date(timestamp) + .toISOString().replace('T', ' ').replace('Z', '000000'); + } + if (nextMessageDate && date > nextMessageDate) { + break; + } + } + }); }; window.GrReviewerUpdatesParser = GrReviewerUpdatesParser; - })(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html index 1ae04a0..bf082e8 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -20,27 +20,25 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - -<link rel="import" href="../../../bower_components/iron-test-helpers/iron-test-helpers.html"> - +<link rel="import" href="../../../test/common-test-setup.html"/> <script src="../../../scripts/util.js"></script> <script src="gr-reviewer-updates-parser.js"></script> <script> - suite('gr-reviewer-updates-parser tests', function() { - var sandbox; - var instance; + suite('gr-reviewer-updates-parser tests', () => { + let sandbox; + let instance; - setup(function() { + setup(() => { sandbox = sinon.sandbox.create(); }); - teardown(function() { + teardown(() => { sandbox.restore(); }); - test('ignores changes without messages', function() { - var change = {}; + test('ignores changes without messages', () => { + const change = {}; sandbox.stub( GrReviewerUpdatesParser.prototype, '_filterRemovedMessages'); sandbox.stub( @@ -56,8 +54,8 @@ GrReviewerUpdatesParser.prototype._formatUpdates.called); }); - test('ignores changes without reviewer updates', function() { - var change = { + test('ignores changes without reviewer updates', () => { + const change = { messages: [], }; sandbox.stub( @@ -75,8 +73,8 @@ GrReviewerUpdatesParser.prototype._formatUpdates.called); }); - test('ignores changes with empty reviewer updates', function() { - var change = { + test('ignores changes with empty reviewer updates', () => { + const change = { messages: [], reviewer_updates: [], }; @@ -95,18 +93,18 @@ GrReviewerUpdatesParser.prototype._formatUpdates.called); }); - test('filter removed messages', function() { - var change = { - messages: [ - { - message: 'msg1', - tag: 'autogenerated:gerrit:deleteReviewer', - }, - { - message: 'msg2', - tag: 'foo', - } - ], + test('filter removed messages', () => { + const change = { + messages: [ + { + message: 'msg1', + tag: 'autogenerated:gerrit:deleteReviewer', + }, + { + message: 'msg2', + tag: 'foo', + }, + ], }; instance = new GrReviewerUpdatesParser(change); instance._filterRemovedMessages(); @@ -118,22 +116,22 @@ }); }); - test('group reviewer updates', function() { - var reviewer1 = {_account_id: 1}; - var reviewer2 = {_account_id: 2}; - var date1 = '2017-01-26 12:11:50.000000000'; - var date2 = '2017-01-26 12:11:55.000000000'; // Within threshold. - var date3 = '2017-01-26 12:33:50.000000000'; - var date4 = '2017-01-26 12:44:50.000000000'; - var makeItem = function(state, reviewer, opt_date, opt_author) { + test('group reviewer updates', () => { + const reviewer1 = {_account_id: 1}; + const reviewer2 = {_account_id: 2}; + const date1 = '2017-01-26 12:11:50.000000000'; + const date2 = '2017-01-26 12:11:55.000000000'; // Within threshold. + const date3 = '2017-01-26 12:33:50.000000000'; + const date4 = '2017-01-26 12:44:50.000000000'; + const makeItem = function(state, reviewer, opt_date, opt_author) { return { - reviewer: reviewer, + reviewer, updated: opt_date || date1, updated_by: opt_author || reviewer1, - state: state, + state, }; }; - var change = { + let change = { reviewer_updates: [ makeItem('REVIEWER', reviewer1), // New group. makeItem('CC', reviewer2), // Appended. @@ -198,36 +196,36 @@ ]); }); - test('format reviewer updates', function() { - var reviewer1 = {_account_id: 1}; - var reviewer2 = {_account_id: 2}; - var makeItem = function(prev, state, opt_reviewer) { + test('format reviewer updates', () => { + const reviewer1 = {_account_id: 1}; + const reviewer2 = {_account_id: 2}; + const makeItem = function(prev, state, opt_reviewer) { return { reviewer: opt_reviewer || reviewer1, prev_state: prev, - state: state, + state, }; }; - var makeUpdate = function(items) { + const makeUpdate = function(items) { return { author: reviewer1, updated: '', updates: items, }; }; - var change = { - reviewer_updates: [ - makeUpdate([ - makeItem(undefined, 'CC'), - makeItem(undefined, 'CC', reviewer2) - ]), - makeUpdate([ - makeItem('CC', 'REVIEWER'), - makeItem('REVIEWER', 'REMOVED'), - makeItem('REMOVED', 'REVIEWER'), - makeItem(undefined, 'REVIEWER', reviewer2), - ]), - ], + const change = { + reviewer_updates: [ + makeUpdate([ + makeItem(undefined, 'CC'), + makeItem(undefined, 'CC', reviewer2), + ]), + makeUpdate([ + makeItem('CC', 'REVIEWER'), + makeItem('REVIEWER', 'REMOVED'), + makeItem('REMOVED', 'REVIEWER'), + makeItem(undefined, 'REVIEWER', reviewer2), + ]), + ], }; instance = new GrReviewerUpdatesParser(change); @@ -237,7 +235,7 @@ assert.equal(change.reviewer_updates[0].updates.length, 1); assert.equal(change.reviewer_updates[1].updates.length, 3); - var items = change.reviewer_updates[0].updates; + let items = change.reviewer_updates[0].updates; assert.equal(items[0].message, 'added to CC: '); assert.deepEqual(items[0].reviewers, [reviewer1, reviewer2]); @@ -249,5 +247,56 @@ assert.equal(items[2].message, 'added to REVIEWER: '); assert.deepEqual(items[2].reviewers, [reviewer1, reviewer2]); }); + + test('_advanceUpdates', () => { + const T0 = util.parseDate('2017-02-17 19:04:18.000000000').getTime(); + const tplus = delta => { + return new Date(T0 + delta) + .toISOString().replace('T', ' ').replace('Z', '000000'); + }; + const change = { + reviewer_updates: [{ + date: tplus(0), + type: 'REVIEWER_UPDATE', + updates: [{ + message: 'same time update', + }], + }, { + date: tplus(200), + type: 'REVIEWER_UPDATE', + updates: [{ + message: 'update within threshold', + }], + }, { + date: tplus(600), + type: 'REVIEWER_UPDATE', + updates: [{ + message: 'update between messages', + }], + }, { + date: tplus(1000), + type: 'REVIEWER_UPDATE', + updates: [{ + message: 'late update', + }], + }], + messages: [{ + id: '6734489eb9d642de28dbf2bcf9bda875923800d8', + date: tplus(0), + message: 'Uploaded patch set 1.', + }, { + id: '6734489eb9d642de28dbf2bcf9bda875923800d8', + date: tplus(800), + message: 'Uploaded patch set 2.', + }], + }; + instance = new GrReviewerUpdatesParser(change); + instance._advanceUpdates(); + const updates = instance.result.reviewer_updates; + assert.isBelow(util.parseDate(updates[0].date).getTime(), T0); + assert.isBelow(util.parseDate(updates[1].date).getTime(), T0); + assert.equal(updates[2].date, tplus(100)); + assert.equal(updates[3].date, tplus(500)); + }); }); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html index 8141c8a..add07ea 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.html
@@ -15,51 +15,51 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> - +<link rel="import" href="../../../test/common-test-setup.html"/> <dom-module id="mock-diff-response"> <template></template> <script> (function() { 'use strict'; - var RESPONSE = { - 'meta_a': { - 'name': 'lorem-ipsum.txt', - 'content_type': 'text/plain', - 'lines': 45, + const RESPONSE = { + meta_a: { + name: 'lorem-ipsum.txt', + content_type: 'text/plain', + lines: 45, }, - 'meta_b': { - 'name': 'lorem-ipsum.txt', - 'content_type': 'text/plain', - 'lines': 48, + meta_b: { + name: 'lorem-ipsum.txt', + content_type: 'text/plain', + lines: 48, }, - 'intraline_status': 'OK', - 'change_type': 'MODIFIED', - 'diff_header': [ + intraline_status: 'OK', + change_type: 'MODIFIED', + diff_header: [ 'diff --git a/lorem-ipsum.txt b/lorem-ipsum.txt', 'index b2adcf4..554ae49 100644', '--- a/lorem-ipsum.txt', '+++ b/lorem-ipsum.txt', ], - 'content': [ + content: [ { - 'ab': [ + ab: [ 'Lorem ipsum dolor sit amet, suspendisse inceptos vehicula, ' + 'nulla phasellus.', 'Mattis lectus.', 'Sodales duis.', 'Orci a faucibus.', - ] + ], }, { - 'b': [ + b: [ 'Nullam neque, ligula ac, id blandit.', 'Sagittis tincidunt torquent, tempor nunc amet.', 'At rhoncus id.', ], }, { - 'ab': [ + ab: [ 'Sem nascetur, erat ut, non in.', 'A donec, venenatis pellentesque dis.', 'Mauris mauris.', @@ -68,7 +68,7 @@ ], }, { - 'a': [ + a: [ 'Est amet, vestibulum pellentesque.', 'Erat ligula.', 'Justo eros.', @@ -76,25 +76,25 @@ ], }, { - 'ab': [ + ab: [ 'Arcu eget, rhoncus amet cursus, ipsum elementum.', 'Eros suspendisse.', ], }, { - 'a': [ + a: [ 'Rhoncus tempor, ultricies aliquam ipsum.', ], - 'b': [ + b: [ 'Rhoncus tempor, ultricies praesent ipsum.', ], - 'edit_a': [ + edit_a: [ [ 26, 7, ], ], - 'edit_b': [ + edit_b: [ [ 26, 8, @@ -102,7 +102,7 @@ ], }, { - 'ab': [ + ab: [ 'Sollicitudin duis.', 'Blandit blandit, ante nisl fusce.', 'Felis ac at, tellus consectetuer.', @@ -131,7 +131,7 @@ ], }, { - 'b': [ + b: [ 'Eu congue risus.', 'Enim ac, quis elementum.', 'Non et elit.', @@ -139,7 +139,7 @@ ], }, { - 'ab': [ + ab: [ 'Nec at.', 'Arcu mauris, venenatis lacus fermentum, praesent duis.', 'Pellentesque amet et, tellus duis.', @@ -155,7 +155,7 @@ properties: { diffResponse: { type: Object, - value: function() { + value() { return RESPONSE; }, },
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js index bef260e9..09787cb 100644 --- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js +++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -26,21 +26,21 @@ }, listeners: { - change: '_valueChanged', + 'change': '_valueChanged', 'dom-change': '_updateValue', }, - _updateValue: function() { + _updateValue() { if (this.bindValue) { this.value = this.bindValue; } }, - _valueChanged: function() { + _valueChanged() { this.bindValue = this.value; }, - ready: function() { + ready() { // If not set via the property, set bind-value to the element value. if (!this.bindValue) { this.bindValue = this.value; } },
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html index bd22505..fa8a54c 100644 --- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html +++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
@@ -20,7 +20,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-select.html"> <script>void(0);</script> @@ -36,15 +36,15 @@ </test-fixture> <script> - suite('gr-select tests', function() { - var element; + suite('gr-select tests', () => { + let element; - setup(function() { + setup(() => { element = fixture('basic'); }); - test('bidirectional binding property-to-attribute', function() { - var changeStub = sinon.stub(); + test('bidirectional binding property-to-attribute', () => { + const changeStub = sinon.stub(); element.addEventListener('bind-value-changed', changeStub); // The selected element should be the first one by default. @@ -61,8 +61,8 @@ assert.isTrue(changeStub.called); }); - test('bidirectional binding attribute-to-property', function() { - var changeStub = sinon.stub(); + test('bidirectional binding attribute-to-property', () => { + const changeStub = sinon.stub(); element.addEventListener('bind-value-changed', changeStub); // The selected element should be the first one by default.
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js index 77d1c05..2e7f17c 100644 --- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js +++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -15,10 +15,10 @@ 'use strict'; // Date cutoff is one day: - var DRAFT_MAX_AGE = 24 * 60 * 60 * 1000; + const DRAFT_MAX_AGE = 24 * 60 * 60 * 1000; // Clean up old entries no more frequently than one day. - var CLEANUP_THROTTLE_INTERVAL = 24 * 60 * 60 * 1000; + const CLEANUP_THROTTLE_INTERVAL = 24 * 60 * 60 * 1000; Polymer({ is: 'gr-storage', @@ -27,7 +27,7 @@ _lastCleanup: Number, _storage: { type: Object, - value: function() { + value() { return window.localStorage; }, }, @@ -37,42 +37,43 @@ }, }, - getDraftComment: function(location) { + getDraftComment(location) { this._cleanupDrafts(); return this._getObject(this._getDraftKey(location)); }, - setDraftComment: function(location, message) { - var key = this._getDraftKey(location); - this._setObject(key, {message: message, updated: Date.now()}); + setDraftComment(location, message) { + const key = this._getDraftKey(location); + this._setObject(key, {message, updated: Date.now()}); }, - eraseDraftComment: function(location) { - var key = this._getDraftKey(location); + eraseDraftComment(location) { + const key = this._getDraftKey(location); this._storage.removeItem(key); }, - getPreferences: function() { + getPreferences() { return this._getObject('localPrefs'); }, - savePreferences: function(localPrefs) { + savePreferences(localPrefs) { this._setObject('localPrefs', localPrefs || null); }, - _getDraftKey: function(location) { - var range = location.range ? location.range.start_line + '-' + - location.range.start_character + '-' + location.range.end_character + - '-' + location.range.end_line : null; - var key = ['draft', location.changeNum, location.patchNum, location.path, - location.line || ''].join(':'); + _getDraftKey(location) { + const range = location.range ? + `${location.range.start_line}-${location.range.start_character}` + + `-${location.range.end_character}-${location.range.end_line}` : + null; + let key = ['draft', location.changeNum, location.patchNum, location.path, + location.line || ''].join(':'); if (range) { key = key + ':' + range; } return key; }, - _cleanupDrafts: function() { + _cleanupDrafts() { // Throttle cleanup to the throttle interval. if (this._lastCleanup && Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL) { @@ -80,9 +81,9 @@ } this._lastCleanup = Date.now(); - var draft; - for (var key in this._storage) { - if (key.indexOf('draft:') === 0) { + let draft; + for (const key in this._storage) { + if (key.startsWith('draft:')) { draft = this._getObject(key); if (Date.now() - draft.updated > DRAFT_MAX_AGE) { this._storage.removeItem(key); @@ -91,13 +92,13 @@ } }, - _getObject: function(key) { - var serial = this._storage.getItem(key); + _getObject(key) { + const serial = this._storage.getItem(key); if (!serial) { return null; } return JSON.parse(serial); }, - _setObject: function(key, obj) { + _setObject(key, obj) { if (this._exceededQuota) { return; } try { this._storage.setItem(key, JSON.stringify(obj));
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html index 6d77c55..ce8ec20 100644 --- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html +++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -19,7 +19,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-storage.html"> <script>void(0);</script> @@ -31,43 +31,44 @@ </test-fixture> <script> - suite('gr-storage tests', function() { - var element; + suite('gr-storage tests', () => { + let element; function mockStorage(opt_quotaExceeded) { return { - getItem: function(key) { return this[key]; }, - removeItem: function(key) { delete this[key]; }, - setItem: function(key, value) { + getItem(key) { return this[key]; }, + removeItem(key) { delete this[key]; }, + setItem(key, value) { + // eslint-disable-next-line no-throw-literal if (opt_quotaExceeded) { throw {code: 22}; /* Quota exceeded */ } this[key] = value; }, }; } - setup(function() { + setup(() => { element = fixture('basic'); element._storage = mockStorage(); }); - test('storing, retrieving and erasing drafts', function() { - var changeNum = 1234; - var patchNum = 5; - var path = 'my_source_file.js'; - var line = 123; - var location = { - changeNum: changeNum, - patchNum: patchNum, - path: path, - line: line, + test('storing, retrieving and erasing drafts', () => { + const changeNum = 1234; + const patchNum = 5; + const path = 'my_source_file.js'; + const line = 123; + const location = { + changeNum, + patchNum, + path, + line, }; // The key is in the expected format. - var key = element._getDraftKey(location); + const key = element._getDraftKey(location); assert.equal(key, ['draft', changeNum, patchNum, path, line].join(':')); // There should be no draft initially. - var draft = element.getDraftComment(location); + const draft = element.getDraftComment(location); assert.isNotOk(draft); // Setting the draft stores it under the expected key. @@ -82,24 +83,24 @@ assert.isNotOk(element._storage.getItem(key)); }); - test('automatically removes old drafts', function() { - var changeNum = 1234; - var patchNum = 5; - var path = 'my_source_file.js'; - var line = 123; - var location = { - changeNum: changeNum, - patchNum: patchNum, - path: path, - line: line, + test('automatically removes old drafts', () => { + const changeNum = 1234; + const patchNum = 5; + const path = 'my_source_file.js'; + const line = 123; + const location = { + changeNum, + patchNum, + path, + line, }; - var key = element._getDraftKey(location); + const key = element._getDraftKey(location); // Make sure that the call to cleanup doesn't get throttled. element._lastCleanup = 0; - var cleanupSpy = sinon.spy(element, '_cleanupDrafts'); + const cleanupSpy = sinon.spy(element, '_cleanupDrafts'); // Create a message with a timestamp that is a second behind the max age. element._storage.setItem(key, JSON.stringify({ @@ -108,7 +109,7 @@ })); // Getting the draft should cause it to be removed. - var draft = element.getDraftComment(location); + const draft = element.getDraftComment(location); assert.isTrue(cleanupSpy.called); assert.isNotOk(draft); @@ -117,18 +118,18 @@ cleanupSpy.restore(); }); - test('_getDraftKey', function() { - var changeNum = 1234; - var patchNum = 5; - var path = 'my_source_file.js'; - var line = 123; - var location = { - changeNum: changeNum, - patchNum: patchNum, - path: path, - line: line, + test('_getDraftKey', () => { + const changeNum = 1234; + const patchNum = 5; + const path = 'my_source_file.js'; + const line = 123; + const location = { + changeNum, + patchNum, + path, + line, }; - var expectedResult = 'draft:1234:5:my_source_file.js:123'; + let expectedResult = 'draft:1234:5:my_source_file.js:123'; assert.equal(element._getDraftKey(location), expectedResult); location.range = { start_character: 1, @@ -140,21 +141,21 @@ assert.equal(element._getDraftKey(location), expectedResult); }); - test('exceeded quota disables storage', function() { + test('exceeded quota disables storage', () => { element._storage = mockStorage(true); assert.isFalse(element._exceededQuota); - var changeNum = 1234; - var patchNum = 5; - var path = 'my_source_file.js'; - var line = 123; - var location = { - changeNum: changeNum, - patchNum: patchNum, - path: path, - line: line, + const changeNum = 1234; + const patchNum = 5; + const path = 'my_source_file.js'; + const line = 123; + const location = { + changeNum, + patchNum, + path, + line, }; - var key = element._getDraftKey(location); + const key = element._getDraftKey(location); element.setDraftComment(location, 'my comment'); assert.isTrue(element._exceededQuota); assert.isNotOk(element._storage.getItem(key));
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html new file mode 100644 index 0000000..37bcba8 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.html
@@ -0,0 +1,78 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> + +<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> +<link rel="import" href="../../shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html"> +<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> +<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> +<link rel="import" href="../../../bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> +<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> +<link rel="import" href="../../../styles/shared-styles.html"> + +<dom-module id="gr-textarea"> + <template> + <style include="shared-styles"> + :host { + display: block; + } + :host(.monospace) { + font-family: var(--monospace-font-family); + } + gr-autocomplete { + display: inline-block + } + #textarea { + background-color: var(--background-color, none); + width: 100%; + } + /*This is needed to not add a scroll bar on the side of gr-textarea + since there is 2px of padding in iron-autogrow-textarea for the + native textarea*/ + iron-autogrow-textarea { + padding: 2px; + } + #textarea.noBorder { + border: none; + } + #hiddenText { + display: block; + float: left; + position: absolute; + visibility: hidden; + white-space: pre-wrap + } + </style> + <gr-autocomplete-dropdown id="emojiSuggestions" + suggestions="[[_suggestions]]" + index="[[_index]]" + move-to-root + fixed-position="[[fixedPositionDropdown]]" + hidden> + </gr-autocomplete-dropdown> + <div id="hiddenText"></div> + <iron-autogrow-textarea + id="textarea" + autocomplete="[[autocomplete]]" + placeholder=[[placeholder]] + disabled="[[disabled]]" + rows="[[rows]]" + max-rows="[[maxRows]]" + bind-value="{{text}}" + on-bind-value-changed="_onValueChanged"></iron-autogrow-textarea> + </template> + <script src="gr-textarea.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js new file mode 100644 index 0000000..07454e5 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -0,0 +1,328 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +(function() { + 'use strict'; + + const MAX_ITEMS_DROPDOWN = 10; + const VERTICAL_OFFSET = 7; + + const ALL_SUGGESTIONS = [ + {value: '💯', match: '100'}, + {value: '💔', match: 'broken heart'}, + {value: '🍺', match: 'beer'}, + {value: '✔', match: 'check'}, + {value: '😎', match: 'cool'}, + {value: '😕', match: 'confused'}, + {value: '😭', match: 'crying'}, + {value: '🔥', match: 'fire'}, + {value: '👊', match: 'fistbump'}, + {value: '🐨', match: 'koala'}, + {value: '😄', match: 'laugh'}, + {value: '🤓', match: 'glasses'}, + {value: '😆', match: 'grin'}, + {value: '😐', match: 'neutral'}, + {value: '👌', match: 'ok'}, + {value: '🎉', match: 'party'}, + {value: '💩', match: 'poop'}, + {value: '🙏', match: 'pray'}, + {value: '😞', match: 'sad'}, + {value: '😮', match: 'shock'}, + {value: '😊', match: 'smile'}, + {value: '😢', match: 'tear'}, + {value: '😂', match: 'tears'}, + {value: '😋', match: 'tongue'}, + {value: '👍', match: 'thumbs up'}, + {value: '👎', match: 'thumbs down'}, + {value: '😒', match: 'unamused'}, + {value: '😉', match: 'wink'}, + {value: '🍷', match: 'wine'}, + {value: '😜', match: 'winking tongue'}, + ]; + + Polymer({ + is: 'gr-textarea', + + properties: { + autocomplete: Boolean, + disabled: Boolean, + rows: Number, + maxRows: Number, + placeholder: String, + fixedPositionDropdown: Boolean, + moveToRoot: Boolean, + text: { + type: String, + notify: true, + observer: '_handleTextChanged', + }, + backgroundColor: { + type: String, + value: '#fff', + }, + hideBorder: { + type: Boolean, + value: false, + }, + monospace: { + type: Boolean, + value: false, + }, + _colonIndex: Number, + _currentSearchString: { + type: String, + value: '', + observer: '_determineSuggestions', + }, + _hideAutocomplete: { + type: Boolean, + value: true, + }, + _index: Number, + _suggestions: Array, + }, + + behaviors: [ + Gerrit.KeyboardShortcutBehavior, + ], + + keyBindings: { + esc: '_handleEscKey', + tab: '_handleEnterByKey', + enter: '_handleEnterByKey', + up: '_handleUpKey', + down: '_handleDownKey', + }, + + ready() { + this._resetEmojiDropdown(); + if (this.monospace) { + this.classList.add('monospace'); + } + if (this.hideBorder) { + this.$.textarea.classList.add('noBorder'); + } + if (this.backgroundColor) { + this.customStyle['--background-color'] = this.backgroundColor; + this.updateStyles(); + } + this.listen(this.$.emojiSuggestions, 'dropdown-closed', '_resetAndFocus'); + this.listen(this.$.emojiSuggestions, 'item-selected', + '_handleEmojiSelect'); + }, + + detached() { + this.closeDropdown(); + this.listen(this.$.emojiSuggestions, 'dropdown-closed', '_resetAndFocus'); + this.listen(this.$.emojiSuggestions, 'item-selected', + '_handleEmojiSelect'); + }, + + closeDropdown() { + if (!this.$.emojiSuggestions.hidden) { + this._closeEmojiDropdown(); + } + }, + + getNativeTextarea() { + return this.$.textarea.textarea; + }, + + putCursorAtEnd() { + const textarea = this.getNativeTextarea(); + // Put the cursor at the end always. + textarea.selectionStart = textarea.value.length; + textarea.selectionEnd = textarea.selectionStart; + this.async(() => { + textarea.focus(); + }); + }, + + _handleEscKey(e) { + if (this._hideAutocomplete) { return; } + e.preventDefault(); + e.stopPropagation(); + this._resetAndFocus(); + }, + + _resetAndFocus() { + this._resetEmojiDropdown(); + this.$.textarea.textarea.focus(); + }, + + _handleUpKey(e) { + if (this._hideAutocomplete) { return; } + e.preventDefault(); + e.stopPropagation(); + this.$.emojiSuggestions.cursorUp(); + this.$.textarea.textarea.focus(); + }, + + _handleDownKey(e) { + if (this._hideAutocomplete) { return; } + e.preventDefault(); + e.stopPropagation(); + this.$.emojiSuggestions.cursorDown(); + this.$.textarea.textarea.focus(); + }, + + _handleEnterByKey(e) { + if (this._hideAutocomplete) { return; } + e.preventDefault(); + e.stopPropagation(); + this.text = this._getText(this.$.emojiSuggestions.getCurrentText()); + this._resetEmojiDropdown(); + }, + + _handleEmojiSelect(e) { + this.text = this._getText(e.detail.selected.dataset.value); + this._resetEmojiDropdown(); + }, + + _getText(value) { + return this.text.substr(0, this._colonIndex) + + value + this.text.substr(this.$.textarea.selectionStart) + ' '; + }, + + _getPositionOfCursor() { + this.$.hiddenText.textContent = this.$.textarea.value.substr(0, + this.$.textarea.selectionStart); + + const caratSpan = document.createElement('span'); + this.$.hiddenText.appendChild(caratSpan); + return caratSpan.getBoundingClientRect(); + }, + + _getFontSize() { + const fontSizePx = getComputedStyle(this).fontSize || '12px'; + return parseInt(fontSizePx.substr(0, fontSizePx.length - 2), + 10); + }, + + _getScrollTop() { + return document.body.scrollTop; + }, + + /** + * This positions the dropdown to be just below the cursor position. It is + * calculated by having a hidden element with the same width and styling of + * the tetarea and the text up until the point of interest. Then a span + * element is added to the end so that there is a specific element to get + * the position of. Line height is determined (or falls back to 12px) as + * extra height to add. + */ + _updateSelectorPosition() { + // These are broken out into separate functions for testability. + const caratPosition = this._getPositionOfCursor(); + const fontSize = this._getFontSize(); + + let top = caratPosition.top + fontSize + VERTICAL_OFFSET; + + if (!this.fixedPositionDropdown) { + top += this._getScrollTop(); + } + top += 'px'; + const left = caratPosition.left + 'px'; + this.$.emojiSuggestions.setPosition(top, left); + }, + + /** + * _handleKeydown used for key handling in the this.$.textarea AND all child + * autocomplete options. + */ + _onValueChanged(e) { + // If cursor is not in textarea (just opened with colon as last char), + // Don't do anything. + if (!e.currentTarget.focused) { return; } + const newChar = e.detail.value[this.$.textarea.selectionStart - 1]; + + // When a colon is detected, set a colon index, but don't do anything else + // yet. + if (newChar === ':') { + this._colonIndex = this.$.textarea.selectionStart - 1; + // If the colon index exists, continue to determine what needs to be done + // with the dropdown. It may be open or closed at this point. + } else if (this._colonIndex !== null) { + // The search string is a substring of the textarea's value from (1 + // position after) the colon index to the cursor position. + this._currentSearchString = e.detail.value.substr(this._colonIndex + 1, + this.$.textarea.selectionStart); + // Under the following conditions, close and reset the dropdown: + // - The cursor is no longer at the end of the current search string + // - The search string is an space or new line + // - The colon has been removed + // - There are no suggestions that match the search string + if (this.$.textarea.selectionStart !== + this._currentSearchString.length + this._colonIndex + 1 || + this._currentSearchString === ' ' || + this._currentSearchString === '\n' || + !e.detail.value[this._colonIndex] === ':' || + !this._suggestions.length) { + this._resetEmojiDropdown(); + // Otherwise open the dropdown and set the position to be just below the + // cursor. + } else if (this.$.emojiSuggestions.hidden) { + this._hideAutocomplete = false; + this._openEmojiDropdown(); + this._updateSelectorPosition(); + } + this.$.textarea.textarea.focus(); + } + }, + + _closeEmojiDropdown() { + this.$.emojiSuggestions.close(); + this.$.emojiSuggestions.hidden = true; + }, + + _openEmojiDropdown() { + this.$.emojiSuggestions.open(); + this.$.emojiSuggestions.hidden = false; + }, + + _formatSuggestions(matchedSuggestions) { + const suggestions = []; + for (const suggestion of matchedSuggestions) { + suggestion.dataValue = suggestion.value; + suggestion.text = suggestion.value + ' ' + suggestion.match; + suggestions.push(suggestion); + } + this._suggestions = suggestions; + }, + + _determineSuggestions(emojiText) { + if (!emojiText.length) { + this._formatSuggestions(ALL_SUGGESTIONS); + } + const matches = ALL_SUGGESTIONS.filter(suggestion => { + return suggestion.match.includes(emojiText); + }).splice(0, MAX_ITEMS_DROPDOWN); + this._formatSuggestions(matches); + }, + + _resetEmojiDropdown() { + // hide and reset the autocomplete dropdown. + Polymer.dom.flush(); + this._currentSearchString = ''; + this._hideAutocomplete = true; + this.closeDropdown(); + this._colonIndex = null; + this.$.textarea.textarea.focus(); + }, + + _handleTextChanged(text) { + this.dispatchEvent( + new CustomEvent('value-changed', {detail: {value: text}})); + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html new file mode 100644 index 0000000..637fbbfc --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -0,0 +1,253 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-textarea</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../../../test/common-test-setup.html"/> +<link rel="import" href="gr-textarea.html"> + +<script>void(0);</script> + +<test-fixture id="basic"> + <template> + <gr-textarea></gr-textarea> + </template> +</test-fixture> + +<script> + suite('gr-textarea tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('monospace is set properly', () => { + assert.isFalse(element.classList.contains('monospace')); + element.monospace = true; + element.ready(); + assert.isTrue(element.classList.contains('monospace')); + }); + + test('hideBorder is set properly', () => { + assert.isFalse(element.$.textarea.classList.contains('noBorder')); + element.hideBorder = true; + element.ready(); + assert.isTrue(element.$.textarea.classList.contains('noBorder')); + }); + + test('background color is set properly', () => { + assert.equal(getComputedStyle(element.$.textarea).backgroundColor, + 'rgb(255, 255, 255)'); + element.backgroundColor = 'pink'; + element.ready(); + assert.equal(getComputedStyle(element.$.textarea).backgroundColor, + 'rgb(255, 192, 203)'); + }); + + test('emoji selector is not open with the textarea lacks focus', () => { + element.$.textarea.selectionStart = 1; + element.$.textarea.selectionEnd = 1; + element.text = ':'; + assert.isFalse(!element.$.emojiSuggestions.hidden); + }); + + test('emoji selector is not open when a general text is entered', () => { + MockInteractions.focus(element.$.textarea); + element.$.textarea.selectionStart = 9; + element.$.textarea.selectionEnd = 9; + element.text = 'some text'; + assert.isFalse(!element.$.emojiSuggestions.hidden); + }); + + test('emoji selector opens when a colon is typed & the textarea has focus', + () => { + MockInteractions.focus(element.$.textarea); + // Needed for Safari tests. selectionStart is not updated when text is + // updated. + element.$.textarea.selectionStart = 1; + element.$.textarea.selectionEnd = 1; + element.text = ':'; + element.$.textarea.selectionStart = 2; + element.$.textarea.selectionEnd = 2; + element.text = ':t'; + flushAsynchronousOperations(); + assert.isFalse(element.$.emojiSuggestions.hidden); + assert.equal(element._colonIndex, 0); + assert.isFalse(element._hideAutocomplete); + assert.equal(element._currentSearchString, 't'); + }); + + test('emoji selector closes when text changes before the colon', () => { + const resetStub = sandbox.stub(element, '_resetEmojiDropdown'); + MockInteractions.focus(element.$.textarea); + flushAsynchronousOperations(); + element.$.textarea.selectionStart = 10; + element.$.textarea.selectionEnd = 10; + element.text = 'test test '; + element.$.textarea.selectionStart = 12; + element.$.textarea.selectionEnd = 12; + element.text = 'test test :'; + element.$.textarea.selectionStart = 15; + element.$.textarea.selectionEnd = 15; + element.text = 'test test :smi'; + + assert.equal(element._currentSearchString, 'smi'); + assert.isFalse(resetStub.called); + element.text = 'test test test :smi'; + assert.isTrue(resetStub.called); + }); + + test('_resetEmojiDropdown', () => { + const closeSpy = sandbox.spy(element, 'closeDropdown'); + element._resetEmojiDropdown(); + assert.equal(element._currentSearchString, ''); + assert.isTrue(element._hideAutocomplete); + assert.equal(element._colonIndex, null); + + element.$.emojiSuggestions.open(); + flushAsynchronousOperations(); + element._resetEmojiDropdown(); + assert.isTrue(closeSpy.called); + }); + + test('_determineSuggestions', () => { + const emojiText = 'tear'; + const formatSpy = sandbox.spy(element, '_formatSuggestions'); + element._determineSuggestions(emojiText); + assert.isTrue(formatSpy.called); + assert.isTrue(formatSpy.lastCall.calledWithExactly( + [{dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'}, + {dataValue: '😂', value: '😂', match: 'tears', text: '😂 tears'}])); + }); + + test('_formatSuggestions', () => { + const matchedSuggestions = [{value: '😢', match: 'tear'}, + {value: '😂', match: 'tears'}]; + element._formatSuggestions(matchedSuggestions); + assert.deepEqual( + [{value: '😢', dataValue: '😢', match: 'tear', text: '😢 tear'}, + {value: '😂', dataValue: '😂', match: 'tears', text: '😂 tears'}], + element._suggestions); + }); + + test('_handleEmojiSelect', () => { + element.$.textarea.selectionStart = 16; + element.$.textarea.selectionEnd = 16; + element.text = 'test test :tears'; + element._colonIndex = 10; + const selectedItem = {dataset: {value: '😂'}}; + const event = {detail: {selected: selectedItem}}; + element._handleEmojiSelect(event); + assert.equal(element.text, 'test test 😂 '); + }); + + test('_getPositionOfCursor', () => { + element.$.textarea.selectionStart = 4; + element.$.textarea.selectionEnd = 4; + element.text = 'test'; + element._getPositionOfCursor(); + assert.deepEqual(element.$.hiddenText.innerHTML, element.text + + '<span></span>'); + }); + + test('_updateSelectorPosition', () => { + const setPositionSpy = + sandbox.spy(element.$.emojiSuggestions, 'setPosition'); + sandbox.stub(element, '_getPositionOfCursor', () => { + return {top: 100, left: 30}; + }); + sandbox.stub(element, '_getFontSize', () => 12); + sandbox.stub(element, '_getScrollTop', () => 100); + element._updateSelectorPosition(); + assert.isTrue(setPositionSpy.lastCall.calledWithExactly('219px', '30px')); + + element.fixedPositionDropdown = true; + element._updateSelectorPosition(); + assert.isTrue(setPositionSpy.lastCall.calledWithExactly('119px', '30px')); + }); + + test('emoji dropdown is closed when dropdown-closed is fired', () => { + const resetSpy = sandbox.spy(element, '_resetAndFocus'); + element.$.emojiSuggestions.fire('dropdown-closed'); + assert.isTrue(resetSpy.called); + }); + + suite('keyboard shortcuts', () => { + function setupDropdown() { + MockInteractions.focus(element.$.textarea); + flushAsynchronousOperations(); + element.$.textarea.selectionStart = 1; + element.$.textarea.selectionEnd = 1; + element.text = ':'; + element.$.textarea.selectionStart = 1; + element.$.textarea.selectionEnd = 1; + element.text = ':1'; + } + + test('escape key', () => { + const resestSpy = sandbox.spy(element, '_resetAndFocus'); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27); + assert.isFalse(resestSpy.called); + setupDropdown(); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27); + assert.isTrue(resestSpy.called); + assert.isFalse(!element.$.emojiSuggestions.hidden); + }); + + test('up key', () => { + const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp'); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38); + assert.isFalse(upSpy.called); + setupDropdown(); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38); + assert.isTrue(upSpy.called); + }); + + test('down key', () => { + const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown'); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40); + assert.isFalse(downSpy.called); + setupDropdown(); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40); + assert.isTrue(downSpy.called); + }); + + test('enter key', () => { + const enterSpy = sandbox.spy(element.$.emojiSuggestions, + 'getCursorTarget'); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); + assert.isFalse(enterSpy.called); + setupDropdown(); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); + assert.isTrue(enterSpy.called); + flushAsynchronousOperations(); + // A space is automatically added at the end. + assert.equal(element.text, '💯 '); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html index 81e65e3..58f8e39 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html +++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.html
@@ -19,8 +19,8 @@ <dom-module id="gr-tooltip-content"> <template> - <content></content> - <span class="arrow" hidden$="[[!showIcon]]">ⓘ</span> + <content></content><!-- + --><span class="arrow" hidden$="[[!showIcon]]">ⓘ</span> </template> <script src="gr-tooltip-content.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html index aac2ea8..2fa02a3 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html +++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
@@ -19,7 +19,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-tooltip-content.html"> <script>void(0);</script> @@ -32,18 +32,18 @@ </test-fixture> <script> - suite('gr-tooltip-content tests', function() { - var element; - setup(function() { + suite('gr-tooltip-content tests', () => { + let element; + setup(() => { element = fixture('basic'); }); - test('icon is not visible by default', function() { + test('icon is not visible by default', () => { assert.equal(Polymer.dom(element.root) .querySelector('.arrow').hidden, true); }); - test('icon is visible with showIcon property', function() { + test('icon is visible with showIcon property', () => { element.showIcon = true; assert.equal(Polymer.dom(element.root) .querySelector('.arrow').hidden, false);
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html index 2af9c86..e79fb19 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html +++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
@@ -15,10 +15,11 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../../styles/shared-styles.html"> <dom-module id="gr-tooltip"> <template> - <style> + <style include="shared-styles"> :host { --gr-tooltip-arrow-size: .5em; --gr-tooltip-arrow-center-offset: 0; @@ -27,11 +28,13 @@ box-shadow: 0 1px 3px rgba(0, 0, 0, .3); color: #fff; font-size: .75rem; - padding: .5em .85em; position: absolute; z-index: 1000; max-width: var(--tooltip-max-width); } + :host .tooltip { + padding: .5em .85em; + } .arrow { border-left: var(--gr-tooltip-arrow-size) solid transparent; border-right: var(--gr-tooltip-arrow-size) solid transparent; @@ -44,8 +47,10 @@ width: 0; } </style> - [[text]] - <i class="arrow"></i> + <div class="tooltip"> + [[text]] + <i class="arrow"></i> + </div> </template> <script src="gr-tooltip.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js index 4a5f631..30038e4 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js +++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -25,7 +25,7 @@ }, }, - _updateWidth: function(maxWidth) { + _updateWidth(maxWidth) { this.customStyle['--tooltip-max-width'] = maxWidth; this.updateStyles(); },
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html index 69a5b75..e1e6449 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html +++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
@@ -19,7 +19,7 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> - +<link rel="import" href="../../../test/common-test-setup.html"/> <link rel="import" href="gr-tooltip.html"> <script>void(0);</script> @@ -32,13 +32,13 @@ </test-fixture> <script> - suite('gr-tooltip tests', function() { - var element; - setup(function() { + suite('gr-tooltip tests', () => { + let element; + setup(() => { element = fixture('basic'); }); - test('max-width is respected if set', function() { + test('max-width is respected if set', () => { element.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit' + ', sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'; element.maxWidth = '50px';
diff --git a/polygerrit-ui/app/elements/test/plugin.html b/polygerrit-ui/app/elements/test/plugin.html new file mode 100644 index 0000000..bd29b90 --- /dev/null +++ b/polygerrit-ui/app/elements/test/plugin.html
@@ -0,0 +1,18 @@ +<dom-module id="my-plugin"> + <script> + Gerrit.install(plugin => + plugin.registerStyleModule('app-theme', 'myplugin-app-theme') + ); + </script> +</dom-module> + +<dom-module id="myplugin-app-theme"> + <style> + html { + --primary-text-color: #F00BAA; + --header-background-color: #F01BAA; + --header-title-content: "MyGerrit"; + --footer-background-color: #F02BAA; + } + </style> +</dom-module>
diff --git a/polygerrit-ui/app/embed/change-diff-views.html b/polygerrit-ui/app/embed/change-diff-views.html new file mode 100644 index 0000000..8426585 --- /dev/null +++ b/polygerrit-ui/app/embed/change-diff-views.html
@@ -0,0 +1,19 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<link rel="import" href="../bower_components/polymer/polymer.html"> +<link rel="import" href="../elements/change/gr-change-view/gr-change-view.html"> +<link rel="import" href="../elements/diff/gr-diff-view/gr-diff-view.html"> +<link rel="import" href="../styles/app-theme.html">
diff --git a/polygerrit-ui/app/embed/embed_test.html b/polygerrit-ui/app/embed/embed_test.html new file mode 100644 index 0000000..26ea895 --- /dev/null +++ b/polygerrit-ui/app/embed/embed_test.html
@@ -0,0 +1,51 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>change-diff-views-embed_test</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> +<link rel="import" href="../polygerrit_ui/elements/change-diff-views.html"/> + +<script>void(0);</script> + +<test-fixture id="change-view"> + <template> + <gr-change-view></gr-change-view> + </template> +</test-fixture> + +<test-fixture id="diff-view"> + <template> + <gr-diff-view></gr-diff-view> + </template> +</test-fixture> + +<script> + suite('embed test', () => { + test('gr-change-view is embedded', () => { + const element = fixture('change-view'); + assert.equal(element.is, 'gr-change-view'); + }); + + test('diff-view is embedded', () => { + const element = fixture('diff-view'); + assert.equal(element.is, 'gr-diff-view'); + }); + }); +</script>
diff --git a/polygerrit-ui/app/embed/test.html b/polygerrit-ui/app/embed/test.html new file mode 100644 index 0000000..0587562 --- /dev/null +++ b/polygerrit-ui/app/embed/test.html
@@ -0,0 +1,25 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>Embed Test Runner</title> +<meta charset="utf-8"> +<script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../bower_components/web-component-tester/browser.js"></script> +<script> + WCT.loadSuites(['embed_test.html']); +</script>
diff --git a/polygerrit-ui/app/embed_test.sh b/polygerrit-ui/app/embed_test.sh new file mode 100755 index 0000000..adcc653 --- /dev/null +++ b/polygerrit-ui/app/embed_test.sh
@@ -0,0 +1,57 @@ +#!/bin/sh + +set -ex + +t=$(mktemp -d || mktemp -d -t wct-XXXXXXXXXX) +components=$TEST_SRCDIR/gerrit/polygerrit-ui/app/test_components.zip +code=$TEST_SRCDIR/gerrit/polygerrit-ui/app/polygerrit_embed_ui.zip +index=$TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/test.html +tests=$TEST_SRCDIR/gerrit/polygerrit-ui/app/embed/*_test.html + +unzip -qd $t $components +unzip -qd $t $code +mkdir -p $t/test +cp $index $t/test/ +cp $tests $t/test/ + +# For some reason wct tries to install selenium into its node_modules +# directory on first run. If you've installed into /usr/local and +# aren't running wct as root, you're screwed. Turning this option off +# through skipSeleniumInstall seems to still work, so there's that. + +# Sauce tests are disabled by default in order to run local tests +# only. Run it with (saucelabs.com account required; free for open +# source): WCT_ARGS='--plugin sauce' ./polygerrit-ui/app/embed_test.sh + +cat <<EOF > $t/wct.conf.js +module.exports = { + 'suites': ['test'], + 'webserver': { + 'pathMappings': [ + {'/components/bower_components': 'bower_components'} + ] + }, + 'plugins': { + 'local': { + 'skipSeleniumInstall': true + }, + 'sauce': { + 'disabled': true, + 'browsers': [ + 'OS X 10.12/chrome', + 'Windows 10/chrome', + 'Linux/firefox', + 'OS X 10.12/safari', + 'Windows 10/microsoftedge' + ] + } + } + }; +EOF + +export PATH="$(dirname $WCT):$(dirname $NPM):$PATH" + +cd $t +test -n "${WCT}" + +$(basename ${WCT}) ${WCT_ARGS}
diff --git a/polygerrit-ui/app/index.html b/polygerrit-ui/app/index.html index db9a1c5..46fc46b 100644 --- a/polygerrit-ui/app/index.html +++ b/polygerrit-ui/app/index.html
@@ -21,14 +21,22 @@ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0"> <!-- -SourceCodePro fonts are used in styles/fonts.css +RobotoMono fonts are used in styles/fonts.css @see https://github.com/w3c/preload/issues/32 regarding crossorigin --> -<link rel="preload" href="/fonts/SourceCodePro-Regular.woff2" as="font" type="font/woff2" crossorigin> -<link rel="preload" href="/fonts/SourceCodePro-Regular.woff" as="font" type="font/woff" crossorigin> +<link rel="preload" href="/fonts/RobotoMono-Regular.woff2" as="font" type="font/woff2" crossorigin> +<link rel="preload" href="/fonts/RobotoMono-Regular.woff" as="font" type="font/woff" crossorigin> <link rel="stylesheet" href="/styles/fonts.css"> <link rel="stylesheet" href="/styles/main.css"> <script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> +<!-- + - Content between webcomponents-lite and the load of the main app element + - run before polymer-resin is installed so may have security consequences. + - Contact your local security engineer if you have any questions, and + - CC them on any changes that load content before gr-app.html. + - + - github.com/Polymer/polymer-resin/blob/master/getting-started.md#integrating + --> <link rel="preload" href="/elements/gr-app.js" as="script" crossorigin="anonymous"> <link rel="import" href="/elements/gr-app.html">
diff --git a/polygerrit-ui/app/lint_test.sh b/polygerrit-ui/app/lint_test.sh new file mode 100755 index 0000000..35939ba --- /dev/null +++ b/polygerrit-ui/app/lint_test.sh
@@ -0,0 +1,22 @@ +#!/bin/sh + +set -ex + +eslint_bin=$(which npm) +if [ -z "$eslint_bin" ]; then + echo "NPM must be on the path." + exit 1 +fi + +eslint_bin=$(which eslint) +eslint_config=$(npm list -g | grep -c eslint-config-google) +eslint_plugin=$(npm list -g | grep -c eslint-plugin-html) +if [ -z "$eslint_bin" ] || [ "$eslint_config" -eq "0" ] || [ "$eslint_plugin" -eq "0" ]; then + echo "You must install ESLint and its dependencies from NPM." + echo "> npm install -g eslint eslint-config-google eslint-plugin-html" + echo "For more information, view the README:" + echo "https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/#Style-guide" + exit 1 +fi + +${eslint_bin} --ignore-pattern 'bower_components/' --ignore-pattern 'gr-linked-text' --ignore-pattern 'scripts/vendor' --ext .html,.js .
diff --git a/polygerrit-ui/app/polylint_test.sh b/polygerrit-ui/app/polylint_test.sh new file mode 100755 index 0000000..4bd1600 --- /dev/null +++ b/polygerrit-ui/app/polylint_test.sh
@@ -0,0 +1,20 @@ +#!/bin/sh + +set -ex + +npm_bin=$(which npm) +if [[ -z "$npm_bin" ]]; then + echo "NPM must be on the path." + exit 1 +fi + +polylint_bin=$(which polylint) +if [[ -z "$polylint_bin" ]]; then + echo "You must install polylint and its dependencies from NPM." + echo "> npm install -g polylint" + exit 1 +fi + +unzip polygerrit-ui/polygerrit_components.bower_components.zip -d polygerrit-ui/app + +${polylint_bin} --root polygerrit-ui/app --input elements/gr-app.html
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl new file mode 100644 index 0000000..be80c13 --- /dev/null +++ b/polygerrit-ui/app/rules.bzl
@@ -0,0 +1,94 @@ +load("//tools/bzl:genrule2.bzl", "genrule2") +load("@io_bazel_rules_closure//closure:defs.bzl", "closure_js_library", "closure_js_binary") +load( + "//tools/bzl:js.bzl", + "bower_component_bundle", + "vulcanize", + "bower_component", + "js_component", +) + +def polygerrit_bundle(name, srcs, outs, app): + appName = app.split(".html")[0].split("/").pop() # eg: gr-app + + closure_js_binary( + name = name + "_closure_bin", + # Known issue: Closure compilation not compatible with Polymer behaviors. + # See: https://github.com/google/closure-compiler/issues/2042 + compilation_level = "WHITESPACE_ONLY", + defs = [ + "--polymer_pass", + "--jscomp_off=duplicate", + "--force_inject_library=es6_runtime", + ], + language = "ECMASCRIPT5", + deps = [name + "_closure_lib"], + ) + + closure_js_library( + name = name + "_closure_lib", + srcs = [appName + ".js"], + convention = "GOOGLE", + # TODO(davido): Clean up these issues: http://paste.openstack.org/show/608548 + # and remove this supression + suppress = ["JSC_UNUSED_LOCAL_ASSIGNMENT"], + deps = [ + "//lib/polymer_externs:polymer_closure", + "@io_bazel_rules_closure//closure/library", + ], + ) + + vulcanize( + name = appName, + srcs = srcs, + app = app, + deps = ["//polygerrit-ui:polygerrit_components.bower_components"], + ) + + native.filegroup( + name = name + "_app_sources", + srcs = [ + name + "_closure_bin.js", + appName + ".html", + ], + ) + + native.filegroup( + name = name + "_css_sources", + srcs = native.glob(["styles/**/*.css"]), + ) + + native.filegroup( + name = name + "_top_sources", + srcs = [ + "favicon.ico", + "index.html", + ], + ) + + genrule2( + name = name, + srcs = [ + name + "_app_sources", + name + "_css_sources", + name + "_top_sources", + "//lib/fonts:robotomono", + "//lib/js:highlightjs_files", + # we extract from the zip, but depend on the component for license checking. + "@webcomponentsjs//:zipfile", + "//lib/js:webcomponentsjs" + ], + outs = outs, + cmd = " && ".join([ + "mkdir -p $$TMP/polygerrit_ui/{styles,fonts,bower_components/{highlightjs,webcomponentsjs},elements}", + "for f in $(locations " + name + "_app_sources); do ext=$${f##*.}; cp -p $$f $$TMP/polygerrit_ui/elements/" + appName + ".$$ext; done", + "cp $(locations //lib/fonts:robotomono) $$TMP/polygerrit_ui/fonts/", + "for f in $(locations " + name + "_top_sources); do cp $$f $$TMP/polygerrit_ui/; done", + "for f in $(locations "+ name + "_css_sources); do cp $$f $$TMP/polygerrit_ui/styles; done", + "for f in $(locations //lib/js:highlightjs_files); do cp $$f $$TMP/polygerrit_ui/bower_components/highlightjs/ ; done", + "unzip -qd $$TMP/polygerrit_ui/bower_components $(location @webcomponentsjs//:zipfile) webcomponentsjs/webcomponents-lite.js", + "cd $$TMP", + "find . -exec touch -t 198001010000 '{}' ';'", + "zip -qr $$ROOT/$@ *", + ]), + )
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh index d17a530..0edf41c 100755 --- a/polygerrit-ui/app/run_test.sh +++ b/polygerrit-ui/app/run_test.sh
@@ -1,14 +1,14 @@ #!/usr/bin/env bash -wct_bin=$(which wct) -if [[ -z "$wct_bin" ]]; then - echo "WCT must be on the path." +npm_bin=$(which npm) +if [[ -z "$npm_bin" ]]; then + echo "NPM must be on the path. (https://www.npmjs.com/)" exit 1 fi -npm_bin=$(which npm) -if [[ -z "$npm_bin" ]]; then - echo "NPM must be on the path." +wct_bin=$(which wct) +if [[ -z "$wct_bin" ]]; then + echo "WCT must be on the path. (https://github.com/Polymer/web-component-tester)" exit 1 fi @@ -21,4 +21,5 @@ --test_env="NPM=${npm_bin}" \ --test_env="DISPLAY=${DISPLAY}" \ "$@" \ + //polygerrit-ui/app:embed_test \ //polygerrit-ui/app:wct_test
diff --git a/polygerrit-ui/app/samples/lgtm-plugin.html b/polygerrit-ui/app/samples/lgtm-plugin.html new file mode 100644 index 0000000..d58034d --- /dev/null +++ b/polygerrit-ui/app/samples/lgtm-plugin.html
@@ -0,0 +1,16 @@ +<dom-module id="lgtm-plugin"> + <script> + Gerrit.install(plugin => { + const replyApi = plugin.changeReply(); + replyApi.addReplyTextChangedCallback(text => { + const label = 'Code-Review'; + const labelValue = replyApi.getLabelValue(label); + if (labelValue && + labelValue === ' 0' && + text.indexOf('LGTM') === 0) { + replyApi.setLabelValue(label, '+1'); + } + }); + }); + </script> +</dom-module>
diff --git a/polygerrit-ui/app/scripts/hiddenscroll.js b/polygerrit-ui/app/scripts/hiddenscroll.js new file mode 100644 index 0000000..b80742a --- /dev/null +++ b/polygerrit-ui/app/scripts/hiddenscroll.js
@@ -0,0 +1,29 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +(function(window) { + window.Gerrit = window.Gerrit || {}; + if (window.Gerrit.hasOwnProperty('hiddenscroll')) { return; } + + window.Gerrit.hiddenscroll = undefined; + + window.addEventListener('WebComponentsReady', () => { + const elem = document.createElement('div'); + elem.setAttribute( + 'style', 'width:100px;height:100px;overflow:scroll'); + document.body.appendChild(elem); + window.Gerrit.hiddenscroll = elem.offsetWidth === elem.clientWidth; + elem.remove(); + }); +})(window);
diff --git a/polygerrit-ui/app/scripts/rootElement.js b/polygerrit-ui/app/scripts/rootElement.js new file mode 100644 index 0000000..1a07edf --- /dev/null +++ b/polygerrit-ui/app/scripts/rootElement.js
@@ -0,0 +1,20 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +(function(window) { + window.Gerrit = window.Gerrit || {}; + if (window.Gerrit.hasOwnProperty('getRootElement')) { return; } + + window.Gerrit.getRootElement = () => document.body; +})(window); \ No newline at end of file
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js index 6c83905..07b543f 100644 --- a/polygerrit-ui/app/scripts/util.js +++ b/polygerrit-ui/app/scripts/util.js
@@ -14,7 +14,7 @@ (function(window) { 'use strict'; - var util = window.util || {}; + const util = window.util || {}; util.parseDate = function(dateStr) { // Timestamps are given in UTC and have the format @@ -25,14 +25,14 @@ }; util.getCookie = function(name) { - var key = name + '='; - var cookies = document.cookie.split(';'); - for (var i = 0; i < cookies.length; i++) { - var c = cookies[i]; - while (c.charAt(0) == ' ') { + const key = name + '='; + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + let c = cookies[i]; + while (c.charAt(0) === ' ') { c = c.substring(1); } - if (c.indexOf(key) == 0) { + if (c.startsWith(key)) { return c.substring(key.length, c.length); } } @@ -50,7 +50,7 @@ * @return {String} Returns the truncated value of a URL. */ util.truncatePath = function(path) { - var pathPieces = path.split('/'); + const pathPieces = path.split('/'); if (pathPieces.length < 2) { return path;
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html index 773b341..4728fe6 100644 --- a/polygerrit-ui/app/styles/app-theme.html +++ b/polygerrit-ui/app/styles/app-theme.html
@@ -15,15 +15,22 @@ --> <style is="custom-style"> :root { + /* Following vars have LTS for plugin API. */ --primary-text-color: #000; + --header-background-color: #eee; + --header-title-content: 'PolyGerrit'; + --header-icon: none; + --header-icon-size: 0em; + --footer-background-color: var(--header-background-color); + + /* Following are not part of plugin API. */ --search-border-color: #ddd; --selection-background-color: #ebf5fb; --default-text-color: #000; --view-background-color: #fff; --default-horizontal-margin: 1rem; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; - --monospace-font-family: 'Source Code Pro', Menlo, 'Lucida Console', Monaco, monospace; - + --monospace-font-family: 'Roboto Mono', Menlo, 'Lucida Console', Monaco, monospace; --iron-overlay-backdrop: { transition: none; };
diff --git a/polygerrit-ui/app/styles/fonts.css b/polygerrit-ui/app/styles/fonts.css index b5bf9ae..d339e16 100644 --- a/polygerrit-ui/app/styles/fonts.css +++ b/polygerrit-ui/app/styles/fonts.css
@@ -1,20 +1,20 @@ /* latin-ext */ @font-face { - font-family: 'Source Code Pro'; + font-family: 'Roboto Mono'; font-style: normal; font-weight: 400; - src: local('Source Code Pro'), local('SourceCodePro-Regular'), - url(../fonts/SourceCodePro-Regular.woff2) format('woff2'), - url(../fonts/SourceCodePro-Regular.woff) format('woff'); + src: local('Roboto Mono'), local('RobotoMono-Regular'), + url('../fonts/RobotoMono-Regular.woff2') format('woff2'), + url('../fonts/RobotoMono-Regular.woff') format('woff'); unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; } /* latin */ @font-face { - font-family: 'Source Code Pro'; + font-family: 'Roboto Mono'; font-style: normal; font-weight: 400; - src: local('Source Code Pro'), local('SourceCodePro-Regular'), - url(../fonts/SourceCodePro-Regular.woff2) format('woff2'), - url(../fonts/SourceCodePro-Regular.woff) format('woff'); + src: local('Roboto Mono'), local('RobotoMono-Regular'), + url('../fonts/RobotoMono-Regular.woff2') format('woff2'), + url('../fonts/RobotoMono-Regular.woff') format('woff'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; -} +} \ No newline at end of file
diff --git a/polygerrit-ui/app/styles/gr-form-styles.html b/polygerrit-ui/app/styles/gr-form-styles.html new file mode 100644 index 0000000..9fefc0e --- /dev/null +++ b/polygerrit-ui/app/styles/gr-form-styles.html
@@ -0,0 +1,131 @@ +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<dom-module id="gr-form-styles"> + <template> + <style> + .gr-form-styles h1 { + margin-bottom: .1em; + } + .gr-form-styles h2 { + margin-bottom: .3em; + } + .gr-form-styles fieldset { + border: none; + margin: 0 0 2em 2em; + } + .gr-form-styles section { + margin: .15em 0; + min-height: 2em; + } + .gr-form-styles section * { + vertical-align: middle; + } + .gr-form-styles .title, + .gr-form-styles .value { + display: inline-block; + } + .gr-form-styles .title { + color: #666; + font-weight: bold; + padding-right: .5em; + width: 11em; + } + .gr-form-styles iron-autogrow-textarea { + font-size: 1em; + } + .gr-form-styles th { + color: #666; + text-align: left; + vertical-align: bottom; + } + .gr-form-styles td, + .gr-form-styles tfoot th { + height: 2em; + vertical-align: middle; + } + .gr-form-styles .emptyHeader { + text-align: right; + } + .gr-form-styles tbody tr:nth-child(even) { + background-color: #f4f4f4; + } + .gr-form-styles table { + width: 50em; + } + .gr-form-styles th:first-child, + .gr-form-styles td:first-child { + width: 11em; + } + .gr-form-styles th:first-child input, + .gr-form-styles td:first-child input { + width: 10em; + } + .gr-form-styles input:not([type="checkbox"]), + .gr-form-styles select, + .gr-form-styles textarea { + border: 1px solid #d1d2d3; + border-radius: 2px; + font-size: 1em; + height: 2em; + padding: 0 .15em; + } + .gr-form-styles gr-button:not([link]) { + height: 2.2em; + } + .gr-form-styles td:last-child { + width: 5em; + } + .gr-form-styles th:last-child gr-button, + .gr-form-styles td:last-child gr-button { + width: 100%; + } + .gr-form-styles iron-autogrow-textarea { + border: none; + height: auto; + min-height: 2em; + --iron-autogrow-textarea: { + border: 1px solid #d1d2d3; + border-radius: 2px; + font-size: 1em; + padding: .25em .15em 0 .15em; + } + } + .gr-form-styles gr-autocomplete { + border: none; + --gr-autocomplete: { + border: 1px solid #d1d2d3; + border-radius: 2px; + font-size: 1em; + height: 2em; + padding: 0 .15em; + width: 10em; + } + } + @media only screen and (max-width: 40em) { + .gr-form-styles section { + margin-bottom: 1em; + } + .gr-form-styles .title, + .gr-form-styles .value { + display: block; + } + .gr-form-styles table { + width: 100%; + } + } + </style> + </template> +</dom-module>
diff --git a/polygerrit-ui/app/styles/gr-menu-page-styles.html b/polygerrit-ui/app/styles/gr-menu-page-styles.html new file mode 100644 index 0000000..19ff2e5 --- /dev/null +++ b/polygerrit-ui/app/styles/gr-menu-page-styles.html
@@ -0,0 +1,62 @@ +<!-- +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<dom-module id="gr-menu-page-styles"> + <template> + <style> + :host { + background-color: var(--view-background-color); + display: block; + } + main { + margin: 2em auto; + max-width: 46em; + } + main.table { + margin-top: 0; + margin-right: 0; + margin-left: 14em; + max-width: none; + } + h2.edited:after { + color: #444; + content: ' *'; + } + .loading { + color: #666; + padding: 1em var(--default-horizontal-margin); + } + @media only screen and (max-width: 67em) { + main { + margin: 2em 0 2em 15em; + } + main.table { + margin-left: 14em; + } + } + @media only screen and (max-width: 53em) { + .loading { + padding: 0 var(--default-horizontal-margin); + } + main { + margin: 2em 1em; + } + main.table { + margin: 0; + } + } + </style> + </template> +</dom-module>
diff --git a/polygerrit-ui/app/styles/gr-settings-styles.html b/polygerrit-ui/app/styles/gr-settings-styles.html deleted file mode 100644 index fcda1b4..0000000 --- a/polygerrit-ui/app/styles/gr-settings-styles.html +++ /dev/null
@@ -1,58 +0,0 @@ -<!-- -Copyright (C) 2016 The Android Open Source Project - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. ---> -<dom-module id="gr-settings-styles"> - <template> - <style> - .gr-settings-styles fieldset { - border: none; - margin: 0 0 2em 2em; - } - .gr-settings-styles section { - margin-bottom: .5em; - } - .gr-settings-styles .title, - .gr-settings-styles .value { - display: inline-block; - vertical-align: top; - } - .gr-settings-styles .title { - color: #666; - font-weight: bold; - padding-right: .5em; - width: 11em; - } - .gr-settings-styles input { - font-size: 1em; - } - .gr-settings-styles th { - color: #666; - text-align: left; - } - .gr-settings-styles tbody tr:nth-child(even) { - background-color: #f4f4f4; - } - @media only screen and (max-width: 40em) { - .gr-settings-styles section { - margin-bottom: 1em; - } - .gr-settings-styles .title, - .gr-settings-styles .value { - display: block; - } - } - </style> - </template> -</dom-module>
diff --git a/polygerrit-ui/app/styles/main.css b/polygerrit-ui/app/styles/main.css index 6e48ae5..b18543a 100644 --- a/polygerrit-ui/app/styles/main.css +++ b/polygerrit-ui/app/styles/main.css
@@ -21,6 +21,7 @@ margin: 0; padding: 0; } + html { -webkit-text-size-adjust: none; } @@ -34,6 +35,7 @@ * IE has shoddy support for the font shorthand property. * Work around this using font-size and font-family. */ + -webkit-text-size-adjust: none; font-size: 13px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; line-height: 1.4;
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html new file mode 100644 index 0000000..cc1dabd --- /dev/null +++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -0,0 +1,78 @@ +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> +<dom-module id="shared-styles"> + <template> + <style> + /* CSS reset */ + html, body, button, div, span, applet, object, iframe, h1, h2, h3, + h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, + code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, + sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, + label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, + aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, + main, menu, nav, output, ruby, section, summary, time, mark, audio, video { + border: 0; + box-sizing: border-box; + font-size: 100%; + font: inherit; + margin: 0; + padding: 0; + vertical-align: baseline; + } + input, + iron-autogrow-textarea { + box-sizing: border-box; + margin: 0; + padding: 0; + } + body { + line-height: 1; + } + ol, ul { + list-style: none; + } + blockquote, q { + quotes: none; + } + blockquote:before, blockquote:after, + q:before, q:after { + content: ''; + content: none; + } + table { + border-collapse: collapse; + border-spacing: 0; + } + /* Other Shared Styles*/ + h1 { + font-size: 2em; + font-weight: bold; + } + h2 { + font-size: 1.5em; + font-weight: bold; + } + h3 { + font-size: 1.17em; + font-weight: bold; + } + /* Stopgap solution until we remove hidden$ attributes. */ + [hidden] { + display: none !important; + } + </style> + </template> +</dom-module>
diff --git a/polygerrit-ui/app/test/common-test-setup.html b/polygerrit-ui/app/test/common-test-setup.html new file mode 100644 index 0000000..7c894b6 --- /dev/null +++ b/polygerrit-ui/app/test/common-test-setup.html
@@ -0,0 +1,38 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" + href="../bower_components/polymer-resin/standalone/polymer-resin.html" /> +<script> + security.polymer_resin.install({ + allowedIdentifierPrefixes: [''], + reportHandler(isViolation, fmt, ...args) { + const log = security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER; + log(isViolation, fmt, ...args); + if (isViolation) { + // This will cause the test to fail if there is a data binding + // violation. + throw new Error( + 'polymer-resin violation: ' + fmt + + JSON.stringify(args)); + } + }, + }); +</script> +<link rel="import" + href="../bower_components/iron-test-helpers/iron-test-helpers.html" /> +<link rel="import" href="test-router.html" />
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index 4dcc9a8..1f51774 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html
@@ -21,12 +21,22 @@ <script src="../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> <script src="../bower_components/web-component-tester/browser.js"></script> <script> - var testFiles = []; - var elementsPath = '../elements/'; - var behaviorsPath = '../behaviors/'; + const testFiles = []; + const elementsPath = '../elements/'; + const behaviorsPath = '../behaviors/'; // Elements tests. - [ + const elements = [ + // This seemed to be flakey when it was farther down the list. Keep at the + // beginning. + 'gr-app_test.html', + 'admin/gr-admin-create-project/gr-admin-create-project_test.html', + 'admin/gr-admin-group-list/gr-admin-group-list_test.html', + 'admin/gr-admin-plugin-list/gr-admin-plugin-list_test.html', + 'admin/gr-admin-project/gr-admin-project_test.html', + 'admin/gr-admin-project-list/gr-admin-project-list_test.html', + 'admin/gr-admin-view/gr-admin-view_test.html', + 'admin/gr-project-detail-list/gr-project-detail-list_test.html', 'change-list/gr-change-list-item/gr-change-list-item_test.html', 'change-list/gr-change-list-view/gr-change-list-view_test.html', 'change-list/gr-change-list/gr-change-list_test.html', @@ -34,27 +44,33 @@ 'change/gr-account-list/gr-account-list_test.html', 'change/gr-change-actions/gr-change-actions_test.html', 'change/gr-change-metadata/gr-change-metadata_test.html', + 'change/gr-change-metadata/gr-change-metadata-it_test.html', 'change/gr-change-view/gr-change-view_test.html', 'change/gr-comment-list/gr-comment-list_test.html', 'change/gr-commit-info/gr-commit-info_test.html', + 'change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html', 'change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html', 'change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html', - 'change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html', 'change/gr-download-dialog/gr-download-dialog_test.html', + 'change/gr-label-scores/gr-label-scores_test.html', + 'change/gr-label-score-row/gr-label-score-row_test.html', 'change/gr-file-list/gr-file-list_test.html', 'change/gr-message/gr-message_test.html', 'change/gr-messages-list/gr-messages-list_test.html', 'change/gr-related-changes-list/gr-related-changes-list_test.html', 'change/gr-reply-dialog/gr-reply-dialog_test.html', + 'change/gr-reply-dialog/gr-reply-dialog-it_test.html', 'change/gr-reviewer-list/gr-reviewer-list_test.html', 'core/gr-account-dropdown/gr-account-dropdown_test.html', 'core/gr-error-manager/gr-error-manager_test.html', 'core/gr-main-header/gr-main-header_test.html', + 'core/gr-navigation/gr-navigation_test.html', + 'core/gr-router/gr-router_test.html', 'core/gr-reporting/gr-reporting_test.html', 'core/gr-search-bar/gr-search-bar_test.html', 'diff/gr-diff-builder/gr-diff-builder_test.html', - 'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html', 'diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html', + 'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html', 'diff/gr-diff-comment/gr-diff-comment_test.html', 'diff/gr-diff-cursor/gr-diff-cursor_test.html', 'diff/gr-diff-highlight/gr-annotation_test.html', @@ -70,7 +86,8 @@ 'diff/gr-selection-action-box/gr-selection-action-box_test.html', 'diff/gr-syntax-layer/gr-syntax-layer_test.html', 'diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html', - 'gr-app_test.html', + 'plugins/gr-external-style/gr-external-style_test.html', + 'plugins/gr-plugin-host/gr-plugin-host_test.html', 'settings/gr-account-info/gr-account-info_test.html', 'settings/gr-change-table-editor/gr-change-table-editor_test.html', 'settings/gr-email-editor/gr-email-editor_test.html', @@ -85,45 +102,55 @@ 'shared/gr-account-link/gr-account-link_test.html', 'shared/gr-alert/gr-alert_test.html', 'shared/gr-autocomplete/gr-autocomplete_test.html', + 'shared/gr-textarea/gr-textarea_test.html', + 'shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html', 'shared/gr-avatar/gr-avatar_test.html', 'shared/gr-button/gr-button_test.html', 'shared/gr-change-star/gr-change-star_test.html', 'shared/gr-confirm-dialog/gr-confirm-dialog_test.html', + 'shared/gr-copy-clipboard/gr-copy-clipboard_test.html', 'shared/gr-cursor-manager/gr-cursor-manager_test.html', 'shared/gr-date-formatter/gr-date-formatter_test.html', + 'shared/gr-download-commands/gr-download-commands_test.html', 'shared/gr-editable-content/gr-editable-content_test.html', 'shared/gr-editable-label/gr-editable-label_test.html', 'shared/gr-formatted-text/gr-formatted-text_test.html', + 'shared/gr-page-nav/gr-page-nav_test.html', 'shared/gr-js-api-interface/gr-change-actions-js-api_test.html', 'shared/gr-js-api-interface/gr-change-reply-js-api_test.html', 'shared/gr-js-api-interface/gr-js-api-interface_test.html', 'shared/gr-linked-chip/gr-linked-chip_test.html', 'shared/gr-linked-text/gr-linked-text_test.html', + 'shared/gr-list-view/gr-list-view_test.html', + 'shared/gr-rest-api-interface/gr-gapi-auth_test.html', 'shared/gr-rest-api-interface/gr-rest-api-interface_test.html', 'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html', 'shared/gr-select/gr-select_test.html', 'shared/gr-storage/gr-storage_test.html', 'shared/gr-tooltip/gr-tooltip_test.html', 'shared/gr-tooltip-content/gr-tooltip-content_test.html', - ].forEach(function(file) { + ]; + for (let file of elements) { file = elementsPath + file; testFiles.push(file); testFiles.push(file + '?dom=shadow'); - }); + } // Behaviors tests. - [ + const behaviors = [ 'base-url-behavior/base-url-behavior_test.html', + 'keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html', 'rest-client-behavior/rest-client-behavior_test.html', 'gr-change-table-behavior/gr-change-table-behavior_test.html', 'gr-patch-set-behavior/gr-patch-set-behavior_test.html', 'gr-path-list-behavior/gr-path-list-behavior_test.html', 'gr-tooltip-behavior/gr-tooltip-behavior_test.html', - ].forEach(function(file) { + ]; + for (let file of behaviors) { // Behaviors do not utilize the DOM, so no shadow DOM test is necessary. file = behaviorsPath + file; testFiles.push(file); - }); + } WCT.loadSuites(testFiles); </script>
diff --git a/polygerrit-ui/app/test/test-router.html b/polygerrit-ui/app/test/test-router.html new file mode 100644 index 0000000..37a20c4 --- /dev/null +++ b/polygerrit-ui/app/test/test-router.html
@@ -0,0 +1,21 @@ +<!DOCTYPE html> +<!-- +Copyright (C) 2017 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +--> + +<link rel="import" href="../elements/core/gr-navigation/gr-navigation.html"> +<script> + Gerrit.Nav.setup(url => { /* noop */ }, params => ''); +</script>
diff --git a/polygerrit-ui/app/wct_test.sh b/polygerrit-ui/app/wct_test.sh index e6f3e0e..e81adfb 100755 --- a/polygerrit-ui/app/wct_test.sh +++ b/polygerrit-ui/app/wct_test.sh
@@ -36,10 +36,10 @@ 'sauce': { 'disabled': true, 'browsers': [ - 'OS X 10.11/chrome', + 'OS X 10.12/chrome', 'Windows 10/chrome', 'Linux/firefox', - 'OS X 10.11/safari', + 'OS X 10.12/safari', 'Windows 10/microsoftedge' ] }
diff --git a/polygerrit-ui/server.go b/polygerrit-ui/server.go index b19137e..a42acc3 100644 --- a/polygerrit-ui/server.go +++ b/polygerrit-ui/server.go
@@ -17,9 +17,11 @@ import ( "bufio" "compress/gzip" + "encoding/json" "errors" "flag" "io" + "io/ioutil" "log" "net" "net/http" @@ -53,7 +55,7 @@ if len(*plugins) > 0 { http.Handle("/plugins/", http.StripPrefix("/plugins/", http.FileServer(http.Dir(*plugins)))) - log.Println("Local plugins at", *plugins) + log.Println("Local plugins from", *plugins) } log.Println("Serving on port", *port) log.Fatal(http.ListenAndServe(*port, &server{})) @@ -77,12 +79,89 @@ } defer res.Body.Close() w.WriteHeader(res.StatusCode) - if _, err := io.Copy(w, res.Body); err != nil { + if _, err := io.Copy(w, patchResponse(r, res)); err != nil { log.Println("Error copying response to ResponseWriter:", err) return } } +func getJsonPropByPath(json map[string]interface{}, path []string) interface{} { + prop, path := path[0], path[1:] + if json[prop] == nil { + return nil + } + switch json[prop].(type) { + case map[string]interface{}: // map + return getJsonPropByPath(json[prop].(map[string]interface{}), path) + case []interface{}: // array + return json[prop].([]interface{}) + default: + return json[prop].(interface{}) + } +} + +func setJsonPropByPath(json map[string]interface{}, path []string, value interface{}) { + prop, path := path[0], path[1:] + if json[prop] == nil { + return // path not found + } + if len(path) > 0 { + setJsonPropByPath(json[prop].(map[string]interface{}), path, value) + } else { + json[prop] = value + } +} + +func patchResponse(r *http.Request, res *http.Response) io.Reader { + switch r.URL.EscapedPath() { + case "/config/server/info": + return injectLocalPlugins(res.Body) + default: + return res.Body + } +} + +func injectLocalPlugins(r io.Reader) io.Reader { + if len(*plugins) == 0 { + return r + } + // Skip escape prefix + io.CopyN(ioutil.Discard, r, 5) + dec := json.NewDecoder(r) + + var response map[string]interface{} + err := dec.Decode(&response) + if err != nil { + log.Fatal(err) + } + + // Configuration path in the JSON server response + pluginsPath := []string{"plugin", "html_resource_paths"} + + htmlResources := getJsonPropByPath(response, pluginsPath).([]interface{}) + files, err := ioutil.ReadDir(*plugins) + if err != nil { + log.Fatal(err) + } + for _, f := range files { + if strings.HasSuffix(f.Name(), ".html") { + htmlResources = append(htmlResources, "plugins/"+f.Name()) + } + } + setJsonPropByPath(response, pluginsPath, htmlResources) + + reader, writer := io.Pipe() + go func() { + defer writer.Close() + io.WriteString(writer, ")]}'") // Write escape prefix + err := json.NewEncoder(writer).Encode(&response) + if err != nil { + log.Fatal(err) + } + }() + return reader +} + func handleAccountDetail(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) }
diff --git a/tools/bzl/asciidoc.bzl b/tools/bzl/asciidoc.bzl index c39541d..1ddb692 100644 --- a/tools/bzl/asciidoc.bzl +++ b/tools/bzl/asciidoc.bzl
@@ -1,6 +1,6 @@ def documentation_attributes(): return [ - "toc", + "toc2", 'newline="\\n"', 'asterisk="*"', 'plus="+"',
diff --git a/tools/coverage.sh b/tools/coverage.sh new file mode 100755 index 0000000..8fa979f --- /dev/null +++ b/tools/coverage.sh
@@ -0,0 +1,45 @@ +#!/bin/sh +# +# Usage +# +# COVERAGE_CPUS=32 tools/coverage.sh [/path/to/report-directory/] +# +# COVERAGE_CPUS defaults to 2, and the default destination is a temp +# dir. + +genhtml=$(which genhtml) +if [[ -z "${genhtml}" ]]; then + echo "Install 'genhtml' (contained in the 'lcov' package)" + exit 1 +fi + +destdir="$1" +if [[ -z "${destdir}" ]]; then + destdir=$(mktemp -d /tmp/gerritcov.XXXXXX) +fi + +echo "Running 'bazel coverage'; this may take a while" + +# coverage is expensive to run; use --jobs=2 to avoid overloading the +# machine. +bazel coverage -k --jobs=${COVERAGE_CPUS:-2} -- ... -//gerrit-common:auto_value_tests + +# The coverage data contains filenames relative to the Java root, and +# genhtml has no logic to search these elsewhere. Workaround this +# limitation by running genhtml in a directory with the files in the +# right place. Also -inexplicably- genhtml wants to have the source +# files relative to the output directory. +mkdir -p ${destdir}/ +cp -a */src/{main,test}/java/* ${destdir}/ + +base=$(bazel info bazel-testlogs) +for f in $(find ${base} -name 'coverage.dat') ; do + cp $f ${destdir}/$(echo $f| sed "s|${base}/||" | sed "s|/|_|g") +done + +cd ${destdir} +find -name '*coverage.dat' -size 0 -delete + +genhtml -o . --ignore-errors source *coverage.dat + +echo "coverage report at file://${destdir}/index.html"
diff --git a/tools/eclipse/project.py b/tools/eclipse/project.py index 97c2b12..d8072e2 100755 --- a/tools/eclipse/project.py +++ b/tools/eclipse/project.py
@@ -154,7 +154,7 @@ src.add(m.group(1)) # Exceptions: both source and lib if p.endswith('libquery_parser.jar') or \ - p.endswith('prolog/libcommon.jar'): + p.endswith('libprolog-common.jar'): lib.add(p) # JGit dependency from external repository if 'gerrit-' not in p and 'jgit' in p:
diff --git a/tools/js/bower2bazel.py b/tools/js/bower2bazel.py index 745a7de..ea0402c 100755 --- a/tools/js/bower2bazel.py +++ b/tools/js/bower2bazel.py
@@ -53,6 +53,7 @@ "neon-animation": "polymer", "page": "page.js", "polymer": "polymer", + "polymer-resin": "polymer", "promise-polyfill": "promise-polyfill", "web-animations-js": "Apache2.0", "webcomponentsjs": "polymer",
diff --git a/tools/version.py b/tools/version.py index 2603829..fed6d5d 100755 --- a/tools/version.py +++ b/tools/version.py
@@ -53,7 +53,3 @@ src_pattern = re.compile(r'^(GERRIT_VERSION = ")([-.\w]+)(")$', re.MULTILINE) replace_in_file('version.bzl', src_pattern) - -src_pattern = re.compile(r'^(\s*-DarchetypeVersion=)([-.\w]+)(\s*\\)$', - re.MULTILINE) -replace_in_file(os.path.join('Documentation', 'dev-plugins.txt'), src_pattern)
diff --git a/version.bzl b/version.bzl index 4ddfa16..488a83f 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 = "2.14.1" +GERRIT_VERSION = "2.15-SNAPSHOT"