Merge branch 'stable-2.13' * stable-2.13: Add es6-promise for Internet Explorer Change-Id: I72f323e79332054d53aeb4548d83145bdee28cd8
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt index eeb1b30..d48034a 100644 --- a/Documentation/access-control.txt +++ b/Documentation/access-control.txt
@@ -466,7 +466,10 @@ To push lightweight (non-annotated) tags, grant `Create Reference` for reference name `+refs/tags/*+`, as lightweight -tags are implemented just like branches in Git. +tags are implemented just like branches in Git. To push a lightweight +tag on a new commit (commit not reachable from any branch/tag) grant +`Push` permission on `+refs/tags/*+` too. The `Push` permission on +`+refs/tags/*+` also allows fast-forwarding of lightweight tags. For example, to grant the possibility to create new branches under the namespace `foo`, you have to grant this permission on @@ -480,6 +483,19 @@ you grant the users the push force permission to be able to clean up stale branches. +[[category_delete]] +=== Delete Reference + +The delete reference category controls whether it is possible to delete +references, branches or tags. It doesn't allow any other update of +references. + +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 +references for new commits is bad if bypassing of code review must be +prevented. + [[category_forge_author]] === Forge Author @@ -644,7 +660,8 @@ [[category_push_annotated]] -=== Push Annotated Tag +[[category_create_annotated]] +=== Create Annotated Tag This category permits users to push an annotated tag object into the project's repository. Typically this would be done with a command line @@ -671,7 +688,7 @@ To push tags created by users other than the current user (such as tags mirrored from an upstream project), `Forge Committer Identity` -must be also granted in addition to `Push Annotated Tag`. +must be also granted in addition to `Create Annotated Tag`. To push lightweight (non annotated) tags, grant <<category_create,`Create Reference`>> for reference name @@ -682,9 +699,16 @@ option enabled for reference name `+refs/tags/*+`, as deleting a tag requires the same permission as deleting a branch. +To push an annotated tag on a new commit (commit not reachable from any +branch/tag) grant `Push` permission on `+refs/tags/*+` too. +The `Push` permission on `+refs/tags/*+` does *not* allow updating of annotated +tags, not even fast-forwarding of annotated tags. Update of annotated tags +is only allowed by granting `Push` with `force` option on `+refs/tags/*+`. + [[category_push_signed]] -=== Push Signed Tag +[[category_create_signed]] +=== Create Signed Tag This category permits users to push a PGP signed tag object into the project's repository. Typically this would be done with a command @@ -997,7 +1021,7 @@ * <<category_push_merge,`Push merge commit`>> to 'refs/heads/*' * <<category_forge_committer,`Forge Committer Identity`>> to 'refs/for/refs/heads/*' * <<category_create,`Create Reference`>> to 'refs/heads/*' -* <<category_push_annotated,`Push Annotated Tag`>> to 'refs/tags/*' +* <<category_create_annotated,`Create Annotated Tag`>> to 'refs/tags/*' [[examples_project-owner]] @@ -1067,12 +1091,15 @@ [[block]] === 'BLOCK' access rule -The 'BLOCK' rule blocks a permission globally. An inherited 'BLOCK' rule cannot -be overridden in the inheriting project. Any 'ALLOW' rule, from a different -access section or from an inheriting project, which conflicts with an -inherited 'BLOCK' rule will not be honored. Searching for 'BLOCK' rules, in -the chain of parent projects, ignores the Exclusive flag that is normally -applied to access sections. +The 'BLOCK' rule blocks a permission globally. An inherited 'BLOCK' +rule cannot be overridden in the inheriting project. Any 'ALLOW' rule +from an inheriting project, which conflicts with an inherited 'BLOCK' +rule will not be honored. Searching for 'BLOCK' rules, in the chain +of parent projects, ignores the Exclusive flag, unless the rule with +the Exclusive flag is defined on the same project as the 'BLOCK' +rule. This means within the same project a 'BLOCK' rule can be +overruled by 'ALLOW' rules on the same access section and 'ALLOW' +rules with Exclusive flag on access section for more specific refs. A 'BLOCK' rule that blocks the 'push' permission blocks any type of push, force or not. A blocking force push rule blocks only force pushes, but
diff --git a/Documentation/config-cla.txt b/Documentation/config-cla.txt index c07a24f..2234808 100644 --- a/Documentation/config-cla.txt +++ b/Documentation/config-cla.txt
@@ -37,8 +37,13 @@ Each `contributor-agreement` section within the `project.config` file must have a unique name. The section name will appear in the web UI. -If not already present, add the UUID of the groups used in the -`autoVerify` and `accepted` variables in the groups file. +If not already present, add the group(s) used in the `autoVerify` and +`accepted` variables in the `groups` file: +---- + # UUID Group Name + # + 3dedb32915ecdbef5fced9f0a2587d164cd614d4 CLA Accepted - Individual +---- Commit the configuration change, and push it back: ----
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index 1c08a68..bb112a5 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt
@@ -3331,7 +3331,9 @@ Full Name and Preferred Email. This may cause messages to be classified as spam if the user's domain has SPF or DKIM enabled and <<sendemail.smtpServer,sendemail.smtpServer>> is not a trusted -relay for that domain. +relay for that domain. You can specify +<<sendemail.allowedDomain,sendemail.allowedDomain>> to instruct Gerrit to only +send as USER if USER is from those domains. + * `MIXED` + @@ -3357,6 +3359,16 @@ + By default, MIXED. +[[sendemail.allowedDomain]]sendemail.allowedDomain:: ++ +Only used when `sendemail.from` is set to `USER`. +List of allowed domains. If user's email matches one of the domains, emails will +be sent as USER, otherwise as MIXED mode. Wildcards may be specified by +including `*` to match any number of characters, for example `*.example.com` +matches any subdomain of `example.com`. ++ +By default, `*`. + [[sendemail.smtpServer]]sendemail.smtpServer:: + Hostname (or IP address) of a SMTP server that will relay @@ -3442,6 +3454,15 @@ [[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. ++ +By default, unset, denying all cross-origin requests. + [[site.refreshHeaderFooter]]site.refreshHeaderFooter:: + If true the server checks the site header, footer and CSS files for
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt index 7121265..5483d85 100644 --- a/Documentation/config-project-config.txt +++ b/Documentation/config-project-config.txt
@@ -20,6 +20,11 @@ that you will have to configure push rights for the +refs/meta/config+ name space if you'd like to use the possibility to automate permission updates. +== Property inheritance + +If a property is set to INHERIT, then the value from the parent project is +used. If the property is not set in any parent project, the default value is +FALSE. [[file-project_config]] == The file +project.config+
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt index 71ed460..ecea83f 100644 --- a/Documentation/dev-plugins.txt +++ b/Documentation/dev-plugins.txt
@@ -36,7 +36,7 @@ ---- mvn archetype:generate -DarchetypeGroupId=com.google.gerrit \ -DarchetypeArtifactId=gerrit-plugin-archetype \ - -DarchetypeVersion=2.13-SNAPSHOT \ + -DarchetypeVersion=2.14-SNAPSHOT \ -DgroupId=com.googlesource.gerrit.plugins.testplugin \ -DartifactId=testplugin ----
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt index 921244f..900e95c 100644 --- a/Documentation/dev-release-deploy-config.txt +++ b/Documentation/dev-release-deploy-config.txt
@@ -143,10 +143,9 @@ ---- [NOTE] -In case of JGit the `pom.xml` already contains a distributionManagement -section. Replace the existing distributionManagement section with this snippet -in order to deploy the artifacts only in the gerrit-maven repository. - +In case of JGit the `pom.xml` already contains a `distributionManagement` +section. To deploy the artifacts to the `gerrit-maven` repository, replace +the existing `distributionManagement` section with this snippet. * Add these two snippets to the `pom.xml` to enable the wagon provider:
diff --git a/Documentation/dev-release-jgit.txt b/Documentation/dev-release-jgit.txt index f6d4d68..1a8b501 100644 --- a/Documentation/dev-release-jgit.txt +++ b/Documentation/dev-release-jgit.txt
@@ -1,33 +1,44 @@ -= Making a Release of JGit += Making a Snapshot Release of JGit This step is only necessary if we need to create an unofficial JGit snapshot release and publish it to the link:https://developers.google.com/storage/[Google Cloud Storage]. +[[prepare-environment]] +== Prepare the Maven Environment + +First, make sure you have done the necessary +link:dev-release-deploy-config.html#deploy-configuration-settings-xml[ +configuration in Maven `settings.xml`]. + +To apply the necessary settings in JGit's `pom.xml`, follow the instructions +in link:dev-release-deploy-config.html#deploy-configuration-subprojects[ +Configuration for Subprojects in `pom.xml`], or apply the provided diff by +executing the following command in the JGit workspace: + +---- + git apply /path/to/gerrit/tools/jgit-snapshot-deploy-pom.diff +---- [[prepare-release]] == Prepare the Release -Since JGit has its own release process we do not push any release tags -for JGit. Instead we will use the output of the `git describe` as the -version of the current JGit snapshot. +Since JGit has its own release process we do not push any release tags. Instead +we will use the output of `git describe` as the version of the current JGit +snapshot. + +In the JGit workspace, execute the following command: ---- ./tools/version.sh --release $(git describe) ---- - [[publish-release]] == Publish the Release -* Make sure you have done the configuration needed for deployment: -** link:dev-release-deploy-config.html#deploy-configuration-settings-xml[ -Configuration in Maven `settings.xml`] -** link:dev-release-deploy-config.html#deploy-configuration-subprojects[ -Configuration for Subprojects in `pom.xml`] +To deploy the new snapshot, execute the following command in the JGit +workspace: -* Deploy the new snapshot. From JGit workspace execute: -+ ---- mvn deploy ----
diff --git a/Documentation/error-prohibited-by-gerrit.txt b/Documentation/error-prohibited-by-gerrit.txt index 3d9bbad..3e5f23b 100644 --- a/Documentation/error-prohibited-by-gerrit.txt +++ b/Documentation/error-prohibited-by-gerrit.txt
@@ -17,10 +17,10 @@ link:access-control.html#category_create['Create Reference'] access right on `+refs/heads/*+` 4. if you push an annotated tag without - link:access-control.html#category_push_annotated['Push Annotated Tag'] + link:access-control.html#category_create_annotated['Create Annotated Tag'] access right on `+refs/tags/*+` 5. if you push a signed tag without - link:access-control.html#category_push_signed['Push Signed Tag'] + link:access-control.html#category_create_signed['Create Signed Tag'] access right on `+refs/tags/*+` 6. if you push a lightweight tag without the access right link:access-control.html#category_create['Create Reference'] for the reference name `+refs/tags/*+`
diff --git a/Documentation/gen_licenses.py b/Documentation/gen_licenses.py index bc2d657..15f470c 100755 --- a/Documentation/gen_licenses.py +++ b/Documentation/gen_licenses.py
@@ -95,16 +95,14 @@ if args.asciidoc: print("""\ -Gerrit Code Review - Licenses -============================= += Gerrit Code Review - Licenses Gerrit open source software is licensed under the <<Apache2_0,Apache License 2.0>>. Executable distributions also include other software components that are provided under additional licenses. [[cryptography]] -Cryptography Notice -------------------- +== Cryptography Notice This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, @@ -139,8 +137,7 @@ link:http://www.bouncycastle.org/java.html[Bouncy Castle Crypto API] to be installed by the end-user. -Licenses --------- +== Licenses """) for n in used: @@ -149,13 +146,13 @@ if args.asciidoc: print() print('[[%s]]' % name.replace('.', '_')) - print(name) - print('~' * len(name)) + print("=== " + name) print() else: print() print(name) - print('--') + print() + print('----') for d in libs: if d.startswith('//lib:') or d.startswith('//lib/'): p = d[len('//lib:'):] @@ -166,12 +163,12 @@ print('* ' + p) if args.asciidoc: print() - print('[[license]]') - print('[verse]') - print('--') + print('[[%s_license]]' % name.replace('.', '_')) + print('----') with open(n[2:].replace(':', '/')) as fd: copyfileobj(fd, stdout) - print('--') + print() + print('----') if args.asciidoc: print("""
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt index 9bf6842..c0af651 100644 --- a/Documentation/intro-user.txt +++ b/Documentation/intro-user.txt
@@ -470,11 +470,16 @@ link:user-review-ui.html#project-branch-topic[change screen]. It is also possible to link:user-upload.html#topic[set a topic on -push]. +push], either by appending `%topic=...` to the ref name or through +the use of the command line flag `--push-option`, aliased to `-o`, +followed by `topic=...`. .Set Topic on Push ---- $ git push origin HEAD:refs/for/master%topic=multi-master + + // this is the same as: + $ git push origin HEAD:refs/heads/master -o topic=multi-master ---- [[drafts]] @@ -639,6 +644,23 @@ + Email notifications are disabled. +- [[default-base-for-merges]]`Default Base For Merges`: ++ +This setting controls which base should be pre-selected in the +`Diff Against` drop-down list when the change screen is opened for a +merge commit. ++ +** `Auto Merge`: ++ +Pre-selects `Auto Merge` in the `Diff Against` drop-down list when the +change screen is opened for a merge commit. ++ +** `First Parent`: ++ +Pre-selects `Parent 1` in the `Diff Against` drop-down list when the +change screen is opened for a merge commit. ++ + - [[diff-view]]`Diff View`: + Whether the Side-by-Side diff view or the Unified diff view should be
diff --git a/Documentation/rest-api-access.txt b/Documentation/rest-api-access.txt index ee3e8ce..4531446 100644 --- a/Documentation/rest-api-access.txt +++ b/Documentation/rest-api-access.txt
@@ -132,7 +132,7 @@ }, "refs/tags/*": { "permissions": { - "pushSignedTag": { + "createSignedTag": { "rules": { "53a4f647a89ea57992571187d8025f830625192a": { "action": "ALLOW" @@ -142,7 +142,7 @@ } } }, - "pushTag": { + "createTag": { "rules": { "53a4f647a89ea57992571187d8025f830625192a": { "action": "ALLOW"
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt index 2b44855..13e5ca6 100644 --- a/Documentation/rest-api-accounts.txt +++ b/Documentation/rest-api-accounts.txt
@@ -396,7 +396,7 @@ HTTP/1.1 204 No Content ---- -If the account was already inactive the response is "`404 Not Found`". +If the account was already inactive the response is "`409 Conflict`". [[get-http-password]] === Get HTTP Password @@ -1213,6 +1213,7 @@ "size_bar_in_change_table": true, "review_category_strategy": "ABBREV", "mute_common_path_prefixes": true, + "default_base_for_merges": "FIRST_PARENT", "my": [ { "url": "#/dashboard/self", @@ -2469,6 +2470,10 @@ their own comments. On `DISABLED` the user will not receive any email notifications from Gerrit. Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `DISABLED`. +|`default_base_for_merges` || +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`. |============================================ [[preferences-input]] @@ -2527,6 +2532,10 @@ their own comments. On `DISABLED` the user will not receive any email notifications from Gerrit. Allowed values are `ENABLED`, `CC_ON_OWN_COMMENTS`, `DISABLED`. +|`default_base_for_merges` |optional| +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`. |============================================ [[query-limit-info]]
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt index be03b79..63eccfb 100644 --- a/Documentation/rest-api-changes.txt +++ b/Documentation/rest-api-changes.txt
@@ -2135,9 +2135,17 @@ Promotes change edit to a regular patch set. +Options can be provided in the request body as a +link:#publish-change-edit-input[PublishChangeEditInput] entity. + .Request ---- POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/edit:publish HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "notify": "NONE" + } ---- As response "`204 No Content`" is returned. @@ -2408,14 +2416,33 @@ [[delete-reviewer]] === Delete Reviewer -- -'DELETE /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]' +'DELETE /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]' + +'POST /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/delete' -- Deletes a reviewer from a change. +Options can be provided in the request body as a +link:#delete-reviewer-input[DeleteReviewerInput] entity. + .Request ---- DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe HTTP/1.0 + POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe/delete HTTP/1.0 +---- + +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/reviewers/John%20Doe/delete HTTP/1.0 + Content-Type: application/json; charset=UTF-8 + + { + "notify": "NONE" + } ---- .Response @@ -2456,7 +2483,7 @@ [[delete-vote]] === Delete Vote -- -'DELETE /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/link:#label-id[\{label-id\}]' +'DELETE /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/link:#label-id[\{label-id\}]' + 'POST /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]/votes/link:#label-id[\{label-id\}]/delete' -- @@ -2545,6 +2572,60 @@ Adding query parameter `links` (for example `/changes/.../commit?links`) returns a link:#commit-info[CommitInfo] with the additional field `web_links`. +[[get-merge-list]] +=== Get Merge List +-- +'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/mergelist' +-- + +Returns the list of commits that are being integrated into a target +branch by a merge commit. By default the first parent is assumed to be +uninteresting. By using the `parent` option another parent can be set +as uninteresting (parents are 1-based). + +The list of commits is returned as a list of +link:#commit-info[CommitInfo] entities. Web links are only included if +the `links` option was set. + +.Request +---- + GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/7e30d802b890ec8d0be45b1cc2a8ef092bcfc858/mergelist HTTP/1.0 +---- + +.Response +---- +HTTP/1.1 200 OK + Content-Disposition: attachment + Content-Type: application/json; charset=UTF-8 + + )]}' + [ + { + "commit": "674ac754f91e64a0efb8087e59a176484bd534d1", + "parents": [ + { + "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646", + "subject": "Migrate contributor agreements to All-Projects." + } + ], + "author": { + "name": "Shawn O. Pearce", + "email": "sop@google.com", + "date": "2012-04-24 18:08:08.000000000", + "tz": -420 + }, + "committer": { + "name": "Shawn O. Pearce", + "email": "sop@google.com", + "date": "2012-04-24 18:08:08.000000000", + "tz": -420 + }, + "subject": "Use an EventBus to manage star icons", + "message": "Use an EventBus to manage star icons\n\nImage widgets that need to ..." + } + ] +---- + [[get-revision-actions]] === Get Revision Actions -- @@ -4588,6 +4669,21 @@ link:#web-link-info[WebLinkInfo] entities. |=========================== +[[delete-reviewer-input]] +=== DeleteReviewerInput +The `DeleteReviewerInput` entity contains options for the deletion of a +reviewer. + +[options="header",cols="1,^1,5"] +|======================= +|Field Name||Description +|`notify` |optional| +Notify handling that defines to whom email notifications should be sent +after the reviewer is deleted. + +Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. + +If not set, the default is `ALL`. +|======================= + [[delete-vote-input]] === DeleteVoteInput The `DeleteVoteInput` entity contains options for the deletion of a @@ -4973,6 +5069,21 @@ outcome of the fix. |=========================== +[[publish-change-edit-input]] +=== PublishChangeEditInput +The `PublishChangeEditInput` entity contains options for the publishing of +change edit. + +[options="header",cols="1,^1,5"] +|======================= +|Field Name||Description +|`notify` |optional| +Notify handling that defines to whom email notifications should be sent +after the change edit is published. + +Allowed values are `NONE` and `ALL`. + +If not set, the default is `ALL`. +|======================= + [[push-certificate-info]] === PushCertificateInfo The `PushCertificateInfo` entity contains information about a push
diff --git a/Documentation/rest-api-config.txt b/Documentation/rest-api-config.txt index c7c0878..454a30d 100644 --- a/Documentation/rest-api-config.txt +++ b/Documentation/rest-api-config.txt
@@ -54,6 +54,14 @@ { "auth": { "auth_type": "LDAP", + "use_contributor_agreements": true, + "contributor_agreements": [ + { + "name": "Individual", + "description": "If you are going to be contributing code on your own, this is the one you want. You can sign this one online.", + "url": "static/cla_individual.html" + } + ], "editable_account_fields": [ "FULL_NAME", "REGISTER_NEW_EMAIL" @@ -1226,6 +1234,9 @@ |`use_contributor_agreements` |not set if `false`| Whether link:config-gerrit.html#auth.contributorAgreements[contributor agreements] are required. +|`contributor_agreements` |not set if `use_contributor_agreements` is `false`| +List of contributor agreements as link:rest-api-accounts.html#contributor-agreement-info[ +ContributorAgreementInfo] entities. |`editable_account_fields` || List of account fields that are editable. Possible values are `FULL_NAME`, `USER_NAME` and `REGISTER_NEW_EMAIL`.
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt index ca79b93..7942059 100644 --- a/Documentation/user-upload.txt +++ b/Documentation/user-upload.txt
@@ -176,12 +176,16 @@ To include a short tag associated with all of the changes in the same group, such as the local topic branch name, append it after -the destination branch name. In this example the short topic tag -'driver/i42' will be saved on each change this push creates or +the destination branch name or add it with the command line flag +`--push-option`, aliased to `-o`. In this example the short topic +tag 'driver/i42' will be saved on each change this push creates or updates: ---- git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%topic=driver/i42 + + // this is the same as: + git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental -o topic=driver/i42 ---- [[message]] @@ -399,11 +403,11 @@ link:access-control.html#category_push_direct['Push'] with the 'Force' option ticked. -To push annotated tags, the `Push Annotated Tag` project right must +To push annotated tags, the `Create Annotated Tag` project right must be granted to one (or more) of the user's groups. There is only one level of access in this category. -Project owners may wish to grant themselves `Push Annotated Tag` +Project owners may wish to grant themselves `Create Annotated Tag` only at times when a new release is being prepared, and otherwise grant nothing at all. This ensures that accidental pushes don't make undesired changes to the public repository.
diff --git a/VERSION b/VERSION index 573f909..3035c93 100644 --- a/VERSION +++ b/VERSION
@@ -2,4 +2,4 @@ # Used by :api_install and :api_deploy targets # when talking to the destination repository. # -GERRIT_VERSION = '2.13-SNAPSHOT' +GERRIT_VERSION = '2.14-SNAPSHOT'
diff --git a/WORKSPACE b/WORKSPACE index d465b37..97fdd05 100644 --- a/WORKSPACE +++ b/WORKSPACE
@@ -24,24 +24,30 @@ sha1 = '83cd2cd674a217ade95a4bb83a8a14f351f48bd0', ) -GUICE_VERS = '4.0' +GUICE_VERS = '4.1.0' maven_jar( name = 'guice_library', artifact = 'com.google.inject:guice:' + GUICE_VERS, - sha1 = '0f990a43d3725781b6db7cd0acf0a8b62dfd1649', + sha1 = 'eeb69005da379a10071aa4948c48d89250febb07', ) maven_jar( name = 'guice_assistedinject', artifact = 'com.google.inject.extensions:guice-assistedinject:' + GUICE_VERS, - sha1 = '8fa6431da1a2187817e3e52e967535899e2e46ca', + sha1 = 'af799dd7e23e6fe8c988da12314582072b07edcb', ) maven_jar( name = 'guice_servlet', artifact = 'com.google.inject.extensions:guice-servlet:' + GUICE_VERS, - sha1 = '4503da866f4c402b5090579b40c1c4aaefabb164', + sha1 = '90ac2db772d9b85e2b05417b74f7464bcc061dcb', +) + +maven_jar( + name = 'multibindings', + artifact = 'com.google.inject.extensions:guice-multibindings:' + GUICE_VERS, + sha1 = '3b27257997ac51b0f8d19676f1ea170427e86d51', ) maven_jar( @@ -82,27 +88,27 @@ sha1 = 'b6bd7f9d78f6fdaa3c37dae18a4bd298915f328e', ) -JGIT_VERS = '4.4.1.201607150455-r.105-g81ba2be' +JGIT_VERS = '4.4.1.201607150455-r.118-g1096652' maven_jar( name = 'jgit', repository = 'http://gerrit-maven.storage.googleapis.com/', artifact = 'org.eclipse.jgit:org.eclipse.jgit:' + JGIT_VERS, - sha1 = 'c07c9c66da7983095a40945c0bfab211a473c4c5', + sha1 = 'cd142b9030910babd119702f1c4eeae13ee90018', ) maven_jar( name = 'jgit_servlet', repository = 'http://gerrit-maven.storage.googleapis.com/', artifact = 'org.eclipse.jgit:org.eclipse.jgit.http.server:' + JGIT_VERS, - sha1 = 'bb01841b74a48abe506c2e44f238e107188e6c8f', + sha1 = 'fa67bf925001cfc663bf98772f37d5c5c1abd756', ) # TODO(davido): Remove this hack when maven_jar supports pulling sources # https://github.com/bazelbuild/bazel/issues/308 http_file( name = 'jgit_src', - sha256 = '881906cb1e6743cb78df6dd3788cab7e974308fbb98cab4915e6591a62aa9374', + sha256 = '1a0b2d637359b1b51eba4d094491ef39877a6fc192e2fc1da0422a9adf04f0b8', url = 'http://gerrit-maven.storage.googleapis.com/org/eclipse/jgit/org.eclipse.jgit/' + '%s/org.eclipse.jgit-%s-sources.jar' % (JGIT_VERS, JGIT_VERS), ) @@ -117,14 +123,14 @@ name = 'jgit_archive', repository = 'http://gerrit-maven.storage.googleapis.com/', artifact = 'org.eclipse.jgit:org.eclipse.jgit.archive:' + JGIT_VERS, - sha1 = 'fc3bc40e070c54198a046fcd3a1f7cac47163961', + sha1 = '3f45cd199e40a7c68ee07a1743c06d1c3d07308a', ) maven_jar( name = 'jgit_junit', repository = 'http://gerrit-maven.storage.googleapis.com/', artifact = 'org.eclipse.jgit:org.eclipse.jgit.junit:' + JGIT_VERS, - sha1 = 'b4565ee84a6e1d0952010282b9fcf705ac6171a7', + sha1 = 'dc7edb9c3060655c7fb93ab9b9349e815bab266f', ) maven_jar( @@ -141,8 +147,8 @@ maven_jar( name = 'gson', - artifact = 'com.google.code.gson:gson:2.6.2', - sha1 = 'f1bc476cc167b18e66c297df599b2377131a8947', + artifact = 'com.google.code.gson:gson:2.7', + sha1 = '751f548c85fa49f330cecbb1875893f971b33c4e', ) maven_jar( @@ -165,14 +171,14 @@ maven_jar( name = 'joda_time', - artifact = 'joda-time:joda-time:2.8', - sha1 = '9f2785d7184b97d005a44241ccaf980f43b9ccdb', + artifact = 'joda-time:joda-time:2.9.4', + sha1 = '1c295b462f16702ebe720bbb08f62e1ba80da41b', ) maven_jar( name = 'joda_convert', - artifact = 'org.joda:joda-convert:1.2', - sha1 = '35ec554f0cd00c956cc69051514d9488b1374dec', + artifact = 'org.joda:joda-convert:1.8.1', + sha1 = '675642ac208e0b741bc9118dcbcae44c271b992a', ) maven_jar( @@ -287,8 +293,8 @@ maven_jar( name = 'commons_net', - artifact = 'commons-net:commons-net:2.2', - sha1 = '07993c12f63c78378f8c90de4bc2ee62daa7ca3a', + artifact = 'commons-net:commons-net:3.5', + sha1 = '342fc284019f590e1308056990fdb24a08f06318', ) maven_jar( @@ -327,42 +333,42 @@ sha1 = '2e35862b0435c1b027a21f3d6eecbe50e6e08d54', ) -OW2_VERS = '5.0.3' +OW2_VERS = '5.1' maven_jar( name = 'ow2_asm', artifact = 'org.ow2.asm:asm:' + OW2_VERS, - sha1 = 'dcc2193db20e19e1feca8b1240dbbc4e190824fa', + sha1 = '5ef31c4fe953b1fd00b8a88fa1d6820e8785bb45', ) maven_jar( name = 'ow2_asm_analysis', artifact = 'org.ow2.asm:asm-analysis:' + OW2_VERS, - sha1 = 'c7126aded0e8e13fed5f913559a0dd7b770a10f3', + sha1 = '6d1bf8989fc7901f868bee3863c44f21aa63d110', ) maven_jar( name = 'ow2_asm_commons', artifact = 'org.ow2.asm:asm-commons:' + OW2_VERS, - sha1 = 'a7111830132c7f87d08fe48cb0ca07630f8cb91c', + sha1 = '25d8a575034dd9cfcb375a39b5334f0ba9c8474e', ) maven_jar( name = 'ow2_asm_tree', artifact = 'org.ow2.asm:asm-tree:' + OW2_VERS, - sha1 = '287749b48ba7162fb67c93a026d690b29f410bed', + sha1 = '87b38c12a0ea645791ead9d3e74ae5268d1d6c34', ) maven_jar( name = 'ow2_asm_util', artifact = 'org.ow2.asm:asm-util:' + OW2_VERS, - sha1 = '1512e5571325854b05fb1efce1db75fcced54389', + sha1 = 'b60e33a6bd0d71831e0c249816d01e6c1dd90a47', ) maven_jar( name = 'auto_value', - artifact = 'com.google.auto.value:auto-value:1.2', - sha1 = '6873fed014fe1de1051aae2af68ba266d2934471', + artifact = 'com.google.auto.value:auto-value:1.3-rc1', + sha1 = 'b764e0fb7e11353fbff493b22fd6e83bf091a179', ) maven_jar( @@ -371,36 +377,36 @@ sha1 = '18a9a2ce6abf32ea1b5fd31dae5210ad93f4e5e3', ) -LUCENE_VERS = '5.4.1' +LUCENE_VERS = '5.5.0' maven_jar( name = 'lucene_core', artifact = 'org.apache.lucene:lucene-core:' + LUCENE_VERS, - sha1 = 'c52b2088e2c30dfd95fd296ab6fb9cf8de9855ab', + sha1 = 'a74fd869bb5ad7fe6b4cd29df9543a34aea81164', ) maven_jar( name = 'lucene_analyzers_common', artifact = 'org.apache.lucene:lucene-analyzers-common:' + LUCENE_VERS, - sha1 = 'c2aa2c4e00eb9cdeb5ac00dc0495e70c441f681e', + sha1 = '1e0e8243a4410be20c34683034fafa7bb52e55cc', ) maven_jar( name = 'backward_codecs', artifact = 'org.apache.lucene:lucene-backward-codecs:' + LUCENE_VERS, - sha1 = '5273da96380dfab302ad06c27fe58100db4c4e2f', + sha1 = '68480974b2f54f519763632a7c1c5d51cbff3805', ) maven_jar( name = 'lucene_misc', artifact = 'org.apache.lucene:lucene-misc:' + LUCENE_VERS, - sha1 = '95f433b9d7dd470cc0aa5076e0f233907745674b', + sha1 = '504d855a1a38190622fdf990b2298c067e7d60ca', ) maven_jar( name = 'lucene_queryparser', artifact = 'org.apache.lucene:lucene-queryparser:' + LUCENE_VERS, - sha1 = 'dccd5279bfa656dec21af444a7a66820eb1cd618', + sha1 = '0fddc49725b562fd48dff0cff004336ad2a090a4', ) maven_jar( @@ -447,8 +453,8 @@ maven_jar( name = 'jsr305', - artifact = 'com.google.code.findbugs:jsr305:2.0.2', - sha1 = '516c03b21d50a644d538de0f0369c620989cd8f0', + artifact = 'com.google.code.findbugs:jsr305:3.0.1', + sha1 = 'f7be08ec23c21485b9b5a1cf1654c2ec8c58168d', ) maven_jar( @@ -458,6 +464,19 @@ sha1 = '51d35e6f8bbc2412265066cea9653dd758c95826', ) +# Keep this version of Soy synchronized with the version used in Gitiles. +maven_jar( + name = 'soy', + artifact = 'com.google.template:soy:2016-08-09', + sha1 = '43d33651e95480d515fe26c10a662faafe3ad1e4', +) + +maven_jar( + name = 'icu4j', + artifact = 'com.ibm.icu:icu4j:57.1', + sha1 = '198ea005f41219f038f4291f0b0e9f3259730e92', +) + maven_jar( name = 'dropwizard_core', artifact = 'io.dropwizard.metrics:metrics-core:3.1.2',
diff --git a/contrib/build-consistency.go b/contrib/build-consistency.go new file mode 100644 index 0000000..db63a27 --- /dev/null +++ b/contrib/build-consistency.go
@@ -0,0 +1,108 @@ +// 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 main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "strings" +) + +var ( + // Define regex to find a comment in the build files + commentRE = regexp.MustCompile("#.*") + // Define regexes to extract the lib name and sha1 + mvnRE = regexp.MustCompile("maven_jar([^)]*)") + sha1RE = regexp.MustCompile("sha1=[\"'](?P<SHA1>[^,]*)[\"']") + bSha1RE = regexp.MustCompile("bin_sha1=[\"'](?P<SHA1>[^,]*)[\"']") + libNameRE = regexp.MustCompile("name=[\"'](?P<NAME>[^,]*)[\"']") +) + +func sanitize(s string) string { + // Strip out comments + s = commentRE.ReplaceAllString(s, "") + // Remove newlines and blanks + s = strings.Replace(s, "\n", "", -1) + s = strings.Replace(s, " ", "", -1) + // WORKSPACE syntax disallows the dash char in artifact name and we use an underscore + // So we make this a consistent underscore in all files + s = strings.Replace(s, "-", "_", -1) + return s +} + +func main() { + // Load bazel WORKSPACE file + bzlDat, err := ioutil.ReadFile("WORKSPACE") + if err != nil { + log.Fatal(err) + } + bzlStr := sanitize(string(bzlDat)) + + // Walk all files nested under lib. Find, load and sanitize BUCK files + bckStrs := []string{} + err = filepath.Walk("lib/", func(path string, f os.FileInfo, err error) error { + bckFile := filepath.Join(path, "BUCK") + if _, err := os.Stat(bckFile); err == nil { + bckDat, err := ioutil.ReadFile(bckFile) + if err != nil { + return err + } + bckStrs = append(bckStrs, sanitize(string(bckDat))) + } + return nil + }) + if err != nil { + log.Fatal(err) + } + bckStr := strings.Join(bckStrs, "") + + // Find all bazel dependencies + // bzlVersions maps from a lib name to the referenced sha1 + bzlVersions := make(map[string]string) + for _, mvn := range mvnRE.FindAllString(bzlStr, -1) { + sha1s := sha1RE.FindStringSubmatch(mvn) + names := libNameRE.FindStringSubmatch(mvn) + if len(sha1s) > 1 && len(names) > 1 { + bzlVersions[names[1]] = sha1RE.FindStringSubmatch(mvn)[1] + } else { + fmt.Printf("Can't parse lib sha1/name of target %s\n", mvn) + } + } + + // Find all buck dependencies and check if we have the correct bazel dependency on file + for _, mvn := range mvnRE.FindAllString(bckStr, -1) { + sha1s := bSha1RE.FindStringSubmatch(mvn) + if len(sha1s) < 2 { + // Buck knows two dep version representations: just a SHA1 or a bin_sha1 and src_sha1 + // We try to extract the bin_sha1 first. If that fails, we use the sha1 + sha1s = sha1RE.FindStringSubmatch(mvn) + } + names := libNameRE.FindStringSubmatch(mvn) + if len(sha1s) > 1 && len(names) > 1 { + if _, ok := bzlVersions[names[1]]; !ok { + // TODO(hiesel) This produces too many false positives. + //fmt.Printf("Don't have lib %s in bazel\n", names[1]) + } else if bzlVersions[names[1]] != sha1s[1] { + fmt.Printf("SHA1 of lib %s does not match: buck has %s while bazel has %s\n", names[1], sha1s[1], bzlVersions[names[1]]) + } + } else { + fmt.Printf("Can't parse lib sha1/name on target %s\n", mvn) + } + } +}
diff --git a/gerrit-acceptance-framework/BUCK b/gerrit-acceptance-framework/BUCK index ba68fa3..4bce902 100644 --- a/gerrit-acceptance-framework/BUCK +++ b/gerrit-acceptance-framework/BUCK
@@ -1,6 +1,7 @@ SRCS = glob(['src/test/java/com/google/gerrit/acceptance/*.java']) DEPS = [ + '//gerrit-antlr:query_exception', '//gerrit-gpg:gpg', '//gerrit-launcher:launcher', '//gerrit-openid:openid', @@ -37,10 +38,20 @@ java_binary( name = 'acceptance-framework', + merge_manifests = False, + manifest_file = ':manifest', deps = [':lib'], visibility = ['PUBLIC'], ) +genrule( + name = 'manifest', + cmd = 'echo "Manifest-Version: 1.0" >$OUT;' + + 'echo "Implementation-Title: Gerrit Acceptance Test Framework" >>$OUT;' + + 'echo "Implementation-Vendor: Gerrit Code Review Project" >>$OUT', + out = 'manifest.txt', +) + java_library( name = 'lib', srcs = SRCS, @@ -57,18 +68,8 @@ ) java_sources( - name = 'src', - srcs = SRCS, - visibility = ['PUBLIC'], -) - -# The above java_sources produces a .jar somewhere in the depths of -# buck-out, but it does not bring it to -# buck-out/gen/gerrit-acceptance-framework/gerrit-acceptance-framework-src.jar. -# We fix that by the following java_binary. -java_binary( name = 'acceptance-framework-src', - deps = [ ':src' ], + srcs = SRCS, visibility = ['PUBLIC'], ) @@ -76,7 +77,7 @@ name = 'acceptance-framework-javadoc', title = 'Gerrit Acceptance Test Framework Documentation', pkgs = [' com.google.gerrit.acceptance'], - paths = ['src/test/java'], + source_jar = ':acceptance-framework-src', srcs = SRCS, deps = DEPS + PROVIDED + [ '//lib:guava',
diff --git a/gerrit-acceptance-framework/BUILD b/gerrit-acceptance-framework/BUILD index 1439ba9..934e8d1 100644 --- a/gerrit-acceptance-framework/BUILD +++ b/gerrit-acceptance-framework/BUILD
@@ -3,6 +3,7 @@ SRCS = glob(['src/test/java/com/google/gerrit/acceptance/*.java']) DEPS = [ + '//gerrit-antlr:query_exception', '//gerrit-gpg:gpg', '//gerrit-launcher:launcher', '//gerrit-openid:openid',
diff --git a/gerrit-acceptance-framework/pom.xml b/gerrit-acceptance-framework/pom.xml index b5156c0..d9d701c 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.13-SNAPSHOT</version> + <version>2.14-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 ae480c7..db4658a 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
@@ -30,12 +30,15 @@ import com.google.common.primitives.Chars; import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context; import com.google.gerrit.common.data.AccessSection; +import com.google.gerrit.common.data.ContributorAgreement; +import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.data.PermissionRule; import com.google.gerrit.extensions.api.GerritApi; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.RevisionApi; import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo; +import com.google.gerrit.extensions.api.groups.GroupApi; import com.google.gerrit.extensions.api.groups.GroupInput; import com.google.gerrit.extensions.api.projects.BranchApi; import com.google.gerrit.extensions.api.projects.BranchInput; @@ -61,6 +64,7 @@ import com.google.gerrit.server.PatchSetUtil; import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.change.Abandon; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.change.RevisionResource; import com.google.gerrit.server.change.Revisions; @@ -70,6 +74,8 @@ import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; +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.EmailHeader; import com.google.gerrit.server.notedb.ChangeNoteUtil; @@ -217,6 +223,9 @@ @Inject private EventRecorder.Factory eventRecorderFactory; + @Inject + private ChangeIndexCollection changeIndexes; + protected TestRepository<InMemoryRepository> testRepo; protected GerritServer server; protected TestAccount admin; @@ -235,6 +244,9 @@ @Inject protected ChangeNotes.Factory notesFactory; + @Inject + protected Abandon changeAbandoner; + @Rule public ExpectedException exception = ExpectedException.none(); @@ -666,6 +678,22 @@ atrScope.set(preDisableContext); } + protected void disableChangeIndexWrites() { + for (ChangeIndex i : changeIndexes.getWriteIndexes()) { + if (!(i instanceof ReadOnlyChangeIndex)) { + changeIndexes.addWriteIndex(new ReadOnlyChangeIndex(i)); + } + } + } + + protected void enableChangeIndexWrites() { + for (ChangeIndex i : changeIndexes.getWriteIndexes()) { + if (i instanceof ReadOnlyChangeIndex) { + changeIndexes.addWriteIndex(((ReadOnlyChangeIndex)i).unwrap()); + } + } + } + protected static Gson newGson() { return OutputFormat.JSON_COMPACT.newGson(); } @@ -800,6 +828,19 @@ } } + protected void removePermission(String permission, Project.NameKey project, + String ref) throws IOException, ConfigInvalidException { + try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) { + md.setMessage(String.format("Remove %s on %s", permission, ref)); + ProjectConfig config = ProjectConfig.read(md); + AccessSection s = config.getAccessSection(ref, true); + Permission p = s.getPermission(permission, true); + p.getRules().clear(); + config.commit(md); + projectCache.evict(config.getProject()); + } + } + protected void blockRead(String ref) throws Exception { block(Permission.READ, REGISTERED_USERS, ref); } @@ -940,4 +981,31 @@ (EmailHeader.String)message.headers().get("Reply-To"); assertThat(replyTo.getString()).isEqualTo(email); } + + protected ContributorAgreement configureContributorAgreement( + boolean autoVerify) throws Exception { + ContributorAgreement ca; + if (autoVerify) { + String g = createGroup("cla-test-group"); + GroupApi groupApi = gApi.groups().id(g); + groupApi.description("CLA test group"); + AccountGroup caGroup = groupCache.get( + new AccountGroup.UUID(groupApi.detail().id)); + GroupReference groupRef = GroupReference.forGroup(caGroup); + PermissionRule rule = new PermissionRule(groupRef); + rule.setAction(PermissionRule.Action.ALLOW); + ca = new ContributorAgreement("cla-test"); + ca.setAutoVerify(groupRef); + ca.setAccepted(ImmutableList.of(rule)); + } else { + ca = new ContributorAgreement("cla-test-no-auto-verify"); + } + ca.setDescription("description"); + ca.setAgreementUrl("agreement-url"); + + ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig(); + cfg.replace(ca); + saveProjectConfig(allProjects, cfg); + return ca; + } }
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 0196d1f..ceab04fe 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
@@ -28,12 +28,15 @@ import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.PushCommand; +import org.eclipse.jgit.api.TagCommand; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; 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.PersonIdent; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.FetchResult; @@ -137,6 +140,26 @@ return cloneProject(project, sshSession.getUrl() + "/" + project.get()); } + public static Ref createAnnotatedTag(TestRepository<?> testRepo, String name, + PersonIdent tagger) throws GitAPIException { + TagCommand cmd = testRepo.git().tag() + .setName(name) + .setAnnotated(true) + .setMessage(name) + .setTagger(tagger); + return cmd.call(); + } + + public static Ref updateAnnotatedTag(TestRepository<?> testRepo, String name, + PersonIdent tagger) throws GitAPIException { + TagCommand tc = testRepo.git().tag().setName(name); + return tc.setAnnotated(true) + .setMessage(name) + .setTagger(tagger) + .setForceUpdate(true) + .call(); + } + public static void fetch(TestRepository<?> testRepo, String spec) throws GitAPIException { FetchCommand fetch = testRepo.git().fetch(); @@ -144,6 +167,11 @@ fetch.call(); } + public static PushResult pushHead(TestRepository<?> testRepo, String ref) + throws GitAPIException { + return pushHead(testRepo, ref, false); + } + public static PushResult pushHead(TestRepository<?> testRepo, String ref, boolean pushTags) throws GitAPIException { return pushHead(testRepo, ref, pushTags, false); @@ -151,9 +179,27 @@ public static PushResult pushHead(TestRepository<?> testRepo, String ref, boolean pushTags, boolean force) throws GitAPIException { + return pushOne(testRepo, "HEAD", ref, pushTags, force, null); + } + + public static PushResult pushHead(TestRepository<?> testRepo, String ref, + boolean pushTags, boolean force, List<String> pushOptions) + throws GitAPIException { + return pushOne(testRepo, "HEAD", ref, pushTags, force, pushOptions); + } + + public static PushResult deleteRef(TestRepository<?> testRepo, String ref) + throws GitAPIException { + return pushOne(testRepo, "", ref, false, true, null); + } + + public static PushResult pushOne(TestRepository<?> testRepo, String source, + String target, boolean pushTags, boolean force, List<String> pushOptions) + throws GitAPIException { PushCommand pushCmd = testRepo.git().push(); pushCmd.setForce(force); - pushCmd.setRefSpecs(new RefSpec("HEAD:" + ref)); + pushCmd.setPushOptions(pushOptions); + pushCmd.setRefSpecs(new RefSpec(source + ":" + target)); if (pushTags) { pushCmd.setPushTags(); } @@ -175,6 +221,20 @@ assertThat(rru.getMessage()).isEqualTo(expectedMessage); } + public static PushResult pushTag(TestRepository<?> testRepo, String tag) + throws GitAPIException { + return pushTag(testRepo, tag, false); + } + + public static PushResult pushTag(TestRepository<?> testRepo, String tag, + boolean force) throws GitAPIException { + PushCommand pushCmd = testRepo.git().push(); + pushCmd.setForce(force); + pushCmd.setRefSpecs(new RefSpec("refs/tags/" + tag + ":refs/tags/" + tag)); + Iterable<PushResult> r = pushCmd.call(); + return Iterables.getOnlyElement(r); + } + public static Optional<String> getChangeId(TestRepository<?> tr, ObjectId id) throws IOException { RevCommit c = tr.getRevWalk().parseCommit(id);
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java index 390cae3..e9c6e96 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpResponse.java
@@ -16,6 +16,7 @@ import com.google.common.base.Preconditions; +import org.apache.http.Header; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; @@ -52,7 +53,12 @@ } public String getContentType() { - return response.getFirstHeader("X-FYI-Content-Type").getValue(); + return getHeader("X-FYI-Content-Type"); + } + + public String getHeader(String name) { + Header hdr = response.getFirstHeader(name); + return hdr != null ? hdr.getValue() : null; } public boolean hasContent() {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java index 1e0920e..669b991 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/HttpSession.java
@@ -37,7 +37,11 @@ account.username, account.httpPassword); } - protected RestResponse execute(Request request) throws IOException { + public String url() { + return url; + } + + public RestResponse execute(Request request) throws IOException { return new RestResponse(executor.execute(request).returnResponse()); } }
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 d79e573..5505263 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,6 +16,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.gerrit.acceptance.GitUtil.pushHead; +import static org.junit.Assert.assertEquals; import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; @@ -136,6 +137,7 @@ private String changeId; private Tag tag; private boolean force; + private List<String> pushOptions; private final TestRepository<?>.CommitBuilder commitBuilder; @@ -275,8 +277,8 @@ } tagCommand.call(); } - return new Result(ref, pushHead(testRepo, ref, tag != null, force), c, - subject); + return new Result(ref, + pushHead(testRepo, ref, tag != null, force, pushOptions), c, subject); } public void setTag(final Tag tag) { @@ -287,6 +289,14 @@ this.force = force; } + public List<String> getPushOptions() { + return pushOptions; + } + + public void setPushOptions(List<String> pushOptions) { + this.pushOptions = pushOptions; + } + public void noParents() { commitBuilder.noParents(); } @@ -326,6 +336,10 @@ return commit; } + public void assertPushOptions(List<String> pushOptions) { + assertEquals(pushOptions, getPushOptions()); + } + public void assertChange(Change.Status expectedStatus, String expectedTopic, TestAccount... expectedReviewers) throws OrmException, NoSuchChangeException {
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java new file mode 100644 index 0000000..cdecf05 --- /dev/null +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ReadOnlyChangeIndex.java
@@ -0,0 +1,74 @@ +// 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; + +import com.google.gerrit.reviewdb.client.Change.Id; +import com.google.gerrit.server.index.QueryOptions; +import com.google.gerrit.server.index.Schema; +import com.google.gerrit.server.index.change.ChangeIndex; +import com.google.gerrit.server.query.DataSource; +import com.google.gerrit.server.query.Predicate; +import com.google.gerrit.server.query.QueryParseException; +import com.google.gerrit.server.query.change.ChangeData; + +import java.io.IOException; + +public class ReadOnlyChangeIndex implements ChangeIndex { + private final ChangeIndex index; + + public ReadOnlyChangeIndex(ChangeIndex index) { + this.index = index; + } + + public ChangeIndex unwrap() { + return index; + } + + @Override + public Schema<ChangeData> getSchema() { + return index.getSchema(); + } + + @Override + public void close() { + index.close(); + } + + @Override + public void replace(ChangeData obj) throws IOException { + // do nothing + } + + @Override + public void delete(Id key) throws IOException { + // do nothing + } + + @Override + public void deleteAll() throws IOException { + // do nothing + } + + @Override + public DataSource<ChangeData> getSource(Predicate<ChangeData> p, + QueryOptions opts) throws QueryParseException { + return index.getSource(p, opts); + } + + @Override + public void markReady(boolean ready) throws IOException { + // do nothing + } +}
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java index 9c59e10..90ece46 100644 --- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java +++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -45,7 +45,7 @@ new BasicHeader(HttpHeaders.ACCEPT, "application/json")); } - private RestResponse getWithHeader(String endPoint, Header header) + public RestResponse getWithHeader(String endPoint, Header header) throws IOException { Request get = Request.Get(url + "/a" + endPoint); if (header != null) {
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 93525a4..ec77484 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
@@ -29,6 +29,7 @@ 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 org.junit.Assert.fail; import com.google.common.base.Function; import com.google.common.collect.FluentIterable; @@ -199,6 +200,36 @@ } @Test + public void active() throws Exception { + assertThat(gApi.accounts().id("user").getActive()).isTrue(); + gApi.accounts().id("user").setActive(false); + assertThat(gApi.accounts().id("user").getActive()).isFalse(); + gApi.accounts().id("user").setActive(true); + assertThat(gApi.accounts().id("user").getActive()).isTrue(); + } + + @Test + public void deactivateSelf() throws Exception { + exception.expect(ResourceConflictException.class); + exception.expectMessage("cannot deactivate own account"); + gApi.accounts().self().setActive(false); + } + + @Test + public void deactivateNotActive() throws Exception { + assertThat(gApi.accounts().id("user").getActive()).isTrue(); + gApi.accounts().id("user").setActive(false); + assertThat(gApi.accounts().id("user").getActive()).isFalse(); + try { + gApi.accounts().id("user").setActive(false); + fail("Expected exception"); + } catch (ResourceConflictException e) { + assertThat(e.getMessage()).isEqualTo("account not active"); + } + gApi.accounts().id("user").setActive(true); + } + + @Test public void starUnstarChange() throws Exception { PushOneCommit.Result r = createChange(); String triplet = project.get() + "~master~" + r.getChangeId();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java index 8cd696c..00b48b4 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -19,27 +19,22 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.fail; -import com.google.common.collect.ImmutableList; import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.common.data.ContributorAgreement; -import com.google.gerrit.common.data.GroupReference; -import com.google.gerrit.common.data.PermissionRule; import com.google.gerrit.extensions.api.changes.CherryPickInput; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.SubmitInput; -import com.google.gerrit.extensions.api.groups.GroupApi; import com.google.gerrit.extensions.api.projects.BranchInfo; import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.common.AgreementInfo; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeInput; +import com.google.gerrit.extensions.common.ServerInfo; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; -import com.google.gerrit.reviewdb.client.AccountGroup; -import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.TestTimeUtil; @@ -52,8 +47,8 @@ import java.util.List; public class AgreementsIT extends AbstractDaemonTest { - private ContributorAgreement ca; - private ContributorAgreement ca2; + private ContributorAgreement caAutoVerify; + private ContributorAgreement caNoAutoVerify; @ConfigSuite.Config public static Config enableAgreementsConfig() { @@ -74,32 +69,26 @@ @Before public void setUp() throws Exception { - String g = createGroup("cla-test-group"); - GroupApi groupApi = gApi.groups().id(g); - groupApi.description("CLA test group"); - AccountGroup caGroup = groupCache.get( - new AccountGroup.UUID(groupApi.detail().id)); - GroupReference groupRef = GroupReference.forGroup(caGroup); - PermissionRule rule = new PermissionRule(groupRef); - rule.setAction(PermissionRule.Action.ALLOW); - ca = new ContributorAgreement("cla-test"); - ca.setDescription("description"); - ca.setAgreementUrl("agreement-url"); - ca.setAutoVerify(groupRef); - ca.setAccepted(ImmutableList.of(rule)); - - ca2 = new ContributorAgreement("cla-test-no-auto-verify"); - ca2.setDescription("description"); - ca2.setAgreementUrl("agreement-url"); - - ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig(); - cfg.replace(ca); - cfg.replace(ca2); - saveProjectConfig(allProjects, cfg); + caAutoVerify = configureContributorAgreement(true); + caNoAutoVerify = configureContributorAgreement(false); setApiUser(user); } @Test + public void getAvailableAgreements() throws Exception { + ServerInfo info = gApi.config().server().getInfo(); + if (isContributorAgreementsEnabled()) { + assertThat(info.auth.useContributorAgreements).isTrue(); + assertThat(info.auth.contributorAgreements).hasSize(2); + assertAgreement(info.auth.contributorAgreements.get(0), caAutoVerify); + assertAgreement(info.auth.contributorAgreements.get(1), caNoAutoVerify); + } else { + assertThat(info.auth.useContributorAgreements).isNull(); + assertThat(info.auth.contributorAgreements).isNull(); + } + } + + @Test public void signNonExistingAgreement() throws Exception { assume().that(isContributorAgreementsEnabled()).isTrue(); exception.expect(UnprocessableEntityException.class); @@ -112,7 +101,7 @@ assume().that(isContributorAgreementsEnabled()).isTrue(); exception.expect(BadRequestException.class); exception.expectMessage("cannot enter a non-autoVerify agreement"); - gApi.accounts().self().signAgreement(ca2.getName()); + gApi.accounts().self().signAgreement(caNoAutoVerify.getName()); } @Test @@ -124,7 +113,7 @@ assertThat(result).isEmpty(); // Sign the agreement - gApi.accounts().self().signAgreement(ca.getName()); + gApi.accounts().self().signAgreement(caAutoVerify.getName()); // Explicitly reset the user to force a new request context setApiUser(user); @@ -133,12 +122,10 @@ result = gApi.accounts().self().listAgreements(); assertThat(result).hasSize(1); AgreementInfo info = result.get(0); - assertThat(info.name).isEqualTo(ca.getName()); - assertThat(info.description).isEqualTo(ca.getDescription()); - assertThat(info.url).isEqualTo(ca.getAgreementUrl()); + assertAgreement(info, caAutoVerify); // Signing the same agreement again has no effect - gApi.accounts().self().signAgreement(ca.getName()); + gApi.accounts().self().signAgreement(caAutoVerify.getName()); result = gApi.accounts().self().listAgreements(); assertThat(result).hasSize(1); } @@ -148,7 +135,7 @@ assume().that(isContributorAgreementsEnabled()).isFalse(); exception.expect(MethodNotAllowedException.class); exception.expectMessage("contributor agreements disabled"); - gApi.accounts().self().signAgreement(ca.getName()); + gApi.accounts().self().signAgreement(caAutoVerify.getName()); } @Test @@ -227,7 +214,7 @@ } // Sign the agreement - gApi.accounts().self().signAgreement(ca.getName()); + gApi.accounts().self().signAgreement(caAutoVerify.getName()); // Explicitly reset the user to force a new request context setApiUser(user); @@ -236,6 +223,18 @@ gApi.changes().create(newChangeInput()); } + private void assertAgreement(AgreementInfo info, ContributorAgreement ca) { + assertThat(info.name).isEqualTo(ca.getName()); + assertThat(info.description).isEqualTo(ca.getDescription()); + assertThat(info.url).isEqualTo(ca.getAgreementUrl()); + if (ca.getAutoVerify() != null) { + assertThat(info.autoVerifyGroup.name) + .isEqualTo(ca.getAutoVerify().getName()); + } else { + assertThat(info.autoVerifyGroup).isNull(); + } + } + private ChangeInput newChangeInput() { ChangeInput in = new ChangeInput(); in.branch = "master";
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 f45bfbbe..f8cfbc9 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
@@ -22,6 +22,7 @@ import com.google.gerrit.acceptance.TestAccount; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DateFormat; +import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DefaultBase; import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView; import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand; import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy; @@ -87,6 +88,7 @@ i.dateFormat = DateFormat.US; i.timeFormat = TimeFormat.HHMM_24; i.emailStrategy = EmailStrategy.DISABLED; + i.defaultBaseForMerges = DefaultBase.AUTO_MERGE; i.relativeDateInChangeTable ^= true; i.sizeBarInChangeTable ^= true; i.legacycidInChangeTable ^= true;
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 006d8a4..520c7db 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
@@ -31,6 +31,7 @@ import static org.junit.Assert.fail; import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.gerrit.acceptance.AbstractDaemonTest; @@ -41,9 +42,11 @@ import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestProjectInput; +import com.google.gerrit.common.FooterConstants; 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.DeleteReviewerInput; import com.google.gerrit.extensions.api.changes.DeleteVoteInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.RebaseInput; @@ -58,6 +61,7 @@ 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.GitPerson; import com.google.gerrit.extensions.common.LabelInfo; import com.google.gerrit.extensions.common.RevisionInfo; @@ -71,6 +75,7 @@ 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.server.CurrentUser; import com.google.gerrit.server.change.ChangeResource; import com.google.gerrit.server.config.AnonymousCowardNameProvider; import com.google.gerrit.server.git.ProjectConfig; @@ -78,7 +83,6 @@ import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.Util; import com.google.gerrit.testutil.FakeEmailSender.Message; -import com.google.gerrit.testutil.NoteDbMode; import com.google.gerrit.testutil.TestTimeUtil; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; @@ -184,6 +188,64 @@ } @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(); @@ -538,6 +600,149 @@ } @Test + public void pushCommitOfOtherUser() throws Exception { + // admin pushes commit of user + PushOneCommit push = pushFactory.create(db, user.getIdent(), testRepo); + PushOneCommit.Result result = push.to("refs/for/master"); + result.assertOkStatus(); + + ChangeInfo change = gApi.changes().id(result.getChangeId()).get(); + assertThat(change.owner._accountId).isEqualTo(admin.id.get()); + CommitInfo commit = change.revisions.get(change.currentRevision).commit; + assertThat(commit.author.email).isEqualTo(user.email); + assertThat(commit.committer.email).isEqualTo(user.email); + + // check that the author/committer was added as reviewer + Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER); + assertThat(reviewers).isNotNull(); + assertThat(reviewers).hasSize(1); + assertThat(reviewers.iterator().next()._accountId) + .isEqualTo(user.getId().get()); + assertThat(change.reviewers.get(CC)).isNull(); + + List<Message> messages = sender.getMessages(); + assertThat(messages).hasSize(1); + Message m = messages.get(0); + assertThat(m.rcpt()).containsExactly(user.emailAddress); + assertThat(m.body()) + .contains(admin.fullName + " has uploaded a new change for review"); + assertThat(m.body()) + .contains("Change subject: " + PushOneCommit.SUBJECT + "\n"); + assertMailFrom(m, admin.email); + } + + @Test + public void pushCommitOfOtherUserThatCannotSeeChange() throws Exception { + // create hidden project that is only visible to administrators + Project.NameKey p = createProject("p"); + ProjectConfig cfg = projectCache.checkedGet(p).getConfig(); + Util.allow(cfg, + Permission.READ, + groupCache.get(new AccountGroup.NameKey("Administrators")) + .getGroupUUID(), + "refs/*"); + Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*"); + saveProjectConfig(p, cfg); + + // admin pushes commit of user + TestRepository<InMemoryRepository> repo = cloneProject(p, admin); + PushOneCommit push = pushFactory.create(db, user.getIdent(), repo); + PushOneCommit.Result result = push.to("refs/for/master"); + result.assertOkStatus(); + + ChangeInfo change = gApi.changes().id(result.getChangeId()).get(); + assertThat(change.owner._accountId).isEqualTo(admin.id.get()); + CommitInfo commit = change.revisions.get(change.currentRevision).commit; + assertThat(commit.author.email).isEqualTo(user.email); + assertThat(commit.committer.email).isEqualTo(user.email); + + // check the user cannot see the change + setApiUser(user); + try { + gApi.changes().id(result.getChangeId()).get(); + fail("Expected ResourceNotFoundException"); + } catch (ResourceNotFoundException e) { + // Expected. + } + + // check that the author/committer was NOT added as reviewer (he can't see + // the change) + assertThat(change.reviewers.get(REVIEWER)).isNull(); + assertThat(change.reviewers.get(CC)).isNull(); + assertThat(sender.getMessages()).isEmpty(); + } + + @Test + public void pushCommitWithFooterOfOtherUser() throws Exception { + // admin pushes commit that references 'user' in a footer + PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo, + PushOneCommit.SUBJECT + "\n\n" + + FooterConstants.REVIEWED_BY.getName() + ": " + + user.getIdent().toExternalString(), + PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT); + PushOneCommit.Result result = push.to("refs/for/master"); + result.assertOkStatus(); + + // check that 'user' was added as reviewer + ChangeInfo change = gApi.changes().id(result.getChangeId()).get(); + Collection<AccountInfo> reviewers = change.reviewers.get(REVIEWER); + assertThat(reviewers).isNotNull(); + assertThat(reviewers).hasSize(1); + assertThat(reviewers.iterator().next()._accountId) + .isEqualTo(user.getId().get()); + assertThat(change.reviewers.get(CC)).isNull(); + + List<Message> messages = sender.getMessages(); + assertThat(messages).hasSize(1); + Message m = messages.get(0); + assertThat(m.rcpt()).containsExactly(user.emailAddress); + assertThat(m.body()).contains("Hello " + user.fullName + ",\n"); + assertThat(m.body()).contains("I'd like you to do a code review."); + assertThat(m.body()) + .contains("Change subject: " + PushOneCommit.SUBJECT + "\n"); + assertMailFrom(m, admin.email); + } + + @Test + public void pushCommitWithFooterOfOtherUserThatCannotSeeChange() + throws Exception { + // create hidden project that is only visible to administrators + Project.NameKey p = createProject("p"); + ProjectConfig cfg = projectCache.checkedGet(p).getConfig(); + Util.allow(cfg, + Permission.READ, groupCache + .get(new AccountGroup.NameKey("Administrators")).getGroupUUID(), + "refs/*"); + Util.block(cfg, Permission.READ, REGISTERED_USERS, "refs/*"); + saveProjectConfig(p, cfg); + + // admin pushes commit that references 'user' in a footer + TestRepository<InMemoryRepository> repo = cloneProject(p, admin); + PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo, + PushOneCommit.SUBJECT + "\n\n" + FooterConstants.REVIEWED_BY.getName() + + ": " + user.getIdent().toExternalString(), + PushOneCommit.FILE_NAME, PushOneCommit.FILE_CONTENT); + PushOneCommit.Result result = push.to("refs/for/master"); + result.assertOkStatus(); + + // check that 'user' cannot see the change + setApiUser(user); + try { + gApi.changes().id(result.getChangeId()).get(); + fail("Expected ResourceNotFoundException"); + } catch (ResourceNotFoundException e) { + // Expected. + } + + // check that 'user' was NOT added as cc ('user' can't see the change) + setApiUser(admin); + ChangeInfo change = gApi.changes().id(result.getChangeId()).get(); + assertThat(change.reviewers.get(REVIEWER)).isNull(); + assertThat(change.reviewers.get(CC)).isNull(); + assertThat(sender.getMessages()).isEmpty(); + } + + @Test public void addReviewerThatCannotSeeChange() throws Exception { // create hidden project that is only visible to administrators Project.NameKey p = createProject("p"); @@ -577,6 +782,22 @@ } @Test + public void addReviewerThatIsInactive() throws Exception { + PushOneCommit.Result r = 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); + } + + @Test public void addReviewer() throws Exception { TestTimeUtil.resetWithClockStep(1, SECONDS); PushOneCommit.Result r = createChange(); @@ -755,11 +976,18 @@ assertThat(reviewers.iterator().next()._accountId) .isEqualTo(user.getId().get()); + sender.clear(); gApi.changes() .id(changeId) .reviewer(user.getId().toString()) .remove(); - assertThat(gApi.changes().id(changeId).get().reviewers.isEmpty()); + assertThat(gApi.changes().id(changeId).get().reviewers).isEmpty(); + + assertThat(sender.getMessages()).hasSize(1); + Message message = sender.getMessages().get(0); + assertThat(message.body()).contains( + "Removed reviewer " + user.fullName + "."); + assertThat(message.body()).doesNotContain("with the following votes"); // Make sure the reviewer can still be added again. gApi.changes() @@ -785,6 +1013,15 @@ @Test public void removeReviewer() throws Exception { + testRemoveReviewer(true); + } + + @Test + public void removeNoNotify() throws Exception { + testRemoveReviewer(false); + } + + private void testRemoveReviewer(boolean notify) throws Exception { PushOneCommit.Result r = createChange(); String changeId = r.getChangeId(); gApi.changes() @@ -810,11 +1047,26 @@ assertThat(reviewerIt.next()._accountId) .isEqualTo(user.getId().get()); + sender.clear(); setApiUser(admin); + DeleteReviewerInput input = new DeleteReviewerInput(); + if (!notify) { + input.notify = NotifyHandling.NONE; + } gApi.changes() .id(changeId) .reviewer(user.getId().toString()) - .remove(); + .remove(input); + + if (notify) { + assertThat(sender.getMessages()).hasSize(1); + Message message = sender.getMessages().get(0); + assertThat(message.body()).contains( + "Removed reviewer " + user.fullName + " with the following votes"); + assertThat(message.body()).contains("* Code-Review+1 by " + user.fullName); + } else { + assertThat(sender.getMessages()).hasSize(0); + } reviewers = gApi.changes() .id(changeId) @@ -882,18 +1134,8 @@ .reviewer(user.getId().toString()) .votes(); - if (NoteDbMode.readWrite()) { - // When NoteDb is enabled each reviewer is explicitly recorded in the - // NoteDb and this record stays even when all votes of that user have been - // deleted, hence there is no dummy 0 approval left when a vote is - // deleted. - assertThat(m).isEmpty(); - } else { - // When NoteDb is disabled there is a dummy 0 approval on the change so - // that the user is still returned as CC when all votes of that user have - // been deleted. - assertThat(m).containsEntry("Code-Review", Short.valueOf((short)0)); - } + // Dummy 0 approval on the change to block vote copying to this patch set. + assertThat(m).containsExactly("Code-Review", Short.valueOf((short)0)); ChangeInfo c = gApi.changes() .id(r.getChangeId()) @@ -1205,6 +1447,31 @@ } @Test + public void submitStaleChange() throws Exception { + PushOneCommit.Result r = createChange(); + + disableChangeIndexWrites(); + try { + r = amendChange(r.getChangeId()); + } finally { + enableChangeIndexWrites(); + } + + gApi.changes() + .id(r.getChangeId()) + .current() + .review(ReviewInput.approve()); + + gApi.changes() + .id(r.getChangeId()) + .current() + .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();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/GetMergeListIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/GetMergeListIT.java new file mode 100644 index 0000000..0cb7d89 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/GetMergeListIT.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.acceptance.api.change; + +import static com.google.common.truth.Truth.assertThat; +import static org.eclipse.jgit.lib.Constants.HEAD; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.acceptance.PushOneCommit; +import com.google.gerrit.extensions.common.CommitInfo; + +import org.eclipse.jgit.lib.ObjectId; +import org.junit.Test; + +import java.util.List; + +@NoHttpd +public class GetMergeListIT extends AbstractDaemonTest { + + @Test + public void getMergeList() throws Exception { + ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId(); + + PushOneCommit.Result gp1 = pushFactory + .create(db, admin.getIdent(), testRepo, "grand parent 1", + ImmutableMap.of("foo", "foo-1.1", "bar", "bar-1.1")) + .to("refs/for/master"); + + PushOneCommit.Result p1 = pushFactory + .create(db, admin.getIdent(), testRepo, "parent 1", + ImmutableMap.of("foo", "foo-1.2", "bar", "bar-1.2")) + .to("refs/for/master"); + + // reset HEAD in order to create a sibling of the first change + testRepo.reset(initial); + + PushOneCommit.Result gp2 = pushFactory + .create(db, admin.getIdent(), testRepo, "grand parent 1", + ImmutableMap.of("foo", "foo-2.1", "bar", "bar-2.1")) + .to("refs/for/master"); + + PushOneCommit.Result p2 = pushFactory + .create(db, admin.getIdent(), testRepo, "parent 2", + ImmutableMap.of("foo", "foo-2.2", "bar", "bar-2.2")) + .to("refs/for/master"); + + PushOneCommit m = pushFactory.create(db, admin.getIdent(), testRepo, + "merge", ImmutableMap.of("foo", "foo-1", "bar", "bar-2")); + m.setParents(ImmutableList.of(p1.getCommit(), p2.getCommit())); + PushOneCommit.Result result = m.to("refs/for/master"); + result.assertOkStatus(); + + List<CommitInfo> mergeList = + gApi.changes().id(result.getChangeId()).current().getMergeList().get(); + assertThat(mergeList).hasSize(2); + assertThat(mergeList.get(0).commit).isEqualTo(p2.getCommit().name()); + assertThat(mergeList.get(1).commit).isEqualTo(gp2.getCommit().name()); + + mergeList = gApi.changes().id(result.getChangeId()).current().getMergeList() + .withUninterestingParent(2).get(); + assertThat(mergeList).hasSize(2); + assertThat(mergeList.get(0).commit).isEqualTo(p1.getCommit().name()); + assertThat(mergeList.get(1).commit).isEqualTo(gp1.getCommit().name()); + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java index 54fe28f..4da22d3 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/StickyApprovalsIT.java
@@ -311,6 +311,27 @@ assertVotes(c, user, 0, 0, REWORK); } + @Test + public void deleteStickyVote() throws Exception { + String label = "Code-Review"; + ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); + cfg.getLabelSections().get(label) + .setCopyMaxScore(true); + saveProjectConfig(project, cfg); + + + // Vote max score on PS1 + String changeId = createChange(REWORK); + vote(admin, changeId, label, 2); + assertVotes(detailedChange(changeId), admin, label, 2, null); + updateChange(changeId, REWORK); + assertVotes(detailedChange(changeId), admin, label, 2, REWORK); + + // Delete vote that was copied via sticky approval + deleteVote(admin, changeId, "Code-Review"); + assertVotes(detailedChange(changeId), admin, label, 0, REWORK); + } + private ChangeInfo detailedChange(String changeId) throws Exception { return gApi.changes().id(changeId) .get(EnumSet.of(ListChangesOption.DETAILED_LABELS, @@ -495,6 +516,15 @@ return c.revisions.get(c.currentRevision).kind; } + private void vote(TestAccount user, String changeId, String label, int vote) + throws Exception { + setApiUser(user); + gApi.changes() + .id(changeId) + .current() + .review(new ReviewInput().label(label, vote)); + } + private void vote(TestAccount user, String changeId, int codeReviewVote, int verifiedVote) throws Exception { setApiUser(user); @@ -504,6 +534,15 @@ gApi.changes().id(changeId).current().review(in); } + private void deleteVote(TestAccount user, String changeId, String label) + throws Exception { + setApiUser(user); + gApi.changes() + .id(changeId) + .reviewer(user.getId().toString()) + .deleteVote(label); + } + private void assertVotes(ChangeInfo c, TestAccount user, int codeReviewVote, int verifiedVote) { assertVotes(c, user, codeReviewVote, verifiedVote, null);
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 ee2dbfe..322cd4e 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
@@ -19,6 +19,7 @@ import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME; import static com.google.gerrit.acceptance.PushOneCommit.PATCH; import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT; +import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG; 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; @@ -44,8 +45,10 @@ import com.google.gerrit.extensions.client.SubmitType; import com.google.gerrit.extensions.common.ChangeInfo; import com.google.gerrit.extensions.common.ChangeMessageInfo; +import com.google.gerrit.extensions.common.ChangeType; import com.google.gerrit.extensions.common.CommentInfo; import com.google.gerrit.extensions.common.DiffInfo; +import com.google.gerrit.extensions.common.FileInfo; import com.google.gerrit.extensions.common.MergeableInfo; import com.google.gerrit.extensions.common.RevisionInfo; import com.google.gerrit.extensions.restapi.AuthException; @@ -53,7 +56,6 @@ import com.google.gerrit.extensions.restapi.ETagView; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; -import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.server.change.GetRevisionActions; import com.google.gerrit.server.change.RevisionResource; import com.google.gerrit.server.git.ProjectConfig; @@ -62,13 +64,16 @@ import com.google.inject.Inject; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.revwalk.RevCommit; import org.junit.Before; import org.junit.Test; import java.io.ByteArrayOutputStream; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -511,17 +516,17 @@ @Test public void files() throws Exception { PushOneCommit.Result r = createChange(); - assertThat(Iterables.all(gApi.changes() + Map<String, FileInfo> files = gApi.changes() .id(r.getChangeId()) .revision(r.getCommit().name()) - .files() - .keySet(), new Predicate<String>() { - @Override - public boolean apply(String file) { - return file.matches(FILE_NAME + '|' + Patch.COMMIT_MSG); - } - })) - .isTrue(); + .files(); + assertThat(files).hasSize(2); + assertThat(Iterables.all(files.keySet(), new Predicate<String>() { + @Override + public boolean apply(String file) { + return file.matches(FILE_NAME + '|' + COMMIT_MSG); + } + })).isTrue(); } @Test @@ -534,7 +539,7 @@ .revision(r.getCommit().name()) .files() .keySet() - ).containsExactly(Patch.COMMIT_MSG, "foo", "bar"); + ).containsExactly(COMMIT_MSG, "foo", "bar"); // list files against parent 1 assertThat(gApi.changes() @@ -542,7 +547,7 @@ .revision(r.getCommit().name()) .files(1) .keySet() - ).containsExactly(Patch.COMMIT_MSG, "bar"); + ).containsExactly(COMMIT_MSG, "bar"); // list files against parent 2 assertThat(gApi.changes() @@ -550,19 +555,30 @@ .revision(r.getCommit().name()) .files(2) .keySet() - ).containsExactly(Patch.COMMIT_MSG, "foo"); + ).containsExactly(COMMIT_MSG, "foo"); } @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).isNull(); - assertThat(diff.metaB.lines).isEqualTo(1); + assertThat(diff.metaA.lines).isEqualTo(1); + assertThat(diff.metaB).isNull(); } @Test @@ -610,15 +626,8 @@ @Test public void content() throws Exception { PushOneCommit.Result r = createChange(); - BinaryResult bin = gApi.changes() - .id(r.getChangeId()) - .revision(r.getCommit().name()) - .file(FILE_NAME) - .content(); - ByteArrayOutputStream os = new ByteArrayOutputStream(); - bin.writeTo(os); - String res = new String(os.toByteArray(), UTF_8); - assertThat(res).isEqualTo(FILE_CONTENT); + assertContent(r, FILE_NAME, FILE_CONTENT); + assertContent(r, COMMIT_MSG, r.getCommit().getFullMessage()); } @Test @@ -822,4 +831,88 @@ assertThat(eTag).isNotEqualTo(oldETag); 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> expectedLines = 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(); + expectedLines.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()); + expectedLines.add("Author: " + author.getName() + " <" + + author.getEmailAddress() + ">"); + expectedLines.add("AuthorDate: " + + dtfmt.format(Long.valueOf(author.getWhen().getTime()))); + + PersonIdent committer = c.getCommitterIdent(); + dtfmt.setTimeZone(committer.getTimeZone()); + expectedLines.add("Commit: " + committer.getName() + " <" + + committer.getEmailAddress() + ">"); + expectedLines.add("CommitDate: " + + dtfmt.format(Long.valueOf(committer.getWhen().getTime()))); + expectedLines.add(""); + } + + for (String line : expectedContentSideB.split("\n")) { + expectedLines.add(line); + } + + assertThat(diff.binary).isNull(); + assertThat(diff.changeType).isEqualTo(ChangeType.ADDED); + assertThat(diff.diffHeader).isNotNull(); + assertThat(diff.intralineStatus).isNull(); + assertThat(diff.webLinks).isNull(); + + assertThat(diff.metaA).isNull(); + assertThat(diff.metaB).isNotNull(); + assertThat(diff.metaB.commitId).isEqualTo(pushResult.getCommit().name()); + assertThat(diff.metaB.contentType).isEqualTo( + path.equals(COMMIT_MSG) + ? "text/x-gerrit-commit-message" + : "text/plain"); + assertThat(diff.metaB.lines).isEqualTo(expectedLines.size()); + assertThat(diff.metaB.name).isEqualTo(path); + assertThat(diff.metaB.webLinks).isNull(); + + assertThat(diff.content).hasSize(1); + DiffInfo.ContentEntry contentEntry = diff.content.get(0); + assertThat(contentEntry.b).hasSize(expectedLines.size()); + for (int i = 0; i < contentEntry.b.size(); i++) { + assertThat(contentEntry.b.get(i)).isEqualTo(expectedLines.get(i)); + } + assertThat(contentEntry.a).isNull(); + assertThat(contentEntry.ab).isNull(); + assertThat(contentEntry.common).isNull(); + assertThat(contentEntry.editA).isNull(); + assertThat(contentEntry.editB).isNull(); + assertThat(contentEntry.skip).isNull(); + } }
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 fdf18a6..affef17 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
@@ -31,6 +31,9 @@ import com.google.gerrit.common.RawInputUtil; 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.NotifyHandling; +import com.google.gerrit.extensions.api.changes.PublishChangeEditInput; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.client.InheritableBoolean; import com.google.gerrit.extensions.client.ListChangesOption; @@ -161,7 +164,7 @@ assertThat( modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME, RawInputUtil.create(CONTENT_NEW2))).isEqualTo(RefUpdate.Result.FORCED); - editUtil.publish(editUtil.byChange(change).get()); + editUtil.publish(editUtil.byChange(change).get(), NotifyHandling.NONE); Optional<ChangeEdit> edit = editUtil.byChange(change); assertThat(edit.isPresent()).isFalse(); assertChangeMessages(change, @@ -191,6 +194,24 @@ } @Test + public void publishEditNotifyRest() throws Exception { + AddReviewerInput in = new AddReviewerInput(); + in.reviewer = user.email; + gApi.changes().id(change.getChangeId()).addReviewer(in); + + modifier.createEdit(change, getCurrentPatchSet(changeId)); + assertThat( + modifier.modifyFile(editUtil.byChange(change).get(), FILE_NAME, + RawInputUtil.create(CONTENT_NEW))).isEqualTo(RefUpdate.Result.FORCED); + + sender.clear(); + PublishChangeEditInput input = new PublishChangeEditInput(); + input.notify = NotifyHandling.NONE; + adminRestSession.post(urlPublish(), input).assertNoContent(); + assertThat(sender.getMessages()).hasSize(0); + } + + @Test public void deleteEditRest() throws Exception { assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW); assertThat( @@ -354,7 +375,7 @@ edit = editUtil.byChange(change); assertThat(edit.get().getEditCommit().getFullMessage()).isEqualTo(msg); - editUtil.publish(edit.get()); + editUtil.publish(edit.get(), NotifyHandling.NONE); assertThat(editUtil.byChange(change).isPresent()).isFalse(); ChangeInfo info = get(changeId, ListChangesOption.CURRENT_COMMIT, @@ -397,7 +418,7 @@ assertThat(readContentFromJson(r)).isEqualTo(commit.getFullMessage()); } - editUtil.publish(edit.get()); + editUtil.publish(edit.get(), NotifyHandling.NONE); assertChangeMessages(change, ImmutableList.of("Uploaded patch set 1.", "Uploaded patch set 2.", @@ -700,7 +721,7 @@ assertThat(modifier.modifyMessage(edit.get(), newMsg)) .isEqualTo(RefUpdate.Result.FORCED); edit = editUtil.byChange(change); - editUtil.publish(edit.get()); + editUtil.publish(edit.get(), NotifyHandling.NONE); ChangeInfo info = get(changeId); assertThat(info.subject).isEqualTo(newSubj); @@ -727,7 +748,7 @@ editUtil.delete(editUtil.byChange(change).get()); assertThat(queryEdits()).hasSize(1); - editUtil.publish(editUtil.byChange(change2).get()); + editUtil.publish(editUtil.byChange(change2).get(), NotifyHandling.NONE); assertThat(queryEdits()).hasSize(0); setApiUser(user);
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 ca669b4..b49bdba 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
@@ -33,6 +33,7 @@ import com.google.gerrit.acceptance.GitUtil; import com.google.gerrit.acceptance.PushOneCommit; import com.google.gerrit.acceptance.TestAccount; +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.NotifyHandling; @@ -129,6 +130,27 @@ } @Test + @TestProjectInput(createEmptyCommit = false) + public void pushInitialCommitForMasterBranch() throws Exception { + RevCommit c = + testRepo.commit().message("Initial commit").insertChangeId().create(); + String id = GitUtil.getChangeId(testRepo, c).get(); + testRepo.reset(c); + + String r = "refs/for/master"; + PushResult pr = pushHead(testRepo, r, false); + assertPushOk(pr, r); + + ChangeInfo change = gApi.changes().id(id).info(); + assertThat(change.branch).isEqualTo("master"); + assertThat(change.status).isEqualTo(ChangeStatus.NEW); + + try (Repository repo = repoManager.openRepository(project)) { + assertThat(repo.resolve("master")).isNull(); + } + } + + @Test public void output() throws Exception { String url = canonicalWebUrl.get(); ObjectId initialHead = testRepo.getRepository().resolve("HEAD"); @@ -176,6 +198,21 @@ } @Test + public void pushForMasterWithTopicOption() throws Exception { + String topicOption = "topic=myTopic"; + List<String> pushOptions = new ArrayList<>(); + pushOptions.add(topicOption); + + PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo); + push.setPushOptions(pushOptions); + PushOneCommit.Result r = push.to("refs/for/master"); + + r.assertOkStatus(); + r.assertChange(Change.Status.NEW, "myTopic"); + r.assertPushOptions(pushOptions); + } + + @Test public void pushForMasterWithNotify() throws Exception { TestAccount user2 = accounts.user2(); String pushSpec = "refs/for/master" @@ -783,6 +820,23 @@ pushWithReviewerInFooter("Notauser", null); } + @Test + public void pushNewPatchsetOverridingStickyLabel() throws Exception { + ProjectConfig cfg = projectCache.checkedGet(project).getConfig(); + LabelType codeReview = Util.codeReview(); + codeReview.setCopyMaxScore(true); + cfg.getLabelSections().put(codeReview.getName(), codeReview); + saveProjectConfig(cfg); + + PushOneCommit.Result r = pushTo("refs/for/master%l=Code-Review+2"); + r.assertOkStatus(); + PushOneCommit push = + pushFactory.create(db, admin.getIdent(), testRepo, PushOneCommit.SUBJECT, + "b.txt", "anotherContent", r.getChangeId()); + r = push.to("refs/for/master%l=Code-Review+1"); + r.assertOkStatus(); + } + private void pushWithReviewerInFooter(String nameEmail, TestAccount expectedReviewer) throws Exception { int n = 5;
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 848b428..ae2e8aa 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
@@ -77,6 +77,7 @@ @Test public void submitOnPushWithAnnotatedTag() throws Exception { grant(Permission.SUBMIT, project, "refs/for/refs/heads/master"); + grant(Permission.PUSH, project, "refs/tags/*"); PushOneCommit.AnnotatedTag tag = new PushOneCommit.AnnotatedTag("v1.0", "annotation", admin.getIdent()); PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo);
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 c9c81df..1d53d66 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
@@ -20,6 +20,8 @@ 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; +import static com.google.gerrit.server.group.SystemGroupBackend.CHANGE_OWNER; +import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.fail; @@ -30,6 +32,7 @@ import com.google.gerrit.acceptance.NoHttpd; import com.google.gerrit.acceptance.PushOneCommit; 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.api.projects.ProjectInput; import com.google.gerrit.extensions.client.ChangeStatus; @@ -52,12 +55,15 @@ import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.change.RevisionResource; import com.google.gerrit.server.change.Submit; +import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.project.Util; import com.google.gerrit.testutil.ConfigSuite; import com.google.gerrit.testutil.TestTimeUtil; import com.google.inject.Inject; import org.eclipse.jgit.diff.DiffFormatter; +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; @@ -116,6 +122,74 @@ } @Test + public void submitNoPermission() throws Exception { + // create project where submit is blocked + Project.NameKey p = createProject("p"); + block(Permission.SUBMIT, REGISTERED_USERS, "refs/*", p); + + TestRepository<InMemoryRepository> repo = cloneProject(p, admin); + PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo); + PushOneCommit.Result result = push.to("refs/for/master"); + result.assertOkStatus(); + + submit(result.getChangeId(), new SubmitInput(), AuthException.class, + "submit not permitted"); + } + + @Test + public void noSelfSubmit() throws Exception { + // create project where submit is blocked for the change owner + Project.NameKey p = createProject("p"); + ProjectConfig cfg = projectCache.checkedGet(p).getConfig(); + Util.block(cfg, Permission.SUBMIT, CHANGE_OWNER, "refs/*"); + Util.allow(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/heads/*"); + Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2, + REGISTERED_USERS, "refs/*"); + saveProjectConfig(p, cfg); + + TestRepository<InMemoryRepository> repo = cloneProject(p, admin); + PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo); + PushOneCommit.Result result = push.to("refs/for/master"); + result.assertOkStatus(); + + ChangeInfo change = gApi.changes().id(result.getChangeId()).get(); + assertThat(change.owner._accountId).isEqualTo(admin.id.get()); + + submit(result.getChangeId(), new SubmitInput(), AuthException.class, + "submit not permitted"); + + setApiUser(user); + submit(result.getChangeId()); + } + + @Test + public void onlySelfSubmit() throws Exception { + // create project where only the change owner can submit + Project.NameKey p = createProject("p"); + ProjectConfig cfg = projectCache.checkedGet(p).getConfig(); + Util.block(cfg, Permission.SUBMIT, REGISTERED_USERS, "refs/*"); + Util.allow(cfg, Permission.SUBMIT, CHANGE_OWNER, "refs/*"); + Util.allow(cfg, Permission.forLabel("Code-Review"), -2, +2, + REGISTERED_USERS, "refs/*"); + saveProjectConfig(p, cfg); + + TestRepository<InMemoryRepository> repo = cloneProject(p, admin); + PushOneCommit push = pushFactory.create(db, admin.getIdent(), repo); + PushOneCommit.Result result = push.to("refs/for/master"); + result.assertOkStatus(); + + ChangeInfo change = gApi.changes().id(result.getChangeId()).get(); + assertThat(change.owner._accountId).isEqualTo(admin.id.get()); + + setApiUser(user); + submit(result.getChangeId(), new SubmitInput(), AuthException.class, + "submit not permitted"); + + setApiUser(admin); + submit(result.getChangeId()); + } + + @Test public void submitWholeTopic() throws Exception { assume().that(isSubmitWholeTopicEnabled()).isTrue(); PushOneCommit.Result change1 =
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 aa7e864..b1b3f2a 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
@@ -32,7 +32,9 @@ import com.google.gerrit.extensions.api.changes.ReviewResult; import com.google.gerrit.extensions.client.ReviewerState; import com.google.gerrit.extensions.common.AccountInfo; +import com.google.gerrit.extensions.common.ApprovalInfo; import com.google.gerrit.extensions.common.ChangeInfo; +import com.google.gerrit.extensions.common.LabelInfo; import com.google.gerrit.extensions.common.ReviewerUpdateInfo; import com.google.gerrit.server.change.PostReviewers; import com.google.gerrit.server.mail.Address; @@ -43,8 +45,10 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; public class ChangeReviewersIT extends AbstractDaemonTest { @Test @@ -260,6 +264,165 @@ } @Test + public void driveByComment() throws Exception { + // Create change owned by admin. + PushOneCommit.Result r = createChange(); + + // Post drive-by message as user. + ReviewInput input = new ReviewInput().message("hello"); + RestResponse resp = userRestSession.post( + "/changes/" + r.getChangeId() + "/revisions/" + + r.getCommit().getName() + "/review", input); + ReviewResult result = readContentFromJson(resp, 200, ReviewResult.class); + assertThat(result.labels).isNull(); + assertThat(result.reviewers).isNull(); + + // Verify user is not added as reviewer. + ChangeInfo c = gApi.changes() + .id(r.getChangeId()) + .get(); + assertReviewers(c, REVIEWER); + assertReviewers(c, CC); + } + + @Test + public void addSelfAsReviewer() throws Exception { + // Create change owned by admin. + PushOneCommit.Result r = createChange(); + + // user adds self as REVIEWER. + ReviewInput input = new ReviewInput().reviewer(user.username); + RestResponse resp = userRestSession.post( + "/changes/" + r.getChangeId() + "/revisions/" + + r.getCommit().getName() + "/review", input); + ReviewResult result = readContentFromJson(resp, 200, ReviewResult.class); + assertThat(result.labels).isNull(); + assertThat(result.reviewers).isNotNull(); + assertThat(result.reviewers).hasSize(1); + + // Verify reviewer state. + ChangeInfo c = gApi.changes() + .id(r.getChangeId()) + .get(); + assertReviewers(c, REVIEWER, user); + assertReviewers(c, CC); + LabelInfo label = c.labels.get("Code-Review"); + assertThat(label).isNotNull(); + assertThat(label.all).isNotNull(); + assertThat(label.all).hasSize(1); + ApprovalInfo approval = label.all.get(0); + assertThat(approval._accountId).isEqualTo(user.getId().get()); + } + + @Test + public void addSelfAsCc() throws Exception { + // Create change owned by admin. + PushOneCommit.Result r = createChange(); + + // user adds self as CC. + ReviewInput input = new ReviewInput().reviewer(user.username, CC, false); + RestResponse resp = userRestSession.post( + "/changes/" + r.getChangeId() + "/revisions/" + + r.getCommit().getName() + "/review", input); + ReviewResult result = readContentFromJson(resp, 200, ReviewResult.class); + assertThat(result.labels).isNull(); + assertThat(result.reviewers).isNotNull(); + assertThat(result.reviewers).hasSize(1); + + // Verify reviewer state. + ChangeInfo c = gApi.changes() + .id(r.getChangeId()) + .get(); + if (notesMigration.readChanges()) { + assertReviewers(c, REVIEWER); + assertReviewers(c, CC, user); + // Verify no approvals were added. + assertThat(c.labels).isNotNull(); + LabelInfo label = c.labels.get("Code-Review"); + assertThat(label).isNotNull(); + assertThat(label.all).isNull(); + } else { + // When approvals are stored in ReviewDb, we still create a label for + // the reviewing user, and force them into the REVIEWER state. + assertReviewers(c, REVIEWER, user); + assertReviewers(c, CC); + LabelInfo label = c.labels.get("Code-Review"); + assertThat(label).isNotNull(); + assertThat(label.all).isNotNull(); + assertThat(label.all).hasSize(1); + ApprovalInfo approval = label.all.get(0); + assertThat(approval._accountId).isEqualTo(user.getId().get()); + } + } + + @Test + public void reviewerReplyWithoutVote() throws Exception { + // Create change owned by admin. + PushOneCommit.Result r = createChange(); + + // Verify reviewer state. + ChangeInfo c = gApi.changes() + .id(r.getChangeId()) + .get(); + assertReviewers(c, REVIEWER); + assertReviewers(c, CC); + LabelInfo label = c.labels.get("Code-Review"); + assertThat(label).isNotNull(); + assertThat(label.all).isNull(); + + // Add user as REVIEWER. + ReviewInput input = new ReviewInput().reviewer(user.username); + ReviewResult result = review(r.getChangeId(), r.getCommit().name(), input); + assertThat(result.labels).isNull(); + assertThat(result.reviewers).isNotNull(); + assertThat(result.reviewers).hasSize(1); + + // Verify reviewer state. Both admin and user should be REVIEWERs now, + // because admin gets forced into REVIEWER state by virtue of being owner. + c = gApi.changes() + .id(r.getChangeId()) + .get(); + assertReviewers(c, REVIEWER, admin, user); + assertReviewers(c, CC); + label = c.labels.get("Code-Review"); + assertThat(label).isNotNull(); + assertThat(label.all).isNotNull(); + assertThat(label.all).hasSize(2); + Map<Integer, Integer> approvals = new HashMap<>(); + for (ApprovalInfo approval : label.all) { + approvals.put(approval._accountId, approval.value); + } + assertThat(approvals).containsEntry(admin.getId().get(), 0); + assertThat(approvals).containsEntry(user.getId().get(), 0); + + // Comment as user without voting. This should delete the approval and + // then replace it with the default value. + input = new ReviewInput().message("hello"); + RestResponse resp = userRestSession.post( + "/changes/" + r.getChangeId() + "/revisions/" + + r.getCommit().getName() + "/review", input); + result = readContentFromJson(resp, 200, ReviewResult.class); + assertThat(result.labels).isNull(); + + // Verify reviewer state. + c = gApi.changes() + .id(r.getChangeId()) + .get(); + assertReviewers(c, REVIEWER, admin, user); + assertReviewers(c, CC); + label = c.labels.get("Code-Review"); + assertThat(label).isNotNull(); + assertThat(label.all).isNotNull(); + assertThat(label.all).hasSize(2); + approvals.clear(); + for (ApprovalInfo approval : label.all) { + approvals.put(approval._accountId, approval.value); + } + assertThat(approvals).containsEntry(admin.getId().get(), 0); + assertThat(approvals).containsEntry(user.getId().get(), 0); + } + + @Test public void reviewAndAddReviewers() throws Exception { TestAccount observer = accounts.user2(); PushOneCommit.Result r = createChange(); @@ -316,7 +479,7 @@ assertThat(m.rcpt()).containsExactly(user.emailAddress, observer.emailAddress); assertThat(m.body()).contains(admin.fullName + " has posted comments on this change."); assertThat(m.body()).contains("Change subject: " + PushOneCommit.SUBJECT + "\n"); - assertThat(m.body()).contains("Patch Set 1: Code-Review+2\n"); + assertThat(m.body()).contains("Patch Set 1: Code-Review+2"); } @Test
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 new file mode 100644 index 0000000..f5ae072 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CorsIT.java
@@ -0,0 +1,160 @@ +// 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.change; + +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_REQUEST_HEADERS; +import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD; +import static com.google.common.net.HttpHeaders.ORIGIN; +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.acceptance.AbstractDaemonTest; +import com.google.gerrit.acceptance.PushOneCommit.Result; +import com.google.gerrit.acceptance.RestResponse; +import com.google.gerrit.testutil.ConfigSuite; + +import org.apache.http.Header; +import org.apache.http.client.fluent.Request; +import org.apache.http.message.BasicHeader; +import org.eclipse.jgit.lib.Config; +import org.junit.Test; + +public class CorsIT extends AbstractDaemonTest { + @ConfigSuite.Default + public static Config allowExampleDotCom() { + Config cfg = new Config(); + cfg.setStringList( + "site", null, "allowOriginRegex", + ImmutableList.of( + "https?://(.+[.])?example[.]com", + "http://friend[.]ly")); + return cfg; + } + + @Test + public void origin() 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(); + + check(url, true, "http://example.com"); + check(url, true, "https://sub.example.com"); + check(url, true, "http://friend.ly"); + + check(url, false, "http://evil.attacker"); + check(url, false, "http://friendsly"); + } + + @Test + public void putWithOriginRefused() 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); + } + + @Test + public void preflightOk() throws Exception { + Result change = createChange(); + + String origin = "http://example.com"; + Request req = Request.Options(adminRestSession.url() + + "/a/changes/" + change.getChangeId() + "/detail"); + req.addHeader(ORIGIN, origin); + req.addHeader(ACCESS_CONTROL_REQUEST_METHOD, "GET"); + req.addHeader(ACCESS_CONTROL_REQUEST_HEADERS, "X-Requested-With"); + + RestResponse res = adminRestSession.execute(req); + res.assertOK(); + 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(); + } + } + + @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"); + + adminRestSession.execute(req).assertBadRequest(); + } + + private RestResponse 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 allowOrigin = r.getHeader(ACCESS_CONTROL_ALLOW_ORIGIN); + String allowCred = r.getHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS); + 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"); + } else { + assertThat(allowOrigin).isNull(); + assertThat(allowCred).isNull(); + assertThat(allowMethods).isNull(); + assertThat(allowHeaders).isNull(); + } + } +}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java index d5b6f14..ce7e76d 100644 --- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitResolvingMergeCommitIT.java
@@ -30,6 +30,7 @@ import com.google.gerrit.testutil.ConfigSuite; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; +import com.google.inject.Provider; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -47,7 +48,7 @@ @NoHttpd public class SubmitResolvingMergeCommitIT extends AbstractDaemonTest { @Inject - private MergeSuperSet mergeSuperSet; + private Provider<MergeSuperSet> mergeSuperSet; @Inject private Submit submit; @@ -293,7 +294,7 @@ throws MissingObjectException, IncorrectObjectTypeException, IOException, OrmException { ChangeSet cs = - mergeSuperSet.completeChangeSet(db, change.change(), user(admin)); + mergeSuperSet.get().completeChangeSet(db, change.change(), user(admin)); assertThat(submit.unmergeableChanges(cs).isEmpty()).isEqualTo(expected); }
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 54fa74c..b0280e8 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
@@ -20,19 +20,20 @@ import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.GerritConfig; import com.google.gerrit.acceptance.GerritConfigs; -import com.google.gerrit.acceptance.RestResponse; -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AuthType; +import com.google.gerrit.acceptance.NoHttpd; +import com.google.gerrit.extensions.client.AccountFieldName; +import com.google.gerrit.extensions.client.AuthType; +import com.google.gerrit.extensions.common.ServerInfo; import com.google.gerrit.server.config.AllProjectsNameProvider; import com.google.gerrit.server.config.AllUsersNameProvider; import com.google.gerrit.server.config.AnonymousCowardNameProvider; -import com.google.gerrit.server.config.GetServerInfo.ServerInfo; import org.junit.Test; import java.nio.file.Files; import java.nio.file.Path; +@NoHttpd public class ServerInfoIT extends AbstractDaemonTest { @Test @@ -74,12 +75,12 @@ @GerritConfig(name = "user.anonymousCoward", value = "Unnamed User"), }) public void serverConfig() throws Exception { - ServerInfo i = getServerConfig(); + ServerInfo i = gApi.config().server().getInfo(); // auth assertThat(i.auth.authType).isEqualTo(AuthType.HTTP); assertThat(i.auth.editableAccountFields).containsExactly( - Account.FieldName.REGISTER_NEW_EMAIL, Account.FieldName.FULL_NAME); + AccountFieldName.REGISTER_NEW_EMAIL, AccountFieldName.FULL_NAME); assertThat(i.auth.useContributorAgreements).isTrue(); assertThat(i.auth.loginUrl).isEqualTo("https://example.com/login"); assertThat(i.auth.loginText).isEqualTo("LOGIN"); @@ -121,9 +122,9 @@ // notedb notesMigration.setReadChanges(true); - assertThat(getServerConfig().noteDbEnabled).isTrue(); + assertThat(gApi.config().server().getInfo().noteDbEnabled).isTrue(); notesMigration.setReadChanges(false); - assertThat(getServerConfig().noteDbEnabled).isNull(); + assertThat(gApi.config().server().getInfo().noteDbEnabled).isNull(); } @Test @@ -134,7 +135,7 @@ Files.write(jsplugin, "Gerrit.install(function(self){});\n".getBytes(UTF_8)); adminSshSession.exec("gerrit plugin reload"); - ServerInfo i = getServerConfig(); + ServerInfo i = gApi.config().server().getInfo(); // plugin assertThat(i.plugin.jsResourcePaths).hasSize(1); @@ -142,13 +143,13 @@ @Test public void serverConfigWithDefaults() throws Exception { - ServerInfo i = getServerConfig(); + ServerInfo i = gApi.config().server().getInfo(); // auth assertThat(i.auth.authType).isEqualTo(AuthType.OPENID); assertThat(i.auth.editableAccountFields).containsExactly( - Account.FieldName.REGISTER_NEW_EMAIL, Account.FieldName.FULL_NAME, - Account.FieldName.USER_NAME); + AccountFieldName.REGISTER_NEW_EMAIL, AccountFieldName.FULL_NAME, + AccountFieldName.USER_NAME); assertThat(i.auth.useContributorAgreements).isNull(); assertThat(i.auth.loginUrl).isNull(); assertThat(i.auth.loginText).isNull(); @@ -189,9 +190,12 @@ assertThat(i.user.anonymousCowardName).isEqualTo(AnonymousCowardNameProvider.DEFAULT); } - private ServerInfo getServerConfig() throws Exception { - RestResponse r = adminRestSession.get("/config/server/info/"); - r.assertOK(); - return newGson().fromJson(r.getReader(), ServerInfo.class); + @Test + @GerritConfig(name = "auth.contributorAgreements", value = "true") + public void anonymousAccess() throws Exception { + configureContributorAgreement(true); + + setApiUserAnonymous(); + gApi.config().server().getInfo(); } }
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 46f93b6..6377710 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
@@ -67,9 +67,9 @@ } @Test - public void createBranchByAdminCreateReferenceBlocked() throws Exception { + public void createBranchByAdminCreateReferenceBlocked_Forbidden() throws Exception { blockCreateReference(); - assertCreateSucceeds(); + assertCreateFails(AuthException.class); } @Test
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 955e580..1c9711f 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
@@ -36,6 +36,7 @@ @Before public void setUp() throws Exception { + project = createProject(name("p")); branch = new Branch.NameKey(project, "test"); branch().create(new BranchInput()); } @@ -73,10 +74,32 @@ assertDeleteForbidden(); } + @Test + public void deleteBranchByUserWithForcePushPermission() throws Exception { + grantForcePush(); + setApiUser(user); + assertDeleteSucceeds(); + } + + @Test + public void deleteBranchByUserWithDeletePermission() throws Exception { + grantDelete(); + setApiUser(user); + assertDeleteSucceeds(); + } + private void blockForcePush() throws Exception { block(Permission.PUSH, ANONYMOUS_USERS, "refs/heads/*").setForce(true); } + private void grantForcePush() throws Exception { + grant(Permission.PUSH, project, "refs/heads/*", true, ANONYMOUS_USERS); + } + + private void grantDelete() throws Exception { + allow(Permission.DELETE, ANONYMOUS_USERS, "refs/*"); + } + private void grantOwner() throws Exception { allow(Permission.OWNER, REGISTERED_USERS, "refs/*"); } @@ -99,6 +122,7 @@ private void assertDeleteForbidden() throws Exception { exception.expect(AuthException.class); + exception.expectMessage("Cannot delete branch"); branch().delete(); } }
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 new file mode 100644 index 0000000..01a2443 --- /dev/null +++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushTagIT.java
@@ -0,0 +1,290 @@ +// 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 33aa726..c4aee29 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
@@ -238,6 +238,7 @@ @Test public void createTagNotAllowed() throws Exception { + block(Permission.CREATE, REGISTERED_USERS, R_TAGS + "*"); TagInput input = new TagInput(); input.ref = "test"; exception.expect(AuthException.class); @@ -247,7 +248,7 @@ @Test public void createAnnotatedTagNotAllowed() throws Exception { - block(Permission.PUSH_TAG, REGISTERED_USERS, R_TAGS + "*"); + block(Permission.CREATE_TAG, REGISTERED_USERS, R_TAGS + "*"); TagInput input = new TagInput(); input.ref = "test"; input.message = "annotation"; @@ -338,8 +339,8 @@ private void grantTagPermissions() throws Exception { grant(Permission.CREATE, project, R_TAGS + "*"); - grant(Permission.PUSH_TAG, project, R_TAGS + "*"); - grant(Permission.PUSH_SIGNED_TAG, project, R_TAGS + "*"); + grant(Permission.CREATE_TAG, project, R_TAGS + "*"); + grant(Permission.CREATE_SIGNED_TAG, project, R_TAGS + "*"); } private ListRefsRequest<TagInfo> getTags() throws Exception {
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 d9f1a5c..70dafaa 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
@@ -544,6 +544,7 @@ + "\n" + "PS2, Line 2: nten\n" + "typo: content\n" + + "\n" + "\n"); }
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 37e551f..e33d163 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
@@ -796,7 +796,7 @@ ins = patchSetInserterFactory.create(ctl, nextPatchSetId(ctl), commit) .setValidatePolicy(CommitValidators.Policy.NONE) .setFireRevisionCreated(false) - .setSendMail(false); + .setNotify(NotifyHandling.NONE); bu.addOp(ctl.getId(), ins).execute(); } return reload(ctl);
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 40ea296..37ced5f 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
@@ -22,6 +22,7 @@ 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.common.RawInputUtil; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.common.CommitInfo; @@ -646,6 +647,39 @@ changeAndCommit(psId1_1, c1_1, 1)); } + @Test + public void getRelatedForStaleChange() throws Exception { + RevCommit c1_1 = commitBuilder() + .add("a.txt", "1") + .message("subject: 1") + .create(); + + RevCommit c2_1 = commitBuilder() + .add("b.txt", "1") + .message("subject: 1") + .create(); + pushHead(testRepo, "refs/for/master", false); + + RevCommit c2_2 = testRepo.amend(c2_1) + .add("b.txt", "2") + .create(); + testRepo.reset(c2_2); + + disableChangeIndexWrites(); + try { + pushHead(testRepo, "refs/for/master", false); + } finally { + enableChangeIndexWrites(); + } + + PatchSet.Id psId1_1 = getPatchSetId(c1_1); + PatchSet.Id psId2_1 = getPatchSetId(c2_1); + PatchSet.Id psId2_2 = new PatchSet.Id(psId2_1.changeId, psId2_1.get() + 1); + + assertRelated(psId2_2, changeAndCommit(psId2_2, c2_2, 2), + changeAndCommit(psId1_1, c1_1, 1)); + } + private List<ChangeAndCommit> getRelated(PatchSet.Id ps) throws Exception { return getRelated(ps.getParentKey(), ps.get()); } @@ -654,8 +688,9 @@ throws Exception { String url = String.format("/changes/%d/revisions/%d/related", changeId.get(), ps); - return newGson().fromJson(adminRestSession.get(url).getReader(), - RelatedInfo.class).changes; + RestResponse r = adminRestSession.get(url); + r.assertOK(); + return newGson().fromJson(r.getReader(), RelatedInfo.class).changes; } private RevCommit parseBody(RevCommit c) throws Exception {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java index 752f0d2..afd6734 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -16,13 +16,11 @@ import com.google.gerrit.common.audit.Audit; import com.google.gerrit.common.auth.SignInRequired; -import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gwtjsonrpc.common.AsyncCallback; import com.google.gwtjsonrpc.common.RemoteJsonService; import com.google.gwtjsonrpc.common.RpcImpl; import com.google.gwtjsonrpc.common.RpcImpl.Version; -import com.google.gwtjsonrpc.common.VoidResult; import java.util.List; import java.util.Set; @@ -36,14 +34,4 @@ @SignInRequired void deleteExternalIds(Set<AccountExternalId.Key> keys, AsyncCallback<Set<AccountExternalId.Key>> callback); - - @Audit - @SignInRequired - void updateContact(String fullName, String emailAddr, - AsyncCallback<Account> callback); - - @Audit - @SignInRequired - void enterAgreement(String agreementName, - AsyncCallback<VoidResult> callback); }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java deleted file mode 100644 index 22482c7..0000000 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java +++ /dev/null
@@ -1,27 +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.common.data; - -import com.google.gerrit.common.auth.SignInRequired; -import com.google.gwtjsonrpc.common.AsyncCallback; -import com.google.gwtjsonrpc.common.RemoteJsonService; -import com.google.gwtjsonrpc.common.RpcImpl; -import com.google.gwtjsonrpc.common.RpcImpl.Version; - -@RpcImpl(version = Version.V2_0) -public interface AccountService extends RemoteJsonService { - @SignInRequired - void myAgreements(AsyncCallback<AgreementInfo> callback); -}
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 97f11b4..8fae4a6 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
@@ -24,6 +24,9 @@ public static final String ABANDON = "abandon"; public static final String ADD_PATCH_SET = "addPatchSet"; public static final String CREATE = "create"; + public static final String DELETE = "delete"; + public static final String CREATE_TAG = "createTag"; + public static final String CREATE_SIGNED_TAG = "createSignedTag"; public static final String DELETE_DRAFTS = "deleteDrafts"; public static final String EDIT_HASHTAGS = "editHashtags"; public static final String EDIT_TOPIC_NAME = "editTopicName"; @@ -36,8 +39,6 @@ public static final String PUBLISH_DRAFTS = "publishDrafts"; public static final String PUSH = "push"; public static final String PUSH_MERGE = "pushMerge"; - public static final String PUSH_TAG = "pushTag"; - public static final String PUSH_SIGNED_TAG = "pushSignedTag"; public static final String READ = "read"; public static final String REBASE = "rebase"; public static final String REMOVE_REVIEWER = "removeReviewer"; @@ -46,8 +47,8 @@ public static final String VIEW_DRAFTS = "viewDrafts"; private static final List<String> NAMES_LC; - private static final int labelIndex; - private static final int labelAsIndex; + private static final int LABEL_INDEX; + private static final int LABEL_AS_INDEX; static { NAMES_LC = new ArrayList<>(); @@ -56,13 +57,14 @@ NAMES_LC.add(ABANDON.toLowerCase()); NAMES_LC.add(ADD_PATCH_SET.toLowerCase()); NAMES_LC.add(CREATE.toLowerCase()); + NAMES_LC.add(CREATE_TAG.toLowerCase()); + NAMES_LC.add(CREATE_SIGNED_TAG.toLowerCase()); + NAMES_LC.add(DELETE.toLowerCase()); NAMES_LC.add(FORGE_AUTHOR.toLowerCase()); NAMES_LC.add(FORGE_COMMITTER.toLowerCase()); NAMES_LC.add(FORGE_SERVER.toLowerCase()); NAMES_LC.add(PUSH.toLowerCase()); NAMES_LC.add(PUSH_MERGE.toLowerCase()); - NAMES_LC.add(PUSH_TAG.toLowerCase()); - NAMES_LC.add(PUSH_SIGNED_TAG.toLowerCase()); NAMES_LC.add(LABEL.toLowerCase()); NAMES_LC.add(LABEL_AS.toLowerCase()); NAMES_LC.add(REBASE.toLowerCase()); @@ -75,8 +77,8 @@ NAMES_LC.add(DELETE_DRAFTS.toLowerCase()); NAMES_LC.add(PUBLISH_DRAFTS.toLowerCase()); - labelIndex = NAMES_LC.indexOf(Permission.LABEL); - labelAsIndex = NAMES_LC.indexOf(Permission.LABEL_AS.toLowerCase()); + LABEL_INDEX = NAMES_LC.indexOf(Permission.LABEL); + LABEL_AS_INDEX = NAMES_LC.indexOf(Permission.LABEL_AS.toLowerCase()); } /** @return true if the name is recognized as a permission name. */ @@ -247,9 +249,9 @@ private static int index(Permission a) { if (isLabel(a.getName())) { - return labelIndex; + return LABEL_INDEX; } else if (isLabelAs(a.getName())) { - return labelAsIndex; + return LABEL_AS_INDEX; } int index = NAMES_LC.indexOf(a.getName().toLowerCase());
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java index 272801f..fb54ef1 100644 --- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java +++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
@@ -14,7 +14,6 @@ package com.google.gerrit.common.data; -import com.google.gerrit.common.auth.SignInRequired; import com.google.gwtjsonrpc.common.AllowCrossSiteRequest; import com.google.gwtjsonrpc.common.AsyncCallback; import com.google.gwtjsonrpc.common.RemoteJsonService; @@ -29,8 +28,5 @@ @AllowCrossSiteRequest void daemonHostKeys(AsyncCallback<List<SshHostKey>> callback); - @SignInRequired - void contributorAgreements(AsyncCallback<List<ContributorAgreement>> callback); - void clientError(String message, AsyncCallback<VoidResult> callback); }
diff --git a/gerrit-extension-api/BUCK b/gerrit-extension-api/BUCK index 61cd406..2cc8291 100644 --- a/gerrit-extension-api/BUCK +++ b/gerrit-extension-api/BUCK
@@ -74,7 +74,7 @@ name = 'extension-api-javadoc', title = 'Gerrit Review Extension API Documentation', pkgs = ['com.google.gerrit.extensions'], - paths = ['src/main/java'], + source_jar = ':extension-api-src', srcs = SRCS, deps = [ '//lib:guava',
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml index 023362f..7375893 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.13-SNAPSHOT</version> + <version>2.14-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/api/accounts/AccountApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java index c1cb3ec..9765bbf 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -34,6 +34,9 @@ public interface AccountApi { AccountInfo get() throws RestApiException; + boolean getActive() throws RestApiException; + void setActive(boolean active) throws RestApiException; + String getAvatarUrl(int size) throws RestApiException; GeneralPreferencesInfo getPreferences() throws RestApiException; @@ -85,6 +88,16 @@ } @Override + public boolean getActive() throws RestApiException { + throw new NotImplementedException(); + } + + @Override + public void setActive(boolean active) throws RestApiException { + throw new NotImplementedException(); + } + + @Override public String getAvatarUrl(int size) throws RestApiException { throw new NotImplementedException(); }
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 new file mode 100644 index 0000000..6af0dbb --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteReviewerInput.java
@@ -0,0 +1,21 @@ +// 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.extensions.api.changes; + +/** 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; +}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/PublishChangeEditInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/PublishChangeEditInput.java new file mode 100644 index 0000000..fa6f18f --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/PublishChangeEditInput.java
@@ -0,0 +1,21 @@ +// 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.extensions.api.changes; + +/** Input passed to {@code POST /changes/[id]/edit:publish/}. */ +public class PublishChangeEditInput { + /** Who to send email notifications to after the change edit is published. */ + public NotifyHandling notify = NotifyHandling.ALL; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java index d1f09e8..79cc12e 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewerApi.java
@@ -25,6 +25,7 @@ void deleteVote(String label) throws RestApiException; void deleteVote(DeleteVoteInput input) throws RestApiException; void remove() throws RestApiException; + void remove(DeleteReviewerInput input) throws RestApiException; /** * A default implementation which allows source compatibility @@ -50,5 +51,10 @@ public void remove() throws RestApiException { throw new NotImplementedException(); } + + @Override + public void remove(DeleteReviewerInput input) throws RestApiException { + throw new NotImplementedException(); + } } }
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 2731476..20dda19 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
@@ -17,6 +17,7 @@ import com.google.gerrit.extensions.client.SubmitType; 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.FileInfo; import com.google.gerrit.extensions.common.MergeableInfo; import com.google.gerrit.extensions.common.TestSubmitRuleInput; @@ -72,6 +73,33 @@ SubmitType submitType() throws RestApiException; SubmitType testSubmitType(TestSubmitRuleInput in) throws RestApiException; + MergeListRequest getMergeList() throws RestApiException; + + abstract class MergeListRequest { + private boolean addLinks; + private int uninterestingParent = 1; + + public abstract List<CommitInfo> get() throws RestApiException; + + public MergeListRequest withLinks() { + this.addLinks = true; + return this; + } + + public MergeListRequest withUninterestingParent(int uninterestingParent) { + this.uninterestingParent = uninterestingParent; + return this; + } + + public boolean getAddLinks() { + return addLinks; + } + + public int getUninterestingParent() { + return uninterestingParent; + } + } + /** * A default implementation which allows source compatibility * when adding new methods to the interface. @@ -217,5 +245,10 @@ throws RestApiException { throw new NotImplementedException(); } + + @Override + public MergeListRequest getMergeList() throws RestApiException { + throw new NotImplementedException(); + } } }
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 a43c29f..1e5c95e 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
@@ -16,6 +16,7 @@ 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.NotImplementedException; import com.google.gerrit.extensions.restapi.RestApiException; @@ -25,6 +26,8 @@ */ String getVersion() throws RestApiException; + ServerInfo getInfo() throws RestApiException; + GeneralPreferencesInfo getDefaultPreferences() throws RestApiException; GeneralPreferencesInfo setDefaultPreferences(GeneralPreferencesInfo in) throws RestApiException; @@ -43,6 +46,11 @@ } @Override + public ServerInfo getInfo() throws RestApiException { + throw new NotImplementedException(); + } + + @Override public GeneralPreferencesInfo getDefaultPreferences() throws RestApiException { throw new NotImplementedException();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java new file mode 100644 index 0000000..07d9f37 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AccountFieldName.java
@@ -0,0 +1,19 @@ +// 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.extensions.client; + +public enum AccountFieldName { + FULL_NAME, USER_NAME, REGISTER_NEW_EMAIL +} \ No newline at end of file
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java similarity index 91% rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java index 38a78ba..2056e25 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/AuthType.java
@@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.reviewdb.client; +package com.google.gerrit.extensions.client; public enum AuthType { - /** Login relies upon the OpenID standard: {@link "http://openid.net/"} */ + /** Login relies upon the <a href="http://openid.net/">OpenID standard</a> */ OPENID, - /** Login relies upon the OpenID standard: {@link "http://openid.net/"} in Single Sign On mode */ + /** Login relies upon the <a href="http://openid.net/">OpenID standard</a> in Single Sign On mode */ OPENID_SSO, /** @@ -49,7 +49,7 @@ * Jetty's SSL channel to request client's SSL certificate. For this * authentication to work a Gerrit administrator has to import the root * certificate of the trust chain used to issue the client's certificate - * into the <review-site>/etc/keystore. + * into the <review-site>/etc/keystore. * <p> * After the authentication is done Gerrit will obtain basic user * registration (name and email) from LDAP, and some group memberships.
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 9754f12..ec9190a 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
@@ -83,6 +83,25 @@ DISABLED } + public enum DefaultBase { + AUTO_MERGE(null), + FIRST_PARENT(-1); + + private final String base; + + DefaultBase(String base) { + this.base = base; + } + + DefaultBase(int base) { + this(Integer.toString(base)); + } + + public String getBase() { + return base; + } + } + public enum TimeFormat { /** 12-hour clock: 1:15 am, 2:13 pm */ HHMM_12("h:mm a"), @@ -123,6 +142,7 @@ public List<MenuItem> my; public Map<String, String> urlAliases; public EmailStrategy emailStrategy; + public DefaultBase defaultBaseForMerges; public boolean isShowInfoInReviewCategory() { return getReviewCategoryStrategy() != ReviewCategoryStrategy.NONE; @@ -180,6 +200,7 @@ p.legacycidInChangeTable = false; p.muteCommonPathPrefixes = true; p.signedOffBy = false; + p.defaultBaseForMerges = DefaultBase.FIRST_PARENT; return p; } }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInfo.java index 6ec5b1d..4242fcd 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInfo.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AgreementInfo.java
@@ -18,4 +18,5 @@ public String name; public String description; public String url; + public GroupInfo autoVerifyGroup; }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AuthInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AuthInfo.java new file mode 100644 index 0000000..1000e9c --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/AuthInfo.java
@@ -0,0 +1,35 @@ +// 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.extensions.common; + +import com.google.gerrit.extensions.client.AccountFieldName; +import com.google.gerrit.extensions.client.AuthType; + +import java.util.List; + +public class AuthInfo { + public AuthType authType; + public Boolean useContributorAgreements; + public List<AgreementInfo> contributorAgreements; + public List<AccountFieldName> editableAccountFields; + public String loginUrl; + public String loginText; + public String switchAccountUrl; + public String registerUrl; + public String registerText; + public String editFullNameUrl; + public String httpPasswordUrl; + public Boolean isGitBasicAuth; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java new file mode 100644 index 0000000..206b2f0 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ChangeConfigInfo.java
@@ -0,0 +1,25 @@ +// 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.extensions.common; + +public class ChangeConfigInfo { + public Boolean allowBlame; + public Boolean allowDrafts; + public int largeChange; + public String replyLabel; + public String replyTooltip; + public int updateDelay; + public Boolean submitWholeTopic; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java new file mode 100644 index 0000000..180e2d2 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadInfo.java
@@ -0,0 +1,23 @@ +// 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.extensions.common; + +import java.util.List; +import java.util.Map; + +public class DownloadInfo { + public Map<String, DownloadSchemeInfo> schemes; + public List<String> archives; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java new file mode 100644 index 0000000..0e8ad65 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/DownloadSchemeInfo.java
@@ -0,0 +1,25 @@ +// 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.extensions.common; + +import java.util.Map; + +public class DownloadSchemeInfo { + public String url; + public Boolean isAuthRequired; + public Boolean isAuthSupported; + public Map<String, String> commands; + public Map<String, String> cloneCommands; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GerritInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GerritInfo.java new file mode 100644 index 0000000..72c474f --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/GerritInfo.java
@@ -0,0 +1,25 @@ +// 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.extensions.common; + +public class GerritInfo { + public String allProjects; + public String allUsers; + public Boolean docSearch; + public String docUrl; + public Boolean editGpgKeys; + public String reportBugUrl; + public String reportBugText; +} \ No newline at end of file
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 new file mode 100644 index 0000000..845f7cb7 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/PluginConfigInfo.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.extensions.common; + +import java.util.List; + +public class PluginConfigInfo { + public Boolean hasAvatars; + public List<String> jsResourcePaths; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java new file mode 100644 index 0000000..e66c242 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ReceiveInfo.java
@@ -0,0 +1,19 @@ +// 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.extensions.common; + +public class ReceiveInfo { + public Boolean enableSignedPush; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java new file mode 100644 index 0000000..3dd8368 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/ServerInfo.java
@@ -0,0 +1,31 @@ +// 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.extensions.common; + +import java.util.Map; + +public class ServerInfo { + public AuthInfo auth; + public ChangeConfigInfo change; + public DownloadInfo download; + public GerritInfo gerrit; + public Boolean noteDbEnabled; + public PluginConfigInfo plugin; + public SshdInfo sshd; + public SuggestInfo suggest; + public Map<String, String> urlAliases; + public UserConfigInfo user; + public ReceiveInfo receive; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java new file mode 100644 index 0000000..98d650c --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SshdInfo.java
@@ -0,0 +1,18 @@ +// 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.extensions.common; + +public class SshdInfo { +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java new file mode 100644 index 0000000..5b0dcbe --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/SuggestInfo.java
@@ -0,0 +1,19 @@ +// 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.extensions.common; + +public class SuggestInfo { + public int from; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java new file mode 100644 index 0000000..5010689 --- /dev/null +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/common/UserConfigInfo.java
@@ -0,0 +1,19 @@ +// 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.extensions.common; + +public class UserConfigInfo { + public String anonymousCowardName; +} \ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java index 40b84a3..d18f3e5 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeAbandonedListener.java
@@ -15,14 +15,11 @@ package com.google.gerrit.extensions.events; import com.google.gerrit.extensions.annotations.ExtensionPoint; -import com.google.gerrit.extensions.common.AccountInfo; /** Notified whenever a Change is abandoned. */ @ExtensionPoint public interface ChangeAbandonedListener { interface Event extends RevisionEvent { - @Deprecated - AccountInfo getAbandoner(); String getReason(); }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java index d0ca6d6..de74a86 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeMergedListener.java
@@ -15,14 +15,11 @@ package com.google.gerrit.extensions.events; import com.google.gerrit.extensions.annotations.ExtensionPoint; -import com.google.gerrit.extensions.common.AccountInfo; /** Notified whenever a Change is merged. */ @ExtensionPoint public interface ChangeMergedListener { interface Event extends RevisionEvent { - @Deprecated - AccountInfo getMerger(); /** * Represents the merged Revision when the submit strategy is cherry-pick or * rebase-if-necessary.
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java index e5f3330..f533339 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ChangeRestoredListener.java
@@ -15,14 +15,11 @@ package com.google.gerrit.extensions.events; import com.google.gerrit.extensions.annotations.ExtensionPoint; -import com.google.gerrit.extensions.common.AccountInfo; /** Notified whenever a Change is restored. */ @ExtensionPoint public interface ChangeRestoredListener { interface Event extends RevisionEvent { - @Deprecated - AccountInfo getRestorer(); String getReason(); }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java index 6c82034..e8388a9 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/CommentAddedListener.java
@@ -15,7 +15,6 @@ package com.google.gerrit.extensions.events; import com.google.gerrit.extensions.annotations.ExtensionPoint; -import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.ApprovalInfo; import java.util.Map; @@ -24,8 +23,6 @@ @ExtensionPoint public interface CommentAddedListener { interface Event extends RevisionEvent { - @Deprecated - AccountInfo getAuthor(); String getComment(); Map<String, ApprovalInfo> getApprovals(); Map<String, ApprovalInfo> getOldApprovals();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java index 3857468..1fc574b 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/DraftPublishedListener.java
@@ -15,14 +15,11 @@ package com.google.gerrit.extensions.events; import com.google.gerrit.extensions.annotations.ExtensionPoint; -import com.google.gerrit.extensions.common.AccountInfo; /** Notified whenever a Draft is published. */ @ExtensionPoint public interface DraftPublishedListener { interface Event extends RevisionEvent { - @Deprecated - AccountInfo getPublisher(); } void onDraftPublished(Event event);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java index c49b0f3..ad13267 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/HashtagsEditedListener.java
@@ -15,7 +15,6 @@ package com.google.gerrit.extensions.events; import com.google.gerrit.extensions.annotations.ExtensionPoint; -import com.google.gerrit.extensions.common.AccountInfo; import java.util.Collection; @@ -23,8 +22,6 @@ @ExtensionPoint public interface HashtagsEditedListener { interface Event extends ChangeEvent { - @Deprecated - AccountInfo getEditor(); Collection<String> getHashtags(); Collection<String> getAddedHashtags(); Collection<String> getRemovedHashtags();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java index 3cc3fdc..bb4ac9d 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/ReviewerAddedListener.java
@@ -17,12 +17,14 @@ import com.google.gerrit.extensions.annotations.ExtensionPoint; import com.google.gerrit.extensions.common.AccountInfo; -/** Notified whenever a Reviewer is added to a change. */ +import java.util.List; + +/** Notified whenever one or more Reviewers are added to a change. */ @ExtensionPoint public interface ReviewerAddedListener { interface Event extends ChangeEvent { - AccountInfo getReviewer(); + List<AccountInfo> getReviewers(); } - void onReviewerAdded(Event event); + void onReviewersAdded(Event event); }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java index 5e4e095..8d148b7 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/RevisionCreatedListener.java
@@ -15,14 +15,11 @@ package com.google.gerrit.extensions.events; import com.google.gerrit.extensions.annotations.ExtensionPoint; -import com.google.gerrit.extensions.common.AccountInfo; /** Notified whenever a Change Revision is created. */ @ExtensionPoint public interface RevisionCreatedListener { interface Event extends RevisionEvent { - @Deprecated - AccountInfo getUploader(); } void onRevisionCreated(Event event);
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java index 68ba22c..0c36d9d 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/TopicEditedListener.java
@@ -15,14 +15,11 @@ package com.google.gerrit.extensions.events; import com.google.gerrit.extensions.annotations.ExtensionPoint; -import com.google.gerrit.extensions.common.AccountInfo; /** Notified whenever a Change Topic is changed. */ @ExtensionPoint public interface TopicEditedListener { interface Event extends ChangeEvent { - @Deprecated - AccountInfo getEditor(); String getOldTopic(); }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java index 068d9a0..4fc9ab6 100644 --- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java +++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -86,12 +86,6 @@ } /** Set the character set used to encode text data and return {@code this}. */ - @Deprecated - public BinaryResult setCharacterEncoding(String encoding) { - return setCharacterEncoding(Charset.forName(encoding)); - } - - /** Set the character set used to encode text data and return {@code this}. */ public BinaryResult setCharacterEncoding(Charset encoding) { characterEncoding = encoding; return this; @@ -235,7 +229,7 @@ StringResult(String str) { super(str.getBytes(UTF_8)); setContentType("text/plain"); - setCharacterEncoding(UTF_8.name()); + setCharacterEncoding(UTF_8); this.str = str; }
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AgreementInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AgreementInfo.java new file mode 100644 index 0000000..5fb2f48 --- /dev/null +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AgreementInfo.java
@@ -0,0 +1,27 @@ +// 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.client.info; + +import com.google.gwt.core.client.JavaScriptObject; + +public class AgreementInfo extends JavaScriptObject { + public final native String name() /*-{ return this.name; }-*/; + public final native String description() /*-{ return this.description; }-*/; + public final native String url() /*-{ return this.url; }-*/; + public final native GroupInfo autoVerifyGroup() /*-{ return this.auto_verify_group; }-*/; + + protected AgreementInfo() { + } +}
diff --git a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java index 0e3c32b..8669dd5 100644 --- a/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/AuthInfo.java
@@ -15,10 +15,10 @@ package com.google.gerrit.client.info; import com.google.gerrit.client.rpc.Natives; -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.Account.FieldName; -import com.google.gerrit.reviewdb.client.AuthType; +import com.google.gerrit.extensions.client.AccountFieldName; +import com.google.gerrit.extensions.client.AuthType; import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.JsArrayString; import java.util.ArrayList; @@ -52,22 +52,30 @@ return authType() == AuthType.CUSTOM_EXTENSION; } - public final boolean canEdit(Account.FieldName f) { + public final boolean canEdit(AccountFieldName f) { return editableAccountFields().contains(f); } - public final List<Account.FieldName> editableAccountFields() { - List<Account.FieldName> fields = new ArrayList<>(); + public final List<AccountFieldName> editableAccountFields() { + List<AccountFieldName> fields = new ArrayList<>(); for (String f : Natives.asList(_editableAccountFields())) { - fields.add(Account.FieldName.valueOf(f)); + fields.add(AccountFieldName.valueOf(f)); } return fields; } + public final List<AgreementInfo> contributorAgreements() { + List<AgreementInfo> agreements = new ArrayList<>(); + for (AgreementInfo a : Natives.asList(_contributorAgreements())) { + agreements.add(a); + } + return agreements; + } + public final boolean siteHasUsernames() { if (isCustomExtension() && httpPasswordUrl() != null - && !canEdit(FieldName.USER_NAME)) { + && !canEdit(AccountFieldName.USER_NAME)) { return false; } return true; @@ -93,6 +101,8 @@ private native String authTypeRaw() /*-{ return this.auth_type; }-*/; private native JsArrayString _editableAccountFields() /*-{ return this.editable_account_fields; }-*/; + private native JsArray<AgreementInfo> _contributorAgreements() + /*-{ return this.contributor_agreements; }-*/; protected AuthInfo() { }
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 45953cb..9e25f25 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
@@ -19,6 +19,7 @@ import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DateFormat; +import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DefaultBase; import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView; import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DownloadCommand; import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy; @@ -55,6 +56,7 @@ p.reviewCategoryStrategy(d.getReviewCategoryStrategy()); p.diffView(d.getDiffView()); p.emailStrategy(d.emailStrategy); + p.defaultBaseForMerges(d.defaultBaseForMerges); return p; } @@ -135,6 +137,14 @@ private native String emailStrategyRaw() /*-{ return this.email_strategy }-*/; + public final DefaultBase defaultBaseForMerges() { + String s = defaultBaseForMergesRaw(); + return s != null ? DefaultBase.valueOf(s) : null; + } + + private native String defaultBaseForMergesRaw() + /*-{ return this.default_base_for_merges }-*/; + public final native JsArray<TopMenuItem> my() /*-{ return this.my; }-*/; @@ -201,6 +211,12 @@ private native void emailStrategyRaw(String s) /*-{ this.email_strategy = s }-*/; + public final void defaultBaseForMerges(DefaultBase b) { + defaultBaseForMergesRaw(b != null ? b.toString() : null); + } + private native void defaultBaseForMergesRaw(String b) + /*-{ this.default_base_for_merges = b }-*/; + public final void setMyMenus(List<TopMenuItem> myMenus) { initMy(); for (TopMenuItem n : myMenus) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java similarity index 95% rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java index 4811e59..deed44d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupBaseInfo.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupBaseInfo.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.client.groups; +package com.google.gerrit.client.info; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gwt.core.client.JavaScriptObject;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java similarity index 95% rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java rename to gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java index c3fd4ed..fa051a1 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java +++ b/gerrit-gwtui-common/src/main/java/com/google/gerrit/client/info/GroupInfo.java
@@ -12,9 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.gerrit.client.groups; +package com.google.gerrit.client.info; -import com.google.gerrit.client.info.AccountInfo; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray;
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 b7405c7..539d53b 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
@@ -89,7 +89,7 @@ import com.google.gerrit.client.documentation.DocScreen; import com.google.gerrit.client.editor.EditScreen; import com.google.gerrit.client.groups.GroupApi; -import com.google.gerrit.client.groups.GroupInfo; +import com.google.gerrit.client.info.GroupInfo; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.RestApi; import com.google.gerrit.client.ui.Screen;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java index acd2e78..9aca859 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountApi.java
@@ -16,6 +16,7 @@ import com.google.gerrit.client.VoidResult; import com.google.gerrit.client.info.AccountInfo; +import com.google.gerrit.client.info.AgreementInfo; import com.google.gerrit.client.info.GpgKeyInfo; import com.google.gerrit.client.rpc.CallbackGroup; import com.google.gerrit.client.rpc.NativeMap; @@ -83,6 +84,14 @@ new RestApi("/accounts/").id(account).view("name").get(cb); } + /** Set the account name */ + public static void setName(String account, String name, + AsyncCallback<NativeString> cb) { + AccountNameInput input = AccountNameInput.create(); + input.name(name); + new RestApi("/accounts/").id(account).view("name").put(input, cb); + } + /** Retrieve email addresses */ public static void getEmails(String account, AsyncCallback<JsArray<EmailInfo>> cb) { @@ -97,6 +106,13 @@ .ifNoneMatch().put(in, cb); } + /** Set preferred email address */ + public static void setPreferredEmail(String account, String email, + AsyncCallback<NativeString> cb) { + new RestApi("/accounts/").id(account).view("emails") + .id(email).view("preferred").put(cb); + } + /** Retrieve SSH keys */ public static void getSshKeys(String account, AsyncCallback<JsArray<SshKeyInfo>> cb) { @@ -196,6 +212,14 @@ new RestApi("/accounts/").id(account).view("password.http").delete(cb); } + /** Enter a contributor agreement */ + public static void enterAgreement(String account, String name, + AsyncCallback<NativeString> cb) { + AgreementInput in = AgreementInput.create(); + in.name(name); + new RestApi("/accounts/").id(account).view("agreements").put(in, cb); + } + private static JsArray<ProjectWatchInfo> projectWatchArrayFromSet( Set<ProjectWatchInfo> set) { JsArray<ProjectWatchInfo> jsArray = JsArray.createArray().cast(); @@ -205,6 +229,17 @@ return jsArray; } + private static class AgreementInput extends JavaScriptObject { + final native void name(String n) /*-{ if(n)this.name=n; }-*/; + + static AgreementInput create() { + return createObject().cast(); + } + + protected AgreementInput() { + } + } + private static class HttpPasswordInput extends JavaScriptObject { final native void generate(boolean g) /*-{ if(g)this.generate=g; }-*/; @@ -227,6 +262,17 @@ } } + private static class AccountNameInput extends JavaScriptObject { + final native void name(String n) /*-{ if(n)this.name=n; }-*/; + + static AccountNameInput create() { + return createObject().cast(); + } + + protected AccountNameInput() { + } + } + public static void addGpgKey(String account, String armored, AsyncCallback<NativeMap<GpgKeyInfo>> cb) { new RestApi("/accounts/") @@ -243,6 +289,12 @@ .post(GpgKeysInput.delete(fingerprints), cb); } + /** List contributor agreements */ + public static void getAgreements(String account, + AsyncCallback<JsArray<AgreementInfo>> cb) { + new RestApi("/accounts/").id(account).view("agreements").get(cb); + } + private static class GpgKeysInput extends JavaScriptObject { static GpgKeysInput add(String key) { return createWithAdd(Natives.arrayOf(key));
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 a084612..c58d6b5 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
@@ -141,11 +141,8 @@ String errorDialogTitleRegisterNewEmail(); String newAgreement(); - String agreementStatus(); String agreementName(); String agreementDescription(); - String agreementStatus_EXPIRED(); - String agreementStatus_VERIFIED(); String newAgreementSelectTypeHeading(); String newAgreementNoneAvailable(); @@ -171,4 +168,8 @@ String messageCCMeOnMyComments(); String messageDisabled(); String emailFieldLabel(); + + String defaultBaseForMerges(); + String autoMerge(); + String firstParent(); }
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 ca2d316..cc15aae 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
@@ -19,6 +19,10 @@ messageCCMeOnMyComments = CC Me On Comments I Write messageDisabled = Disabled +defaultBaseForMerges = Default Base For Merges: +autoMerge = Auto Merge +firstParent = First Parent + maximumPageSizeFieldLabel = Maximum Page Size: diffViewLabel = Diff View: dateFormatLabel = Date/Time Format: @@ -151,10 +155,7 @@ newAgreement = New Contributor Agreement -agreementStatus = Status agreementName = Name -agreementStatus_EXPIRED = Expired -agreementStatus_VERIFIED = Verified agreementDescription = Description newAgreementSelectTypeHeading = Select an agreement type:
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 ae3599d..f5f38fb 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
@@ -15,7 +15,6 @@ package com.google.gerrit.client.account; import com.google.gerrit.client.ErrorDialog; -import com.google.gerrit.client.FormatUtil; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.info.AccountInfo; import com.google.gerrit.client.rpc.CallbackGroup; @@ -25,8 +24,7 @@ import com.google.gerrit.client.ui.OnEditEnabler; import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.errors.EmailException; -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.Account.FieldName; +import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gwt.core.client.JsArray; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; @@ -46,7 +44,6 @@ import com.google.gwt.user.client.ui.Widget; import com.google.gwtexpui.globalkey.client.NpTextBox; import com.google.gwtexpui.user.client.AutoCenterDialogBox; -import com.google.gwtjsonrpc.common.AsyncCallback; class ContactPanelShort extends Composite { protected final FlowPanel body; @@ -104,7 +101,7 @@ } int row = 0; - if (!Gerrit.info().auth().canEdit(FieldName.USER_NAME) + if (!Gerrit.info().auth().canEdit(AccountFieldName.USER_NAME) && Gerrit.info().auth().siteHasUsernames()) { infoPlainText.resizeRows(infoPlainText.getRowCount() + 1); row(infoPlainText, row++, Util.C.userName(), new UsernameField()); @@ -146,7 +143,7 @@ save.addClickHandler(new ClickHandler() { @Override public void onClick(final ClickEvent event) { - doSave(null); + doSave(); } }); @@ -173,11 +170,11 @@ } private boolean canEditFullName() { - return Gerrit.info().auth().canEdit(Account.FieldName.FULL_NAME); + return Gerrit.info().auth().canEdit(AccountFieldName.FULL_NAME); } private boolean canRegisterNewEmail() { - return Gerrit.info().auth().canEdit(Account.FieldName.REGISTER_NEW_EMAIL); + return Gerrit.info().auth().canEdit(AccountFieldName.REGISTER_NEW_EMAIL); } void hideSaveButton() { @@ -347,10 +344,13 @@ inEmail.setFocus(true); } - void doSave(final AsyncCallback<Account> onSave) { - String newName = canEditFullName() ? nameTxt.getText() : null; - if (newName != null && newName.trim().isEmpty()) { + void doSave() { + final String newName; + String name = canEditFullName() ? nameTxt.getText() : null; + if (name != null && name.trim().isEmpty()) { newName = null; + } else { + newName = name; } final String newEmail; @@ -368,24 +368,40 @@ save.setEnabled(false); registerNewEmail.setEnabled(false); - Util.ACCOUNT_SEC.updateContact(newName, newEmail, - new GerritCallback<Account>() { - @Override - public void onSuccess(Account result) { - registerNewEmail.setEnabled(true); - onSaveSuccess(FormatUtil.asInfo(result)); - if (onSave != null) { - onSave.onSuccess(result); - } - } + CallbackGroup group = new CallbackGroup(); + if (!newEmail.equals(currentEmail)) { + AccountApi.setPreferredEmail("self", newEmail, + group.add(new GerritCallback<NativeString>() { + @Override + public void onSuccess(NativeString result) { + } + })); + } + AccountApi.setName("self", newName, + group.add(new GerritCallback<NativeString>() { + @Override + public void onSuccess(NativeString result) { + } - @Override - public void onFailure(final Throwable caught) { - save.setEnabled(true); - registerNewEmail.setEnabled(true); - super.onFailure(caught); - } - }); + @Override + public void onFailure(Throwable caught) { + save.setEnabled(true); + registerNewEmail.setEnabled(true); + super.onFailure(caught); + } + })); + group.done(); + group.addListener(new GerritCallback<Void>() { + @Override + public void onSuccess(Void result) { + currentEmail = newEmail; + AccountInfo me = Gerrit.getUserAccount(); + me.email(currentEmail); + me.name(newName); + onSaveSuccess(me); + registerNewEmail.setEnabled(true); + } + }); } void onSaveSuccess(AccountInfo result) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java index 308cf30..cd7c141 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
@@ -15,15 +15,19 @@ package com.google.gerrit.client.account; import com.google.gerrit.client.Gerrit; +import com.google.gerrit.client.info.AgreementInfo; +import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.rpc.ScreenLoadCallback; import com.google.gerrit.client.ui.FancyFlexTable; import com.google.gerrit.client.ui.Hyperlink; import com.google.gerrit.common.PageLinks; -import com.google.gerrit.common.data.AgreementInfo; import com.google.gerrit.common.data.ContributorAgreement; +import com.google.gwt.core.client.JsArray; import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter; +import java.util.List; + public class MyAgreementsScreen extends SettingsScreen { private AgreementTable agreements; @@ -39,10 +43,11 @@ @Override protected void onLoad() { super.onLoad(); - Util.ACCOUNT_SVC.myAgreements(new ScreenLoadCallback<AgreementInfo>(this) { + AccountApi.getAgreements( + "self", new ScreenLoadCallback<JsArray<AgreementInfo>>(this) { @Override - public void preDisplay(final AgreementInfo result) { - agreements.display(result); + public void preDisplay(JsArray<AgreementInfo> result) { + agreements.display(Natives.asList(result)); } }); } @@ -50,60 +55,43 @@ private static class AgreementTable extends FancyFlexTable<ContributorAgreement> { AgreementTable() { table.setWidth(""); - table.setText(0, 1, Util.C.agreementStatus()); - table.setText(0, 2, Util.C.agreementName()); - table.setText(0, 3, Util.C.agreementDescription()); + table.setText(0, 1, Util.C.agreementName()); + table.setText(0, 2, Util.C.agreementDescription()); - final FlexCellFormatter fmt = table.getFlexCellFormatter(); - for (int c = 1; c < 4; c++) { + FlexCellFormatter fmt = table.getFlexCellFormatter(); + for (int c = 1; c < 3; c++) { fmt.addStyleName(0, c, Gerrit.RESOURCES.css().dataHeader()); } } - void display(final AgreementInfo result) { + void display(List<AgreementInfo> result) { while (1 < table.getRowCount()) { table.removeRow(table.getRowCount() - 1); } - for (final String k : result.accepted) { - addOne(result, k); + for (AgreementInfo info : result) { + addOne(info); } } - void addOne(final AgreementInfo info, final String k) { - final int row = table.getRowCount(); + void addOne(AgreementInfo info) { + int row = table.getRowCount(); table.insertRow(row); applyDataRowStyle(row); - final ContributorAgreement cla = info.agreements.get(k); - final String statusName; - if (cla == null) { - statusName = Util.C.agreementStatus_EXPIRED(); + String url = info.url(); + if (url != null && url.length() > 0) { + Anchor a = new Anchor(info.name(), url); + a.setTarget("_blank"); + table.setWidget(row, 1, a); } else { - statusName = Util.C.agreementStatus_VERIFIED(); + table.setText(row, 1, info.name()); } - table.setText(row, 1, statusName); - - if (cla == null) { - table.setText(row, 2, ""); - table.setText(row, 3, ""); - } else { - final String url = cla.getAgreementUrl(); - if (url != null && url.length() > 0) { - final Anchor a = new Anchor(cla.getName(), url); - a.setTarget("_blank"); - table.setWidget(row, 2, a); - } else { - table.setText(row, 2, cla.getName()); - } - table.setText(row, 3, cla.getDescription()); - } - final FlexCellFormatter fmt = table.getFlexCellFormatter(); - for (int c = 1; c < 4; c++) { + table.setText(row, 2, info.description()); + FlexCellFormatter fmt = table.getFlexCellFormatter(); + for (int c = 1; c < 3; c++) { fmt.addStyleName(row, c, Gerrit.RESOURCES.css().dataCell()); } - - setRowItem(row, cla); } } }
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 2b01b59..9433b04 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
@@ -61,6 +61,7 @@ private ListBox reviewCategoryStrategy; private ListBox diffView; private ListBox emailStrategy; + private ListBox defaultBaseForMerges; private StringListPanel myMenus; private Button save; @@ -106,6 +107,12 @@ GeneralPreferencesInfo.EmailStrategy.DISABLED .name()); + defaultBaseForMerges = new ListBox(); + defaultBaseForMerges.addItem(Util.C.autoMerge(), + GeneralPreferencesInfo.DefaultBase.AUTO_MERGE.name()); + defaultBaseForMerges.addItem(Util.C.firstParent(), + GeneralPreferencesInfo.DefaultBase.FIRST_PARENT.name()); + diffView = new ListBox(); diffView.addItem( com.google.gerrit.client.changes.Util.C.sideBySide(), @@ -156,7 +163,7 @@ signedOffBy = new CheckBox(Util.C.signedOffBy()); boolean flashClippy = !UserAgent.hasJavaScriptClipboard() && UserAgent.Flash.isInstalled(); - final Grid formGrid = new Grid(12 + (flashClippy ? 1 : 0), 2); + final Grid formGrid = new Grid(13 + (flashClippy ? 1 : 0), 2); int row = 0; @@ -176,6 +183,10 @@ formGrid.setWidget(row, fieldIdx, emailStrategy); row++; + formGrid.setText(row, labelIdx, Util.C.defaultBaseForMerges()); + formGrid.setWidget(row, fieldIdx, defaultBaseForMerges); + row++; + formGrid.setText(row, labelIdx, Util.C.diffViewLabel()); formGrid.setWidget(row, fieldIdx, diffView); row++; @@ -239,6 +250,7 @@ e.listenTo(diffView); e.listenTo(reviewCategoryStrategy); e.listenTo(emailStrategy); + e.listenTo(defaultBaseForMerges); } @Override @@ -272,6 +284,7 @@ reviewCategoryStrategy.setEnabled(on); diffView.setEnabled(on); emailStrategy.setEnabled(on); + defaultBaseForMerges.setEnabled(on); } private void display(GeneralPreferences p) { @@ -296,6 +309,9 @@ setListBox(emailStrategy, GeneralPreferencesInfo.EmailStrategy.ENABLED, p.emailStrategy()); + setListBox(defaultBaseForMerges, + GeneralPreferencesInfo.DefaultBase.FIRST_PARENT, + p.defaultBaseForMerges()); display(p.my()); } @@ -385,6 +401,10 @@ GeneralPreferencesInfo.EmailStrategy.ENABLED, GeneralPreferencesInfo.EmailStrategy.values())); + p.defaultBaseForMerges(getListBox(defaultBaseForMerges, + GeneralPreferencesInfo.DefaultBase.FIRST_PARENT, + GeneralPreferencesInfo.DefaultBase.values())); + List<TopMenuItem> items = new ArrayList<>(); for (List<String> v : myMenus.getValues()) { items.add(TopMenuItem.create(v.get(0), v.get(1)));
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 14f8e2f..e7fa14c5 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
@@ -16,14 +16,16 @@ import com.google.gerrit.client.ErrorDialog; import com.google.gerrit.client.Gerrit; +import com.google.gerrit.client.info.AgreementInfo; import com.google.gerrit.client.rpc.GerritCallback; +import com.google.gerrit.client.rpc.NativeString; +import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.ui.AccountScreen; import com.google.gerrit.client.ui.OnEditEnabler; import com.google.gerrit.client.ui.SmallHeading; import com.google.gerrit.common.PageLinks; -import com.google.gerrit.common.data.AgreementInfo; -import com.google.gerrit.common.data.ContributorAgreement; import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.JsArray; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.http.client.Request; @@ -41,7 +43,6 @@ import com.google.gwt.user.client.ui.RadioButton; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwtexpui.globalkey.client.NpTextBox; -import com.google.gwtjsonrpc.common.VoidResult; import java.util.HashSet; import java.util.List; @@ -50,8 +51,8 @@ public class NewAgreementScreen extends AccountScreen { private final String nextToken; private Set<String> mySigned; - private List<ContributorAgreement> available; - private ContributorAgreement current; + private List<AgreementInfo> available; + private AgreementInfo current; private VerticalPanel radios; @@ -73,25 +74,22 @@ @Override protected void onLoad() { super.onLoad(); - Util.ACCOUNT_SVC.myAgreements(new GerritCallback<AgreementInfo>() { + AccountApi.getAgreements( + "self", new GerritCallback<JsArray<AgreementInfo>>() { @Override - public void onSuccess(AgreementInfo result) { + public void onSuccess(JsArray<AgreementInfo> result) { if (isAttached()) { - mySigned = new HashSet<>(result.accepted); + mySigned = new HashSet<>(); + for (AgreementInfo info: Natives.asList(result)) { + mySigned.add(info.name()); + } postRPC(); } } }); - Gerrit.SYSTEM_SVC - .contributorAgreements(new GerritCallback<List<ContributorAgreement>>() { - @Override - public void onSuccess(final List<ContributorAgreement> result) { - if (isAttached()) { - available = result; - postRPC(); - } - } - }); + + available = Gerrit.info().auth().contributorAgreements(); + postRPC(); } @Override @@ -158,12 +156,12 @@ } radios.add(hdr); - for (final ContributorAgreement cla : available) { - final RadioButton r = new RadioButton("cla_id", cla.getName()); + for (final AgreementInfo cla : available) { + final RadioButton r = new RadioButton("cla_id", cla.name()); r.addStyleName(Gerrit.RESOURCES.css().contributorAgreementButton()); radios.add(r); - if (mySigned.contains(cla.getName())) { + if (mySigned.contains(cla.name())) { r.setEnabled(false); final Label l = new Label(Util.C.newAgreementAlreadySubmitted()); l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementAlreadySubmitted()); @@ -177,8 +175,8 @@ }); } - if (cla.getDescription() != null && !cla.getDescription().equals("")) { - final Label l = new Label(cla.getDescription()); + if (cla.description() != null && !cla.description().equals("")) { + final Label l = new Label(cla.description()); l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementShortDescription()); radios.add(l); } @@ -199,24 +197,24 @@ } private void doEnterAgreement() { - Util.ACCOUNT_SEC.enterAgreement(current.getName(), - new GerritCallback<VoidResult>() { + AccountApi.enterAgreement("self", current.name(), + new GerritCallback<NativeString>() { @Override - public void onSuccess(final VoidResult result) { + public void onSuccess(NativeString result) { Gerrit.display(nextToken); } @Override - public void onFailure(final Throwable caught) { + public void onFailure(Throwable caught) { yesIAgreeBox.setText(""); super.onFailure(caught); } }); } - private void showCLA(final ContributorAgreement cla) { + private void showCLA(AgreementInfo cla) { current = cla; - String url = cla.getAgreementUrl(); + String url = cla.url(); if (url != null && url.length() > 0) { agreementGroup.setVisible(true); agreementHtml.setText(Gerrit.C.rpcStatusWorking()); @@ -250,7 +248,7 @@ agreementGroup.setVisible(false); } - finalGroup.setVisible(cla.getAutoVerify() != null); + finalGroup.setVisible(cla.autoVerifyGroup() != null); yesIAgreeBox.setText(""); submit.setEnabled(false); }
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 c32a846..73557aa 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
@@ -20,7 +20,7 @@ import com.google.gerrit.client.ui.InlineHyperlink; import com.google.gerrit.client.ui.SmallHeading; import com.google.gerrit.common.PageLinks; -import com.google.gerrit.reviewdb.client.Account.FieldName; +import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gwt.i18n.client.LocaleInfo; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.FormPanel; @@ -70,7 +70,7 @@ formBody.add(contactGroup); if (Gerrit.getUserAccount().username() == null - && Gerrit.info().auth().canEdit(FieldName.USER_NAME)) { + && Gerrit.info().auth().canEdit(AccountFieldName.USER_NAME)) { final FlowPanel fp = new FlowPanel(); fp.setStyleName(Gerrit.RESOURCES.css().registerScreenSection()); fp.add(new SmallHeading(Util.C.welcomeUsernameHeading()));
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 f388436..d70121b 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
@@ -22,6 +22,7 @@ import com.google.gerrit.client.rpc.NativeString; import com.google.gerrit.client.rpc.RestApi; import com.google.gerrit.client.ui.OnEditEnabler; +import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gerrit.reviewdb.client.Account; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; @@ -86,7 +87,7 @@ } private boolean canEditUserName() { - return Gerrit.info().auth().canEdit(Account.FieldName.USER_NAME); + return Gerrit.info().auth().canEdit(AccountFieldName.USER_NAME); } private void confirmSetUserName() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java index a0f36b9..b4b4390 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/Util.java
@@ -15,7 +15,6 @@ package com.google.gerrit.client.account; import com.google.gerrit.common.data.AccountSecurity; -import com.google.gerrit.common.data.AccountService; import com.google.gerrit.common.data.ProjectAdminService; import com.google.gwt.core.client.GWT; import com.google.gwtjsonrpc.client.JsonUtil; @@ -23,14 +22,10 @@ public class Util { public static final AccountConstants C = GWT.create(AccountConstants.class); public static final AccountMessages M = GWT.create(AccountMessages.class); - public static final AccountService ACCOUNT_SVC; public static final AccountSecurity ACCOUNT_SEC; public static final ProjectAdminService PROJECT_SVC; static { - ACCOUNT_SVC = GWT.create(AccountService.class); - JsonUtil.bind(ACCOUNT_SVC, "rpc/AccountService"); - ACCOUNT_SEC = GWT.create(AccountSecurity.class); JsonUtil.bind(ACCOUNT_SEC, "rpc/AccountSecurity");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java index 254d3e6..7a32f01 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupAuditLogScreen.java
@@ -21,8 +21,8 @@ import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.groups.GroupApi; import com.google.gerrit.client.groups.GroupAuditEventInfo; -import com.google.gerrit.client.groups.GroupInfo; import com.google.gerrit.client.info.AccountInfo; +import com.google.gerrit.client.info.GroupInfo; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.ui.FancyFlexTable;
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 a71dffe..22a57a4 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
@@ -17,7 +17,7 @@ import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.VoidResult; import com.google.gerrit.client.groups.GroupApi; -import com.google.gerrit.client.groups.GroupInfo; +import com.google.gerrit.client.info.GroupInfo; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.ui.AccountGroupSuggestOracle; import com.google.gerrit.client.ui.OnEditEnabler;
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 7c0c8f6..053e7e0c 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
@@ -18,8 +18,8 @@ import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.VoidResult; import com.google.gerrit.client.groups.GroupApi; -import com.google.gerrit.client.groups.GroupInfo; import com.google.gerrit.client.info.AccountInfo; +import com.google.gerrit.client.info.GroupInfo; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
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 8c00ba7..cbe8a06 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
@@ -17,7 +17,7 @@ import static com.google.gerrit.client.Dispatcher.toGroup; import com.google.gerrit.client.groups.GroupApi; -import com.google.gerrit.client.groups.GroupInfo; +import com.google.gerrit.client.info.GroupInfo; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.ui.MenuScreen; import com.google.gerrit.reviewdb.client.AccountGroup;
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 2fe5978..3290aac 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
@@ -123,6 +123,9 @@ abandon, \ addPatchSet, \ create, \ + createTag, \ + createSignedTag, \ + delete, \ deleteDrafts, \ editHashtags, \ editTopicName, \ @@ -133,8 +136,6 @@ publishDrafts, \ push, \ pushMerge, \ - pushTag, \ - pushSignedTag, \ read, \ rebase, \ removeReviewer, \ @@ -145,6 +146,9 @@ abandon = Abandon addPatchSet = Add Patch Set create = Create Reference +createTag = Create Annotated Tag +createSignedTag = Create Signed Tag +delete = Delete Reference deleteDrafts = Delete Drafts editHashtags = Edit Hashtags editTopicName = Edit Topic Name @@ -155,8 +159,6 @@ publishDrafts = Publish Drafts push = Push pushMerge = Push Merge Commit -pushTag = Push Annotated Tag -pushSignedTag = Push Signed Tag read = Read rebase = Rebase removeReviewer = Remove Reviewer
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 a2ba5cd..4efaa61 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
@@ -21,7 +21,7 @@ import com.google.gerrit.client.NotFoundScreen; import com.google.gerrit.client.account.AccountCapabilities; import com.google.gerrit.client.groups.GroupApi; -import com.google.gerrit.client.groups.GroupInfo; +import com.google.gerrit.client.info.GroupInfo; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.ui.OnEditEnabler; import com.google.gerrit.client.ui.Screen;
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 64fc0e5..94d15bd 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
@@ -18,9 +18,9 @@ import com.google.gerrit.client.Dispatcher; import com.google.gerrit.client.Gerrit; -import com.google.gerrit.client.groups.GroupInfo; import com.google.gerrit.client.groups.GroupList; import com.google.gerrit.client.groups.GroupMap; +import com.google.gerrit.client.info.GroupInfo; import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.ui.HighlightingInlineHyperlink; import com.google.gerrit.client.ui.NavigationTable;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java index f66307c..be5bdcb 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
@@ -16,7 +16,6 @@ import static com.google.gerrit.common.data.Permission.EDIT_TOPIC_NAME; import static com.google.gerrit.common.data.Permission.PUSH; -import static com.google.gerrit.common.data.Permission.PUSH_TAG; import com.google.gerrit.client.Dispatcher; import com.google.gerrit.client.Gerrit; @@ -143,7 +142,7 @@ initWidget(uiBinder.createAndBindUi(this)); String name = permission.getName(); - boolean canForce = PUSH.equals(name) || PUSH_TAG.equals(name); + boolean canForce = PUSH.equals(name); if (canForce) { String ref = section.getName(); canForce = !ref.startsWith("refs/for/") && !ref.startsWith("^refs/for/");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java index 62c3636..2a8dacf 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.java
@@ -30,4 +30,5 @@ String submittedTogether(int count); String submittedTogether(String count); String editPatchSet(int patchSet); + String failedToLoadFileList(String error); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties index 6461899..743945d 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ChangeMessages.properties
@@ -6,3 +6,4 @@ sameTopic = Same Topic ({0}) submittedTogether = Submitted Together ({0}) editPatchSet = edit:{0} +failedToLoadFileList = Failed to load file list: {0}
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 b2e9f28..9a4a049 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
@@ -107,8 +107,13 @@ import java.util.Collections; import java.util.EnumSet; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; public class ChangeScreen extends Screen { + private static final Logger logger = + Logger.getLogger(ChangeScreen.class.getName()); + interface Binder extends UiBinder<HTMLPanel, ChangeScreen> {} private static final Binder uiBinder = GWT.create(Binder.class); @@ -282,13 +287,17 @@ info.init(); addExtensionPoints(info, initCurrentRevision(info)); - RevisionInfo rev = info.revision(revision); + final RevisionInfo rev = info.revision(revision); CallbackGroup group = new CallbackGroup(); loadCommit(rev, group); group.addListener(new GerritCallback<Void>() { @Override public void onSuccess(Void result) { + if (base == null && rev.commit().parents().length() > 1) { + base = Gerrit.getUserPreferences() + .defaultBaseForMerges().getBase(); + } loadConfigInfo(info, base); } }); @@ -569,35 +578,40 @@ } private void initEditMode(ChangeInfo info, String revision) { - if (Gerrit.isSignedIn() && info.status().isOpen()) { + if (Gerrit.isSignedIn()) { RevisionInfo rev = info.revision(revision); - if (isEditModeEnabled(info, rev)) { - editMode.setVisible(fileTableMode == FileTable.Mode.REVIEW); - addFile.setVisible(!editMode.isVisible()); - deleteFile.setVisible(!editMode.isVisible()); - renameFile.setVisible(!editMode.isVisible()); - reviewMode.setVisible(!editMode.isVisible()); - addFileAction = new AddFileAction( - changeId, info.revision(revision), - style, addFile, files); - deleteFileAction = new DeleteFileAction( - changeId, info.revision(revision), - style, addFile); - renameFileAction = new RenameFileAction( - changeId, info.revision(revision), - style, addFile); - } else { - editMode.setVisible(false); - addFile.setVisible(false); - reviewMode.setVisible(false); - } - - if (rev.isEdit()) { - if (info.hasEditBasedOnCurrentPatchSet()) { - publishEdit.setVisible(true); + if (info.status().isOpen()) { + if (isEditModeEnabled(info, rev)) { + editMode.setVisible(fileTableMode == FileTable.Mode.REVIEW); + addFile.setVisible(!editMode.isVisible()); + deleteFile.setVisible(!editMode.isVisible()); + renameFile.setVisible(!editMode.isVisible()); + reviewMode.setVisible(!editMode.isVisible()); + addFileAction = new AddFileAction( + changeId, info.revision(revision), + style, addFile, files); + deleteFileAction = new DeleteFileAction( + changeId, info.revision(revision), + style, addFile); + renameFileAction = new RenameFileAction( + changeId, info.revision(revision), + style, addFile); } else { - rebaseEdit.setVisible(true); + editMode.setVisible(false); + addFile.setVisible(false); + reviewMode.setVisible(false); } + + if (rev.isEdit()) { + if (info.hasEditBasedOnCurrentPatchSet()) { + publishEdit.setVisible(true); + } else { + rebaseEdit.setVisible(true); + } + deleteEdit.setVisible(true); + } + } else if (rev.isEdit()) { + deleteEdit.setStyleName(style.highlight()); deleteEdit.setVisible(true); } } @@ -616,37 +630,39 @@ @UiHandler("publishEdit") void onPublishEdit(@SuppressWarnings("unused") ClickEvent e) { - EditActions.publishEdit(changeId); + EditActions.publishEdit(changeId, publishEdit, rebaseEdit, deleteEdit); } @UiHandler("rebaseEdit") void onRebaseEdit(@SuppressWarnings("unused") ClickEvent e) { - EditActions.rebaseEdit(changeId); + EditActions.rebaseEdit(changeId, publishEdit, rebaseEdit, deleteEdit); } @UiHandler("deleteEdit") void onDeleteEdit(@SuppressWarnings("unused") ClickEvent e) { if (Window.confirm(Resources.C.deleteChangeEdit())) { - EditActions.deleteEdit(changeId); + EditActions.deleteEdit(changeId, publishEdit, rebaseEdit, deleteEdit); } } @UiHandler("publish") void onPublish(@SuppressWarnings("unused") ClickEvent e) { - DraftActions.publish(changeId, revision); + DraftActions.publish(changeId, revision, publish, deleteRevision, + deleteChange); } @UiHandler("deleteRevision") void onDeleteRevision(@SuppressWarnings("unused") ClickEvent e) { if (Window.confirm(Resources.C.deleteDraftRevision())) { - DraftActions.delete(changeId, revision); + DraftActions.delete(changeId, revision, publish, deleteRevision, + deleteChange); } } @UiHandler("deleteChange") void onDeleteChange(@SuppressWarnings("unused") ClickEvent e) { if (Window.confirm(Resources.C.deleteDraftChange())) { - DraftActions.delete(changeId); + DraftActions.delete(changeId, publish, deleteRevision, deleteChange); } } @@ -918,7 +934,7 @@ } private void loadConfigInfo(final ChangeInfo info, String base) { - RevisionInfo rev = info.revision(revision); + final RevisionInfo rev = info.revision(revision); RevisionInfo b = resolveRevisionOrPatchSetId(info, base, null); CallbackGroup group = new CallbackGroup(); @@ -933,16 +949,32 @@ } else { loadDiff(b, rev, lastReply, group); } + group.addListener(new AsyncCallback<Void>() { + @Override + public void onSuccess(Void result) { + loadConfigInfo(info, rev); + } + @Override + public void onFailure(Throwable caught) { + logger.log(Level.SEVERE, + "Loading file list and inline comments failed: " + + caught.getMessage()); + loadConfigInfo(info, rev); + } + }); + group.done(); + } + + private void loadConfigInfo(final ChangeInfo info, RevisionInfo rev) { if (loaded) { - group.done(); return; } RevisionInfoCache.add(changeId, rev); ConfigInfoCache.add(info); ConfigInfoCache.get(info.projectNameKey(), - group.addFinal(new ScreenLoadCallback<ConfigInfoCache.Entry>(this) { + new ScreenLoadCallback<ConfigInfoCache.Entry>(this) { @Override protected void preDisplay(Entry result) { loaded = true; @@ -951,7 +983,7 @@ renderChangeInfo(info); loadRevisionInfo(); } - })); + }); } static Timestamp myLastReply(ChangeInfo info) { @@ -1012,6 +1044,7 @@ @Override public void onFailure(Throwable caught) { + files.showError(caught); } })); }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java index 634190a2..6787576 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/DraftActions.java
@@ -21,23 +21,25 @@ import com.google.gerrit.reviewdb.client.Change; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; public class DraftActions { - static void publish(Change.Id id, String revision) { - ChangeApi.publish(id.get(), revision, cs(id)); + static void publish(Change.Id id, String revision, Button... draftButtons) { + ChangeApi.publish(id.get(), revision, cs(id, draftButtons)); } - static void delete(Change.Id id, String revision) { - ChangeApi.deleteRevision(id.get(), revision, cs(id)); + static void delete(Change.Id id, String revision, Button... draftButtons) { + ChangeApi.deleteRevision(id.get(), revision, cs(id, draftButtons)); } - static void delete(Change.Id id) { - ChangeApi.deleteChange(id.get(), mine()); + static void delete(Change.Id id, Button... draftButtons) { + ChangeApi.deleteChange(id.get(), mine(draftButtons)); } public static GerritCallback<JavaScriptObject> cs( - final Change.Id id) { + final Change.Id id, final Button... draftButtons) { + setEnabled(false, draftButtons); return new GerritCallback<JavaScriptObject>() { @Override public void onSuccess(JavaScriptObject result) { @@ -46,6 +48,7 @@ @Override public void onFailure(Throwable err) { + setEnabled(true, draftButtons); if (SubmitFailureDialog.isConflict(err)) { new SubmitFailureDialog(err.getMessage()).center(); Gerrit.display(PageLinks.toChange(id)); @@ -56,7 +59,9 @@ }; } - private static AsyncCallback<JavaScriptObject> mine() { + private static AsyncCallback<JavaScriptObject> mine( + final Button... draftButtons) { + setEnabled(false, draftButtons); return new GerritCallback<JavaScriptObject>() { @Override public void onSuccess(JavaScriptObject result) { @@ -65,6 +70,7 @@ @Override public void onFailure(Throwable err) { + setEnabled(true, draftButtons); if (SubmitFailureDialog.isConflict(err)) { new SubmitFailureDialog(err.getMessage()).center(); Gerrit.display(PageLinks.MINE); @@ -74,4 +80,12 @@ } }; } + + private static void setEnabled(boolean enabled, Button... draftButtons) { + if (draftButtons != null) { + for (Button b : draftButtons) { + b.setEnabled(enabled); + } + } + } }
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 d11cf7e..97abddb 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
@@ -20,23 +20,25 @@ import com.google.gerrit.common.PageLinks; import com.google.gerrit.reviewdb.client.Change; import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.user.client.ui.Button; public class EditActions { - static void deleteEdit(Change.Id id) { - ChangeApi.deleteEdit(id.get(), cs(id)); + static void deleteEdit(Change.Id id, Button... editButtons) { + ChangeApi.deleteEdit(id.get(), cs(id, editButtons)); } - static void publishEdit(Change.Id id) { - ChangeApi.publishEdit(id.get(), cs(id)); + static void publishEdit(Change.Id id, Button... editButtons) { + ChangeApi.publishEdit(id.get(), cs(id, editButtons)); } - static void rebaseEdit(Change.Id id) { - ChangeApi.rebaseEdit(id.get(), cs(id)); + static void rebaseEdit(Change.Id id, Button... editButtons) { + ChangeApi.rebaseEdit(id.get(), cs(id, editButtons)); } public static GerritCallback<JavaScriptObject> cs( - final Change.Id id) { + final Change.Id id, final Button... editButtons) { + setEnabled(false, editButtons); return new GerritCallback<JavaScriptObject>() { @Override public void onSuccess(JavaScriptObject result) { @@ -45,6 +47,7 @@ @Override public void onFailure(Throwable err) { + setEnabled(true, editButtons); if (SubmitFailureDialog.isConflict(err)) { new SubmitFailureDialog(err.getMessage()).center(); Gerrit.display(PageLinks.toChange(id)); @@ -54,4 +57,12 @@ } }; } + + private static void setEnabled(boolean enabled, Button... editButtons) { + if (editButtons != null) { + for (Button b : editButtons) { + b.setEnabled(enabled); + } + } + } }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java index f0a7ce3..c0879e7 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -60,6 +60,7 @@ import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; import com.google.gwt.user.client.ui.ImageResourceRenderer; +import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.Widget; import com.google.gwt.user.client.ui.impl.HyperlinkImpl; import com.google.gwtexpui.globalkey.client.KeyCommand; @@ -94,6 +95,7 @@ String inserted(); String deleted(); String restoreDelete(); + String error(); } public enum Mode { @@ -222,6 +224,13 @@ } } + void showError(Throwable t) { + clear(); + Label l = new Label(Resources.M.failedToLoadFileList(t.getMessage())); + add(l); + l.setStyleName(R.css().error()); + } + void markReviewed(JsArrayString reviewed) { if (table != null) { table.markReviewed(reviewed);
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 cc5c9b7..2d868da 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
@@ -215,10 +215,11 @@ EnumSet.of(ListChangesOption.CURRENT_REVISION, ListChangesOption.CURRENT_COMMIT), new TabChangeListCallback(Tab.CHERRY_PICKS, info.project(), revision)); - // TODO(sbeller): show only on latest revision - ChangeApi.change(info.legacyId().get()).view("submitted_together") - .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER, - info.project(), revision)); + if (info.currentRevision().equals(revision)) { + ChangeApi.change(info.legacyId().get()).view("submitted_together") + .get(new TabChangeListCallback(Tab.SUBMITTED_TOGETHER, + info.project(), revision)); + } if (!Gerrit.info().change().isSubmitWholeTopicEnabled() && info.topic() != null && !"".equals(info.topic())) {
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 a852fa0..2188c03 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
@@ -16,8 +16,8 @@ import com.google.gerrit.client.admin.Util; import com.google.gerrit.client.changes.ChangeApi; -import com.google.gerrit.client.groups.GroupBaseInfo; import com.google.gerrit.client.info.AccountInfo; +import com.google.gerrit.client.info.GroupBaseInfo; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.ui.AccountSuggestOracle;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css index bde9755..6f514df 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
@@ -109,3 +109,7 @@ white-space: nowrap; } +.error { + color: #D33D3D; + font-weight: bold; +}
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 93be87b..760f06d 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
@@ -16,6 +16,7 @@ import com.google.gerrit.client.VoidResult; import com.google.gerrit.client.info.AccountInfo; +import com.google.gerrit.client.info.GroupInfo; import com.google.gerrit.client.rpc.NativeString; import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.rpc.RestApi;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java index ed41b65..5bcdc6b 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupAuditEventInfo.java
@@ -15,6 +15,7 @@ package com.google.gerrit.client.groups; import com.google.gerrit.client.info.AccountInfo; +import com.google.gerrit.client.info.GroupInfo; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java index a24e1dc..f51ecb8 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
@@ -14,6 +14,7 @@ package com.google.gerrit.client.groups; +import com.google.gerrit.client.info.GroupInfo; import com.google.gerrit.client.rpc.RestApi; import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gwt.core.client.JsArray;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java index 5532285..5e23049 100644 --- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java +++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
@@ -14,6 +14,7 @@ package com.google.gerrit.client.groups; +import com.google.gerrit.client.info.GroupInfo; import com.google.gerrit.client.rpc.NativeMap; import com.google.gerrit.client.rpc.RestApi; import com.google.gwt.user.client.rpc.AsyncCallback;
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 a96624a..983d48c 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
@@ -14,8 +14,8 @@ package com.google.gerrit.client.ui; -import com.google.gerrit.client.groups.GroupInfo; import com.google.gerrit.client.groups.GroupMap; +import com.google.gerrit.client.info.GroupInfo; import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.reviewdb.client.AccountGroup;
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java index 943be7e..3d99883 100644 --- a/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java +++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/ModeInfo.java
@@ -82,7 +82,6 @@ Modes.I.htmlmixed(), Modes.I.http(), Modes.I.idl(), - Modes.I.jade(), Modes.I.javascript(), Modes.I.jinja2(), Modes.I.jsx(), @@ -110,6 +109,7 @@ Modes.I.powershell(), Modes.I.properties(), Modes.I.protobuf(), + Modes.I.pug(), Modes.I.puppet(), Modes.I.python(), Modes.I.q(),
diff --git a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java index 668a57f..218b96c 100644 --- a/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java +++ b/gerrit-gwtui/src/main/java/net/codemirror/mode/Modes.java
@@ -67,7 +67,6 @@ @Source("htmlmixed.js") @DoNotEmbed DataResource htmlmixed(); @Source("http.js") @DoNotEmbed DataResource http(); @Source("idl.js") @DoNotEmbed DataResource idl(); - @Source("jade.js") @DoNotEmbed DataResource jade(); @Source("javascript.js") @DoNotEmbed DataResource javascript(); @Source("jinja2.js") @DoNotEmbed DataResource jinja2(); @Source("jsx.js") @DoNotEmbed DataResource jsx(); @@ -95,6 +94,7 @@ @Source("powershell.js") @DoNotEmbed DataResource powershell(); @Source("properties.js") @DoNotEmbed DataResource properties(); @Source("protobuf.js") @DoNotEmbed DataResource protobuf(); + @Source("pug.js") @DoNotEmbed DataResource pug(); @Source("puppet.js") @DoNotEmbed DataResource puppet(); @Source("python.js") @DoNotEmbed DataResource python(); @Source("q.js") @DoNotEmbed DataResource q();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java index 5146b31..7935bb6 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -14,7 +14,7 @@ package com.google.gerrit.httpd; -import static com.google.gerrit.reviewdb.client.AuthType.OAUTH; +import static com.google.gerrit.extensions.client.AuthType.OAUTH; import com.google.gerrit.reviewdb.client.CoreDownloadSchemes; import com.google.gerrit.server.config.AuthConfig;
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 2c67182..71bb981 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
@@ -18,6 +18,7 @@ import com.google.common.base.Strings; import com.google.gerrit.common.PageLinks; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.httpd.raw.CatServlet; import com.google.gerrit.httpd.raw.HostPageServlet; import com.google.gerrit.httpd.raw.LegacyGerritServlet; @@ -30,7 +31,6 @@ import com.google.gerrit.httpd.restapi.GroupsRestApiServlet; import com.google.gerrit.httpd.restapi.ProjectsRestApiServlet; import com.google.gerrit.httpd.rpc.doc.QueryDocumentationFilter; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.config.AuthConfig; @@ -64,6 +64,7 @@ if (options.enableDefaultUi()) { filter("/").through(XsrfCookieFilter.class); + filter("/accounts/self/detail").through(XsrfCookieFilter.class); serve("/").with(HostPageServlet.class); serve("/Gerrit").with(LegacyGerritServlet.class); serve("/Gerrit/*").with(legacyGerritScreen()); @@ -97,6 +98,11 @@ filter("/a/*").through(RequireIdentifiedUserFilter.class); serveRegex("^/(?:a/)?tools/(.*)$").with(ToolServlet.class); + + // Bind servlets for REST root collections. + // The '/plugins/' root collection is already handled by HttpPluginServlet + // which is bound in HttpPluginModule. We cannot bind it here again although + // this means that plugins can't add REST views on PLUGIN_KIND. serveRegex("^/(?:a/)?access/(.*)$").with(AccessRestApiServlet.class); serveRegex("^/(?:a/)?accounts/(.*)$").with(AccountsRestApiServlet.class); serveRegex("^/(?:a/)?changes/(.*)$").with(ChangesRestApiServlet.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 8594e30..27aff21 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
@@ -359,7 +359,6 @@ if (Strings.isNullOrEmpty(entryTitle)) { entryTitle = rsrc.substring(nameOffset, rsrc.length() - 3).replace('-', ' '); } - rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html"; } else { entryTitle = rsrc.substring(nameOffset).replace('-', ' '); }
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 943d824..e3f3fb1 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,6 +15,14 @@ package com.google.gerrit.httpd.restapi; import static com.google.common.base.Preconditions.checkNotNull; +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_REQUEST_HEADERS; +import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD; +import static com.google.common.net.HttpHeaders.ORIGIN; +import static com.google.common.net.HttpHeaders.VARY; import static java.math.RoundingMode.CEILING; import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.UTF_8; @@ -35,9 +43,12 @@ import com.google.common.base.CharMatcher; import com.google.common.base.Function; import com.google.common.base.Joiner; +import com.google.common.base.Predicates; import com.google.common.base.Splitter; import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Lists; @@ -85,6 +96,7 @@ 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.util.http.RequestUtil; import com.google.gson.ExclusionStrategy; import com.google.gson.FieldAttributes; @@ -103,6 +115,7 @@ import com.google.inject.Provider; import com.google.inject.util.Providers; +import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.util.TemporaryBuffer; import org.eclipse.jgit.util.TemporaryBuffer.Heap; import org.slf4j.Logger; @@ -131,6 +144,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import java.util.zip.GZIPOutputStream; import javax.servlet.ServletException; @@ -150,6 +164,9 @@ // HTTP 422 Unprocessable Entity. // 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 ImmutableSet<String> ALLOWED_CORS_REQUEST_HEADERS = + ImmutableSet.of(X_REQUESTED_WITH); private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks. @@ -174,18 +191,29 @@ final Provider<ParameterParser> paramParser; final AuditService auditService; final RestApiMetrics metrics; + final Pattern allowOrigin; @Inject Globals(Provider<CurrentUser> currentUser, DynamicItem<WebSession> webSession, Provider<ParameterParser> paramParser, AuditService auditService, - RestApiMetrics metrics) { + RestApiMetrics metrics, + @GerritServerConfig Config cfg) { this.currentUser = currentUser; this.webSession = webSession; this.paramParser = paramParser; this.auditService = auditService; this.metrics = metrics; + allowOrigin = makeAllowOrigin(cfg); + } + + private static Pattern makeAllowOrigin(Config cfg) { + String[] allow = cfg.getStringList("site", null, "allowOriginRegex"); + if (allow.length > 0) { + return Pattern.compile(Joiner.on('|').join(allow)); + } + return null; } } @@ -222,6 +250,11 @@ ViewData viewData = null; try { + if (isCorsPreflight(req)) { + doCorsPreflight(req, res); + return; + } + checkCors(req, res); checkUserSession(req); List<IdString> path = splitPath(req); @@ -232,7 +265,7 @@ viewData = new ViewData(null, null); if (path.isEmpty()) { - if (isGetOrHead(req)) { + if (isRead(req)) { viewData = new ViewData(null, rc.list()); } else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) { @SuppressWarnings("unchecked") @@ -273,7 +306,7 @@ (RestCollection<RestResource, RestResource>) viewData.view; if (path.isEmpty()) { - if (isGetOrHead(req)) { + if (isRead(req)) { viewData = new ViewData(null, c.list()); } else if (c instanceof AcceptsPost && "POST".equals(req.getMethod())) { @SuppressWarnings("unchecked") @@ -330,7 +363,7 @@ return; } - if (viewData.view instanceof RestReadView<?> && isGetOrHead(req)) { + if (viewData.view instanceof RestReadView<?> && isRead(req)) { result = ((RestReadView<RestResource>) viewData.view).apply(rsrc); } else if (viewData.view instanceof RestModifyView<?, ?>) { @SuppressWarnings("unchecked") @@ -428,6 +461,72 @@ } } + private void checkCors(HttpServletRequest req, HttpServletResponse res) { + String origin = req.getHeader(ORIGIN); + if (isRead(req) + && !Strings.isNullOrEmpty(origin) + && isOriginAllowed(origin)) { + res.addHeader(VARY, ORIGIN); + setCorsHeaders(res, origin); + } + } + + private static boolean isCorsPreflight(HttpServletRequest req) { + return "OPTIONS".equals(req.getMethod()) + && !Strings.isNullOrEmpty(req.getHeader(ORIGIN)) + && !Strings.isNullOrEmpty(req.getHeader(ACCESS_CONTROL_REQUEST_METHOD)); + } + + 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))); + + String origin = req.getHeader(ORIGIN); + if (Strings.isNullOrEmpty(origin) || !isOriginAllowed(origin)) { + throw new BadRequestException("CORS not allowed"); + } + + String method = req.getHeader(ACCESS_CONTROL_REQUEST_METHOD); + if (!"GET".equals(method) && !"HEAD".equals(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 = Iterables.getFirst( + Iterables.filter( + Splitter.on(',').trimResults().split(headers), + Predicates.not(Predicates.in(ALLOWED_CORS_REQUEST_HEADERS))), + null); + if (badHeader != null) { + throw new BadRequestException(badHeader + " not allowed in CORS"); + } + } + + res.setStatus(SC_OK); + setCorsHeaders(res, origin); + res.setContentType("text/plain"); + res.setContentLength(0); + } + + private 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)); + } + + private boolean isOriginAllowed(String origin) { + return globals.allowOrigin != null + && globals.allowOrigin.matcher(origin).matches(); + } + private static String messageOr(Throwable t, String defaultMessage) { if (!Strings.isNullOrEmpty(t.getMessage())) { return t.getMessage(); @@ -438,7 +537,7 @@ @SuppressWarnings({"unchecked", "rawtypes"}) private static boolean notModified(HttpServletRequest req, RestResource rsrc, RestView<RestResource> view) { - if (!isGetOrHead(req)) { + if (!isRead(req)) { return false; } @@ -469,7 +568,7 @@ private static <R extends RestResource> void configureCaching( HttpServletRequest req, HttpServletResponse res, R rsrc, RestView<R> view, CacheControl c) { - if (isGetOrHead(req)) { + if (isRead(req)) { switch (c.getType()) { case NONE: default: @@ -972,25 +1071,20 @@ private void checkUserSession(HttpServletRequest req) throws AuthException { CurrentUser user = globals.currentUser.get(); - if (isStateChange(req)) { - if (user instanceof AnonymousUser) { - throw new AuthException("Authentication required"); - } else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) { - throw new AuthException("Invalid authentication method. In order to authenticate, " - + "prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/)."); - } + if (isRead(req)) { + user.setAccessPath(AccessPath.REST_API); + } else if (user instanceof AnonymousUser) { + throw new AuthException("Authentication required"); + } else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) { + throw new AuthException("Invalid authentication method. In order to authenticate, " + + "prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/)."); } - user.setAccessPath(AccessPath.REST_API); } - private static boolean isGetOrHead(HttpServletRequest req) { + private static boolean isRead(HttpServletRequest req) { return "GET".equals(req.getMethod()) || "HEAD".equals(req.getMethod()); } - private static boolean isStateChange(HttpServletRequest req) { - return !isGetOrHead(req); - } - private void checkRequiresCapability(ViewData viewData) throws AuthException { CapabilityUtils.checkRequiresCapability(globals.currentUser, viewData.pluginName, viewData.view.getClass()); @@ -1029,7 +1123,7 @@ static long replyText(@Nullable HttpServletRequest req, HttpServletResponse res, String text) throws IOException { - if ((req == null || isGetOrHead(req)) && isMaybeHTML(text)) { + if ((req == null || isRead(req)) && isMaybeHTML(text)) { return replyJson(req, res, ImmutableMultimap.of("pp", "0"), new JsonPrimitive(text)); } if (!text.endsWith("\n")) {
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 c0fb86b..bda2d91 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
@@ -14,11 +14,8 @@ package com.google.gerrit.httpd.rpc; -import com.google.common.collect.Lists; -import com.google.gerrit.common.data.ContributorAgreement; import com.google.gerrit.common.data.SshHostKey; import com.google.gerrit.common.data.SystemInfoService; -import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.ssh.SshInfo; import com.google.gwtjsonrpc.common.AsyncCallback; import com.google.gwtjsonrpc.common.VoidResult; @@ -32,7 +29,6 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import javax.servlet.http.HttpServletRequest; @@ -45,28 +41,12 @@ private final List<HostKey> hostKeys; private final Provider<HttpServletRequest> httpRequest; - private final ProjectCache projectCache; @Inject SystemInfoServiceImpl(SshInfo daemon, - Provider<HttpServletRequest> hsr, - ProjectCache pc) { + Provider<HttpServletRequest> hsr) { hostKeys = daemon.getHostKeys(); httpRequest = hsr; - projectCache = pc; - } - - @Override - public void contributorAgreements( - final AsyncCallback<List<ContributorAgreement>> callback) { - Collection<ContributorAgreement> agreements = - projectCache.getAllProjects().getConfig().getContributorAgreements(); - List<ContributorAgreement> cas = - Lists.newArrayListWithCapacity(agreements.size()); - for (ContributorAgreement ca : agreements) { - cas.add(ca.forUi()); - } - callback.onSuccess(cas); } @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java index 62778eb..d32fdaf 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
@@ -28,12 +28,10 @@ install(new FactoryModule() { @Override protected void configure() { - factory(AgreementInfoFactory.Factory.class); factory(DeleteExternalIds.Factory.class); factory(ExternalIdDetailFactory.Factory.class); } }); rpc(AccountSecurityImpl.class); - rpc(AccountServiceImpl.class); } }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java index 8fcf9ea..3d05548 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -14,74 +14,31 @@ package com.google.gerrit.httpd.rpc.account; -import com.google.common.base.Strings; -import com.google.gerrit.audit.AuditService; import com.google.gerrit.common.data.AccountSecurity; -import com.google.gerrit.common.data.ContributorAgreement; -import com.google.gerrit.common.errors.NoSuchEntityException; -import com.google.gerrit.common.errors.PermissionDeniedException; import com.google.gerrit.httpd.rpc.BaseServiceImplementation; -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.CurrentUser; -import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.account.AccountByEmailCache; -import com.google.gerrit.server.account.AccountCache; -import com.google.gerrit.server.account.GroupCache; -import com.google.gerrit.server.account.Realm; -import com.google.gerrit.server.extensions.events.AgreementSignup; -import com.google.gerrit.server.project.ProjectCache; import com.google.gwtjsonrpc.common.AsyncCallback; -import com.google.gwtjsonrpc.common.VoidResult; -import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; -import java.io.IOException; -import java.util.Collections; import java.util.List; import java.util.Set; class AccountSecurityImpl extends BaseServiceImplementation implements AccountSecurity { - private final Realm realm; - private final ProjectCache projectCache; - private final Provider<IdentifiedUser> user; - private final AccountByEmailCache byEmailCache; - private final AccountCache accountCache; - private final DeleteExternalIds.Factory deleteExternalIdsFactory; private final ExternalIdDetailFactory.Factory externalIdDetailFactory; - private final GroupCache groupCache; - private final AuditService auditService; - private final AgreementSignup agreementSignup; - @Inject AccountSecurityImpl(final Provider<ReviewDb> schema, final Provider<CurrentUser> currentUser, - final Realm r, final Provider<IdentifiedUser> u, - final ProjectCache pc, - final AccountByEmailCache abec, final AccountCache uac, final DeleteExternalIds.Factory deleteExternalIdsFactory, - final ExternalIdDetailFactory.Factory externalIdDetailFactory, - final GroupCache groupCache, - final AuditService auditService, - AgreementSignup agreementSignup) { + final ExternalIdDetailFactory.Factory externalIdDetailFactory) { super(schema, currentUser); - realm = r; - user = u; - projectCache = pc; - byEmailCache = abec; - accountCache = uac; - this.auditService = auditService; this.deleteExternalIdsFactory = deleteExternalIdsFactory; this.externalIdDetailFactory = externalIdDetailFactory; - this.groupCache = groupCache; - this.agreementSignup = agreementSignup; } @Override @@ -94,84 +51,4 @@ final AsyncCallback<Set<AccountExternalId.Key>> callback) { deleteExternalIdsFactory.create(keys).to(callback); } - - @Override - public void updateContact(final String name, final String emailAddr, - final AsyncCallback<Account> callback) { - run(callback, new Action<Account>() { - @Override - public Account run(ReviewDb db) - throws OrmException, Failure, IOException { - IdentifiedUser self = user.get(); - final Account me = db.accounts().get(self.getAccountId()); - final String oldEmail = me.getPreferredEmail(); - if (realm.allowsEdit(Account.FieldName.FULL_NAME)) { - me.setFullName(Strings.emptyToNull(name)); - } - if (!Strings.isNullOrEmpty(emailAddr) - && !self.hasEmailAddress(emailAddr)) { - throw new Failure(new PermissionDeniedException("Email address must be verified")); - } - me.setPreferredEmail(Strings.emptyToNull(emailAddr)); - db.accounts().update(Collections.singleton(me)); - if (!eq(oldEmail, me.getPreferredEmail())) { - byEmailCache.evict(oldEmail); - byEmailCache.evict(me.getPreferredEmail()); - } - accountCache.evict(me.getId()); - return me; - } - }); - } - - private static boolean eq(final String a, final String b) { - if (a == null && b == null) { - return true; - } - return a != null && a.equals(b); - } - - @Override - public void enterAgreement(final String agreementName, - final AsyncCallback<VoidResult> callback) { - run(callback, new Action<VoidResult>() { - @Override - public VoidResult run(final ReviewDb db) - throws OrmException, Failure, IOException { - ContributorAgreement ca = projectCache.getAllProjects().getConfig() - .getContributorAgreement(agreementName); - if (ca == null) { - throw new Failure(new NoSuchEntityException()); - } - - if (ca.getAutoVerify() == null) { - throw new Failure(new IllegalStateException( - "cannot enter a non-autoVerify agreement")); - } else if (ca.getAutoVerify().getUUID() == null) { - throw new Failure(new NoSuchEntityException()); - } - - AccountGroup group = groupCache.get(ca.getAutoVerify().getUUID()); - if (group == null) { - throw new Failure(new NoSuchEntityException()); - } - - Account account = user.get().getAccount(); - agreementSignup.fire(account, ca.getName()); - - final AccountGroupMember.Key key = - new AccountGroupMember.Key(account.getId(), group.getId()); - AccountGroupMember m = db.accountGroupMembers().get(key); - if (m == null) { - m = new AccountGroupMember(key); - auditService.dispatchAddAccountsToGroup(account.getId(), Collections - .singleton(m)); - db.accountGroupMembers().insert(Collections.singleton(m)); - accountCache.evict(m.getAccountId()); - } - - return VoidResult.INSTANCE; - } - }); - } }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java deleted file mode 100644 index 8fba47d..0000000 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java +++ /dev/null
@@ -1,42 +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.httpd.rpc.account; - -import com.google.gerrit.common.data.AccountService; -import com.google.gerrit.common.data.AgreementInfo; -import com.google.gerrit.httpd.rpc.BaseServiceImplementation; -import com.google.gerrit.reviewdb.server.ReviewDb; -import com.google.gerrit.server.IdentifiedUser; -import com.google.gwtjsonrpc.common.AsyncCallback; -import com.google.inject.Inject; -import com.google.inject.Provider; - -class AccountServiceImpl extends BaseServiceImplementation implements - AccountService { - private final AgreementInfoFactory.Factory agreementInfoFactory; - - @Inject - AccountServiceImpl(final Provider<ReviewDb> schema, - final Provider<IdentifiedUser> identifiedUser, - final AgreementInfoFactory.Factory agreementInfoFactory) { - super(schema, identifiedUser); - this.agreementInfoFactory = agreementInfoFactory; - } - - @Override - public void myAgreements(final AsyncCallback<AgreementInfo> callback) { - agreementInfoFactory.create().to(callback); - } -}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java deleted file mode 100644 index 91afd97..0000000 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java +++ /dev/null
@@ -1,85 +0,0 @@ -// Copyright (C) 2009 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.gerrit.httpd.rpc.account; - -import com.google.gerrit.common.data.AgreementInfo; -import com.google.gerrit.common.data.ContributorAgreement; -import com.google.gerrit.common.data.PermissionRule; -import com.google.gerrit.common.data.PermissionRule.Action; -import com.google.gerrit.httpd.rpc.Handler; -import com.google.gerrit.reviewdb.client.AccountGroup; -import com.google.gerrit.server.IdentifiedUser; -import com.google.gerrit.server.project.ProjectCache; -import com.google.inject.Inject; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -class AgreementInfoFactory extends Handler<AgreementInfo> { - private static final Logger log = LoggerFactory.getLogger(AgreementInfoFactory.class); - - interface Factory { - AgreementInfoFactory create(); - } - - private final IdentifiedUser user; - private final ProjectCache projectCache; - - private AgreementInfo info; - - @Inject - AgreementInfoFactory(final IdentifiedUser user, - final ProjectCache projectCache) { - this.user = user; - this.projectCache = projectCache; - } - - @Override - public AgreementInfo call() throws Exception { - List<String> accepted = new ArrayList<>(); - Map<String, ContributorAgreement> agreements = new HashMap<>(); - Collection<ContributorAgreement> cas = - projectCache.getAllProjects().getConfig().getContributorAgreements(); - for (ContributorAgreement ca : cas) { - agreements.put(ca.getName(), ca.forUi()); - - List<AccountGroup.UUID> groupIds = new ArrayList<>(); - for (PermissionRule rule : ca.getAccepted()) { - if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)) { - if (rule.getGroup().getUUID() == null) { - log.warn("group \"" + rule.getGroup().getName() + "\" does not " + - " exist, referenced in CLA \"" + ca.getName() + "\""); - } else { - groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get())); - } - } - } - if (user.getEffectiveGroups().containsAnyOf(groupIds)) { - accepted.add(ca.getName()); - } - } - - info = new AgreementInfo(); - info.setAccepted(accepted); - info.setAgreements(agreements); - return info; - } -}
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java index 3a40252..791f9fd 100644 --- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java +++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -24,12 +24,12 @@ import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.auth.openid.OpenIdUrls; import com.google.gerrit.extensions.auth.oauth.OAuthServiceProvider; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.httpd.HtmlDomUtil; import com.google.gerrit.httpd.LoginUrlToken; import com.google.gerrit.httpd.template.SiteHeaderFooter; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.CanonicalWebUrl;
diff --git a/gerrit-pgm/BUILD b/gerrit-pgm/BUILD index 59b371a..ec86c9a 100644 --- a/gerrit-pgm/BUILD +++ b/gerrit-pgm/BUILD
@@ -19,6 +19,7 @@ '//lib/guice:guice-assistedinject', '//lib/guice:guice-servlet', '//lib/jgit/org.eclipse.jgit:jgit', + '//lib/joda:joda-time', '//lib/log:api', '//lib/log:log4j', ]
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 7ee3cde..d98f999 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
@@ -20,6 +20,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.gerrit.common.EventBroker; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.gpg.GpgModule; import com.google.gerrit.httpd.AllRequestFilter; import com.google.gerrit.httpd.GerritOptions; @@ -46,7 +47,6 @@ import com.google.gerrit.pgm.util.LogFileCompressor; import com.google.gerrit.pgm.util.RuntimeShutdown; import com.google.gerrit.pgm.util.SiteProgram; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.server.account.InternalAccountDirectory; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; import com.google.gerrit.server.change.ChangeCleanupRunner;
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 9d27170..f5212ab 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
@@ -18,9 +18,9 @@ import static java.util.concurrent.TimeUnit.SECONDS; import com.google.common.base.Strings; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.extensions.events.LifecycleListener; import com.google.gerrit.pgm.http.jetty.HttpLog.HttpLogFactory; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.config.ThreadSettingsConfig;
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 2de71cc..136ec5a 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
@@ -18,6 +18,7 @@ import com.google.common.base.Strings; import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.pgm.init.api.ConsoleUI; import com.google.gerrit.pgm.init.api.InitFlags; import com.google.gerrit.pgm.init.api.InitStep; @@ -27,7 +28,6 @@ import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.client.AccountGroupName; import com.google.gerrit.reviewdb.client.AccountSshKey; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gwtorm.server.SchemaFactory; import com.google.inject.Inject;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java index 6b30f80..a6471c7 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -16,11 +16,11 @@ import static com.google.gerrit.pgm.init.api.InitUtil.dnOf; +import com.google.gerrit.extensions.client.AuthType; 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.reviewdb.client.AuthType; import com.google.gwtjsonrpc.server.SignedToken; import com.google.inject.Inject; import com.google.inject.Singleton;
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 f16e2ec..978b87c 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
@@ -1,4 +1,4 @@ -// Copyright (C) 2009 The Android Open Source Project +// 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. @@ -99,21 +99,21 @@ chmod(0755, site.gerrit_sh); chmod(0700, site.tmp_dir); - extractMailExample("Abandoned.vm"); - extractMailExample("AddKey.vm"); - extractMailExample("ChangeFooter.vm"); - extractMailExample("ChangeSubject.vm"); - extractMailExample("Comment.vm"); - extractMailExample("CommentFooter.vm"); - extractMailExample("DeleteReviewer.vm"); - extractMailExample("DeleteVote.vm"); - extractMailExample("Footer.vm"); - extractMailExample("Merged.vm"); - extractMailExample("NewChange.vm"); - extractMailExample("RegisterNewEmail.vm"); - extractMailExample("ReplacePatchSet.vm"); - extractMailExample("Restored.vm"); - extractMailExample("Reverted.vm"); + extractMailExample("Abandoned.soy"); + extractMailExample("AddKey.soy"); + extractMailExample("ChangeFooter.soy"); + extractMailExample("ChangeSubject.soy"); + extractMailExample("Comment.soy"); + extractMailExample("CommentFooter.soy"); + extractMailExample("DeleteReviewer.soy"); + extractMailExample("DeleteVote.soy"); + extractMailExample("Footer.soy"); + extractMailExample("Merged.soy"); + extractMailExample("NewChange.soy"); + extractMailExample("RegisterNewEmail.soy"); + extractMailExample("ReplacePatchSet.soy"); + extractMailExample("Restored.soy"); + extractMailExample("Reverted.soy"); if (!ui.isBatch()) { System.err.println();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java index 0360cd6..d39c2fd 100644 --- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/BatchGitModule.java
@@ -19,7 +19,6 @@ import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.server.git.GitModule; import com.google.gerrit.server.git.validators.CommitValidationListener; -import com.google.gerrit.server.git.validators.CommitValidators; /** Module for batch programs that need git access. */ public class BatchGitModule extends FactoryModule { @@ -27,7 +26,6 @@ protected void configure() { DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class); DynamicSet.setOf(binder(), CommitValidationListener.class); - factory(CommitValidators.Factory.class); install(new GitModule()); } }
diff --git a/gerrit-plugin-api/BUCK b/gerrit-plugin-api/BUCK index c18e497..7e38726 100644 --- a/gerrit-plugin-api/BUCK +++ b/gerrit-plugin-api/BUCK
@@ -13,10 +13,20 @@ java_binary( name = 'plugin-api', + merge_manifests = False, + manifest_file = ':manifest', deps = [':lib'], visibility = ['PUBLIC'], ) +genrule( + name = 'manifest', + cmd = 'echo "Manifest-Version: 1.0" >$OUT;' + + 'echo "Implementation-Title: Gerrit Plugin API" >>$OUT;' + + 'echo "Implementation-Vendor: Gerrit Code Review Project" >>$OUT', + out = 'manifest.txt', +) + java_library( name = 'lib', exported_deps = PLUGIN_API + [ @@ -32,20 +42,30 @@ '//lib:gson', '//lib:guava', '//lib:gwtorm', + '//lib:icu4j', '//lib:jsch', + '//lib:jsr305', '//lib:mime-util', + '//lib:protobuf', '//lib:servlet-api-3_1', + '//lib:soy', '//lib:velocity', '//lib/commons:lang', '//lib/dropwizard:dropwizard-core', '//lib/guice:guice', '//lib/guice:guice-assistedinject', + '//lib/guice:javax-inject', + '//lib/guice:multibindings', '//lib/guice:guice-servlet', '//lib/jgit/org.eclipse.jgit:jgit', '//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet', '//lib/joda:joda-time', '//lib/log:api', '//lib/mina:sshd', + '//lib/ow2:ow2-asm', + '//lib/ow2:ow2-asm-analysis', + '//lib/ow2:ow2-asm-commons', + '//lib/ow2:ow2-asm-util', '//lib/prolog:compiler', ], visibility = ['PUBLIC'], @@ -63,7 +83,7 @@ name = 'plugin-api-javadoc', title = 'Gerrit Review Plugin API Documentation', pkgs = ['com.google.gerrit'], - paths = [n for n in SRCS], + source_jar = ':plugin-api-src', srcs = glob([n + '**/*.java' for n in SRCS]), deps = [ ':plugin-api', @@ -72,5 +92,4 @@ '//lib/bouncycastle:bcpkix', ], visibility = ['PUBLIC'], - do_it_wrong = True, )
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD index 2c18ca6..c761703 100644 --- a/gerrit-plugin-api/BUILD +++ b/gerrit-plugin-api/BUILD
@@ -28,24 +28,33 @@ '//gerrit-extension-api:api', '//gerrit-gwtexpui:server', '//gerrit-reviewdb:server', - '//lib:args4j', - '//lib:blame-cache', - '//lib/dropwizard:dropwizard-core', - '//lib:guava', - '//lib:gwtorm', - '//lib:jsch', - '//lib:mime-util', - '//lib:servlet-api-3_1', - '//lib:velocity', '//lib/commons:lang', + '//lib/dropwizard:dropwizard-core', '//lib/guice:guice', '//lib/guice:guice-assistedinject', '//lib/guice:guice-servlet', - '//lib/jgit/org.eclipse.jgit:jgit', + '//lib/guice:javax-inject', + '//lib/guice:multibindings', '//lib/jgit/org.eclipse.jgit.http.server:jgit-servlet', + '//lib/jgit/org.eclipse.jgit:jgit', '//lib/joda:joda-time', '//lib/log:api', '//lib/mina:sshd', + '//lib/ow2:ow2-asm', + '//lib/ow2:ow2-asm-analysis', + '//lib/ow2:ow2-asm-commons', + '//lib/ow2:ow2-asm-util', + '//lib:args4j', + '//lib:blame-cache', + '//lib:guava', + '//lib:gwtorm', + '//lib:icu4j', + '//lib:jsch', + '//lib:mime-util', + '//lib:protobuf', + '//lib:servlet-api-3_1', + '//lib:soy', + '//lib:velocity', ], visibility = ['//visibility:public'], )
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml index 77bb111..9309921 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.13-SNAPSHOT</version> + <version>2.14-SNAPSHOT</version> <packaging>jar</packaging> <name>Gerrit Code Review - Plugin API</name> <description>API for Gerrit Plugins</description>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml index d8e3583..e69da7c 100644 --- a/gerrit-plugin-archetype/pom.xml +++ b/gerrit-plugin-archetype/pom.xml
@@ -20,7 +20,7 @@ <groupId>com.google.gerrit</groupId> <artifactId>gerrit-plugin-archetype</artifactId> - <version>2.13-SNAPSHOT</version> + <version>2.14-SNAPSHOT</version> <name>Gerrit Code Review - Plugin Archetype</name> <description>Maven Archetype for Gerrit Plugins</description> <url>https://www.gerritcodereview.com/</url>
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml index 362b45c..5e9bc33 100644 --- a/gerrit-plugin-gwt-archetype/pom.xml +++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -20,7 +20,7 @@ <groupId>com.google.gerrit</groupId> <artifactId>gerrit-plugin-gwt-archetype</artifactId> - <version>2.13-SNAPSHOT</version> + <version>2.14-SNAPSHOT</version> <name>Gerrit Code Review - Web UI GWT Plugin Archetype</name> <description>Maven Archetype for Gerrit Web UI GWT Plugins</description> <url>https://www.gerritcodereview.com/</url>
diff --git a/gerrit-plugin-gwtui/BUCK b/gerrit-plugin-gwtui/BUCK index 2ee0e19..575ebfc 100644 --- a/gerrit-plugin-gwtui/BUCK +++ b/gerrit-plugin-gwtui/BUCK
@@ -1,8 +1,4 @@ -COMMON = ['gerrit-gwtui-common/src/main/java/'] -GWTEXPUI = ['gerrit-gwtexpui/src/main/java/'] -SRC = 'src/main/java/com/google/gerrit/' -SRCS = glob([SRC + '**/*.java']) - +SRCS = glob(['src/main/java/com/google/gerrit/**/*.java']) DEPS = ['//lib/gwt:user'] java_binary( @@ -50,7 +46,7 @@ 'com.google.gwtexpui.safehtml', 'com.google.gwtexpui.user', ], - paths = COMMON + GWTEXPUI, + source_jar = ':gwtui-api-src', srcs = SRCS, deps = DEPS + [ '//lib:gwtjsonrpc', @@ -61,5 +57,4 @@ '//gerrit-reviewdb:client', ], visibility = ['PUBLIC'], - do_it_wrong = True, )
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml index 982d268..4b104c6 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.13-SNAPSHOT</version> + <version>2.14-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-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml index e22478a..d31b455 100644 --- a/gerrit-plugin-js-archetype/pom.xml +++ b/gerrit-plugin-js-archetype/pom.xml
@@ -20,7 +20,7 @@ <groupId>com.google.gerrit</groupId> <artifactId>gerrit-plugin-js-archetype</artifactId> - <version>2.13-SNAPSHOT</version> + <version>2.14-SNAPSHOT</version> <name>Gerrit Code Review - Web UI JavaScript Plugin Archetype</name> <description>Maven Archetype for Gerrit Web UI JavaScript Plugins</description> <url>https://www.gerritcodereview.com/</url>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java index 9e36fc1..de2134b 100644 --- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java +++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
@@ -55,10 +55,6 @@ * </ul> */ public final class Account { - public enum FieldName { - FULL_NAME, USER_NAME, REGISTER_NEW_EMAIL - } - public static final String USER_NAME_PATTERN_FIRST = "[a-zA-Z0-9]"; public static final String USER_NAME_PATTERN_REST = "[a-zA-Z0-9._-]"; public static final String USER_NAME_PATTERN_LAST = "[a-zA-Z0-9]";
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 index 41336791..5ae8847 100644 --- 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
@@ -14,6 +14,7 @@ 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;
diff --git a/gerrit-server/BUCK b/gerrit-server/BUCK index 4fc578c..080b52b 100644 --- a/gerrit-server/BUCK +++ b/gerrit-server/BUCK
@@ -46,6 +46,7 @@ '//lib:mime-util', '//lib:pegdown', '//lib:protobuf', + '//lib:soy', '//lib:tukaani-xz', '//lib:velocity', '//lib/antlr:java_runtime', @@ -203,7 +204,6 @@ '//lib:guava', '//lib:guava-retrying', '//lib:protobuf', - '//lib/commons:validator', '//lib/dropwizard:dropwizard-core', '//lib/guice:guice-assistedinject', '//lib/prolog:runtime',
diff --git a/gerrit-server/BUILD b/gerrit-server/BUILD index 5a6b50f..a591fba 100644 --- a/gerrit-server/BUILD +++ b/gerrit-server/BUILD
@@ -48,6 +48,7 @@ '//lib:pegdown', '//lib:protobuf', '//lib:servlet-api-3_1', + '//lib:soy', '//lib:tukaani-xz', '//lib:velocity', '//lib/antlr:java_runtime', @@ -188,6 +189,7 @@ ['src/test/java/**/*.java'], exclude = TESTUTIL + PROLOG_TESTS + PROLOG_TEST_CASE + QUERY_TESTS ), + resources = glob(['src/test/resources/com/google/gerrit/server/mail/*']), deps = TESTUTIL_DEPS + [ ':testutil', '//gerrit-antlr:query_exception',
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 bc2ec06..d69ad3f 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
@@ -77,22 +77,54 @@ this.psUtil = psUtil; } + /** + * Apply approval copy settings from prior PatchSets to a new PatchSet. + * + * @param db review database. + * @param ctl change control for user uploading PatchSet + * @param ps new PatchSet + * @throws OrmException + */ public void copy(ReviewDb db, ChangeControl ctl, PatchSet ps) throws OrmException { - db.patchSetApprovals().insert(getForPatchSet(db, ctl, ps)); + copy(db, ctl, ps, Collections.<PatchSetApproval>emptyList()); + } + + /** + * Apply approval copy settings from prior PatchSets to a new PatchSet. + * + * @param db review database. + * @param ctl change control for user uploading PatchSet + * @param ps new PatchSet + * @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) 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()); + } + + Iterable<PatchSetApproval> getForPatchSet(ReviewDb db, + ChangeControl ctl, PatchSet.Id psId, + Iterable<PatchSetApproval> dontCopy) throws OrmException { PatchSet ps = psUtil.get(db, ctl.getNotes(), psId); if (ps == null) { return Collections.emptyList(); } - return getForPatchSet(db, ctl, ps); + return getForPatchSet(db, ctl, ps, dontCopy); } private Iterable<PatchSetApproval> getForPatchSet(ReviewDb db, - ChangeControl ctl, PatchSet ps) throws OrmException { + ChangeControl ctl, PatchSet ps, + Iterable<PatchSetApproval> dontCopy) throws OrmException { checkNotNull(ps, "ps should not be null"); ChangeData cd = changeDataFactory.create(db, ctl); try { @@ -103,10 +135,16 @@ Table<String, Account.Id, PatchSetApproval> wontCopy = HashBasedTable.create(); + for (PatchSetApproval psa : dontCopy) { + wontCopy.put(psa.getLabel(), psa.getAccountId(), psa); + } + Table<String, Account.Id, PatchSetApproval> byUser = HashBasedTable.create(); for (PatchSetApproval psa : all.get(ps.getId())) { - byUser.put(psa.getLabel(), psa.getAccountId(), psa); + if (!wontCopy.contains(psa.getLabel(), psa.getAccountId())) { + byUser.put(psa.getLabel(), psa.getAccountId(), psa); + } } TreeMap<Integer, PatchSet> patchSets = getPatchSets(cd);
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 e0526e4..5c0723a 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
@@ -43,11 +43,15 @@ import com.google.gerrit.server.notedb.NotesMigration; import com.google.gerrit.server.notedb.ReviewerStateInternal; import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.util.LabelVote; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collection; @@ -73,6 +77,9 @@ */ @Singleton public class ApprovalsUtil { + private static final Logger log = + LoggerFactory.getLogger(ApprovalsUtil.class); + private static final Ordering<PatchSetApproval> SORT_APPROVALS = Ordering.natural() .onResultOf( @@ -99,13 +106,19 @@ } private final NotesMigration migration; + private final IdentifiedUser.GenericFactory userFactory; + private final ChangeControl.GenericFactory changeControlFactory; private final ApprovalCopier copier; @VisibleForTesting @Inject public ApprovalsUtil(NotesMigration migration, + IdentifiedUser.GenericFactory userFactory, + ChangeControl.GenericFactory changeControlFactory, ApprovalCopier copier) { this.migration = migration; + this.userFactory = userFactory; + this.changeControlFactory = changeControlFactory; this.copier = copier; } @@ -164,8 +177,8 @@ PatchSetInfo info, Iterable<Account.Id> wantReviewers, Collection<Account.Id> existingReviewers) throws OrmException { return addReviewers(db, update, labelTypes, change, ps.getId(), - ps.isDraft(), info.getAuthor().getAccount(), - info.getCommitter().getAccount(), wantReviewers, existingReviewers); + info.getAuthor().getAccount(), info.getCommitter().getAccount(), + wantReviewers, existingReviewers); } public List<PatchSetApproval> addReviewers(ReviewDb db, ChangeNotes notes, @@ -189,12 +202,12 @@ existingReviewers.add(entry.getKey()); } } - return addReviewers(db, update, labelTypes, change, psId, false, null, null, + return addReviewers(db, update, labelTypes, change, psId, null, null, wantReviewers, existingReviewers); } private List<PatchSetApproval> addReviewers(ReviewDb db, ChangeUpdate update, - LabelTypes labelTypes, Change change, PatchSet.Id psId, boolean isDraft, + LabelTypes labelTypes, Change change, PatchSet.Id psId, Account.Id authorId, Account.Id committerId, Iterable<Account.Id> wantReviewers, Collection<Account.Id> existingReviewers) throws OrmException { @@ -204,11 +217,11 @@ } Set<Account.Id> need = Sets.newLinkedHashSet(wantReviewers); - if (authorId != null && !isDraft) { + if (authorId != null && canSee(db, update.getNotes(), authorId)) { need.add(authorId); } - if (committerId != null && !isDraft) { + if (committerId != null && canSee(db, update.getNotes(), committerId)) { need.add(committerId); } need.remove(change.getOwner()); @@ -229,6 +242,17 @@ return Collections.unmodifiableList(cells); } + private boolean canSee(ReviewDb db, ChangeNotes notes, Account.Id accountId) { + try { + IdentifiedUser user = userFactory.create(accountId); + return changeControlFactory.controlFor(notes, user).isVisible(db); + } catch (OrmException | NoSuchChangeException e) { + log.warn(String.format("Failed to check if account %d can see change %d", + accountId.get(), notes.getChangeId().get()), e); + return false; + } + } + /** * Adds accounts to a change as reviewers in the CC state. * @@ -254,25 +278,40 @@ return need; } - public void addApprovals(ReviewDb db, ChangeUpdate update, + /** + * Adds approvals to ChangeUpdate and writes to ReviewDb. + * + * @param db review database. + * @param update change update. + * @param labelTypes label types for the containing project. + * @param ps patch set being approved. + * @param changeCtl change control for user adding approvals. + * @param approvals approvals to add. + * @throws OrmException + */ + public Iterable<PatchSetApproval> addApprovals(ReviewDb db, ChangeUpdate update, LabelTypes labelTypes, PatchSet ps, ChangeControl changeCtl, Map<String, Short> approvals) throws OrmException { - if (!approvals.isEmpty()) { - checkApprovals(approvals, changeCtl); - List<PatchSetApproval> cells = new ArrayList<>(approvals.size()); - Date ts = update.getWhen(); - for (Map.Entry<String, Short> vote : approvals.entrySet()) { - LabelType lt = labelTypes.byLabel(vote.getKey()); - cells.add(new PatchSetApproval(new PatchSetApproval.Key( - ps.getId(), - ps.getUploader(), - lt.getLabelId()), - vote.getValue(), - ts)); - update.putApproval(vote.getKey(), vote.getValue()); - } - db.patchSetApprovals().insert(cells); + if (approvals.isEmpty()) { + return Collections.emptyList(); } + checkApprovals(approvals, changeCtl); + List<PatchSetApproval> cells = new ArrayList<>(approvals.size()); + Date ts = update.getWhen(); + for (Map.Entry<String, Short> vote : approvals.entrySet()) { + LabelType lt = labelTypes.byLabel(vote.getKey()); + cells.add(new PatchSetApproval(new PatchSetApproval.Key( + ps.getId(), + ps.getUploader(), + lt.getLabelId()), + vote.getValue(), + ts)); + } + for (PatchSetApproval psa : cells) { + update.putApproval(psa.getLabel(), psa.getValue()); + } + db.patchSetApprovals().insert(cells); + return cells; } public static void checkLabel(LabelTypes labelTypes, String name, Short value) {
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 30420e0..a0c6118 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
@@ -16,8 +16,7 @@ import com.google.common.base.Strings; import com.google.common.collect.Sets; -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.Account.FieldName; +import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.mail.EmailSender; @@ -37,11 +36,11 @@ } @Override - public Set<FieldName> getEditableFields() { - Set<Account.FieldName> fields = new HashSet<>(); - for (Account.FieldName n : Account.FieldName.values()) { + public Set<AccountFieldName> getEditableFields() { + Set<AccountFieldName> fields = new HashSet<>(); + for (AccountFieldName n : AccountFieldName.values()) { if (allowsEdit(n)) { - if (n == Account.FieldName.REGISTER_NEW_EMAIL) { + if (n == AccountFieldName.REGISTER_NEW_EMAIL) { if (emailSender != null && emailSender.isEnabled()) { fields.add(n); }
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 19f9fcf..178cc79 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
@@ -21,6 +21,7 @@ import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.Permission; import com.google.gerrit.common.errors.NameAlreadyUsedException; +import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.reviewdb.client.AccountGroup; @@ -201,14 +202,14 @@ db.accountExternalIds().update(Collections.singleton(extId)); } - if (!realm.allowsEdit(Account.FieldName.FULL_NAME) + 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()); } - if (!realm.allowsEdit(Account.FieldName.USER_NAME) + if (!realm.allowsEdit(AccountFieldName.USER_NAME) && who.getUserName() != null && !eq(user.getUserName(), who.getUserName())) { log.warn(String.format("Not changing already set username %s to %s", @@ -340,7 +341,7 @@ } else { log.error(errorMessage); } - if (!realm.allowsEdit(Account.FieldName.USER_NAME)) { + if (!realm.allowsEdit(AccountFieldName.USER_NAME)) { // setting the given user name has failed, but the realm does not // allow the user to manually set a user name, // this means we would end with an account without user name
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 c985859..a864dab 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
@@ -14,8 +14,11 @@ package com.google.gerrit.server.account; +import static com.google.gerrit.extensions.client.AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT; + import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.extensions.api.accounts.EmailInput; +import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; @@ -23,8 +26,6 @@ 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.Account.FieldName; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.GetEmails.EmailInfo; @@ -50,11 +51,11 @@ private final Provider<CurrentUser> self; private final Realm realm; - private final AuthConfig authConfig; private final AccountManager accountManager; private final RegisterNewEmailSender.Factory registerNewEmailFactory; private final PutPreferred putPreferred; private final String email; + private final boolean isDevMode; @Inject CreateEmail(Provider<CurrentUser> self, @@ -66,11 +67,11 @@ @Assisted String email) { this.self = self; this.realm = realm; - this.authConfig = authConfig; this.accountManager = accountManager; this.registerNewEmailFactory = registerNewEmailFactory; this.putPreferred = putPreferred; this.email = email; + this.isDevMode = authConfig.getAuthType() == DEVELOPMENT_BECOME_ANY_ACCOUNT; } @Override @@ -96,7 +97,7 @@ throw new AuthException("not allowed to use no_confirmation"); } - if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) { + if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) { throw new MethodNotAllowedException("realm does not allow adding emails"); } @@ -113,8 +114,10 @@ EmailInfo info = new EmailInfo(); info.email = email; - if (input.noConfirmation - || authConfig.getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) { + if (input.noConfirmation || isDevMode) { + if (isDevMode) { + log.warn("skipping email validation in developer mode"); + } try { accountManager.link(user.getAccountId(), AuthRequest.forEmail(email));
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 eb3c9a0..57af333 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
@@ -15,8 +15,9 @@ package com.google.gerrit.server.account; import com.google.common.base.Strings; +import com.google.gerrit.extensions.client.AccountFieldName; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.server.config.AuthConfig; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -39,7 +40,7 @@ } @Override - public boolean allowsEdit(final Account.FieldName field) { + public boolean allowsEdit(final AccountFieldName field) { if (authConfig.getAuthType() == AuthType.HTTP) { switch (field) { case USER_NAME:
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 f6c48af..94c099e 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
@@ -16,11 +16,14 @@ import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.extensions.annotations.RequiresCapability; +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.RestApiException; import com.google.gerrit.extensions.restapi.RestModifyView; 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.DeleteActive.Input; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; @@ -38,22 +41,28 @@ private final Provider<ReviewDb> dbProvider; private final AccountCache byIdCache; + private final Provider<IdentifiedUser> self; @Inject - DeleteActive(Provider<ReviewDb> dbProvider, AccountCache byIdCache) { + DeleteActive(Provider<ReviewDb> dbProvider, AccountCache byIdCache, + Provider<IdentifiedUser> self) { this.dbProvider = dbProvider; this.byIdCache = byIdCache; + this.self = self; } @Override public Response<?> apply(AccountResource rsrc, Input input) - throws ResourceNotFoundException, OrmException, IOException { + throws RestApiException, OrmException, IOException { Account a = dbProvider.get().accounts().get(rsrc.getUser().getAccountId()); if (a == null) { throw new ResourceNotFoundException("account not found"); } if (!a.isActive()) { - throw new ResourceNotFoundException(); + throw new ResourceConflictException("account not active"); + } + if (self.get() == rsrc.getUser()) { + throw new ResourceConflictException("cannot deactivate own account"); } a.setActive(false); dbProvider.get().accounts().update(Collections.singleton(a));
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 76f63b7..1f073ae 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
@@ -14,13 +14,13 @@ package com.google.gerrit.server.account; +import com.google.gerrit.extensions.client.AccountFieldName; 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.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestModifyView; -import com.google.gerrit.reviewdb.client.Account.FieldName; import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; @@ -67,7 +67,7 @@ public Response<?> apply(IdentifiedUser user, String email) throws ResourceNotFoundException, ResourceConflictException, MethodNotAllowedException, OrmException, IOException { - if (!realm.allowsEdit(FieldName.REGISTER_NEW_EMAIL)) { + if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) { throw new MethodNotAllowedException("realm does not allow deleting emails"); } AccountExternalId.Key key = new AccountExternalId.Key(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java index d3b938f..a53f64e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/FakeRealm.java
@@ -14,13 +14,13 @@ package com.google.gerrit.server.account; +import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.Account.FieldName; /** Fake implementation of {@link Realm} that does not communicate. */ public class FakeRealm extends AbstractRealm { @Override - public boolean allowsEdit(FieldName field) { + public boolean allowsEdit(AccountFieldName field) { return false; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java index 10b6df9..9864b45 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetActive.java
@@ -14,7 +14,6 @@ package com.google.gerrit.server.account; -import com.google.gerrit.extensions.restapi.BinaryResult; import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.inject.Singleton; @@ -22,9 +21,9 @@ @Singleton public class GetActive implements RestReadView<AccountResource> { @Override - public Object apply(AccountResource rsrc) { + public Response<String> apply(AccountResource rsrc) { if (rsrc.getUser().getAccount().isActive()) { - return BinaryResult.create("ok\n"); + return Response.ok("ok"); } return Response.none(); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java index 9e1201a..46d6f11 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAgreements.java
@@ -25,6 +25,7 @@ import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.config.AgreementJson; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.project.ProjectCache; import com.google.inject.Inject; @@ -46,14 +47,17 @@ private final Provider<CurrentUser> self; private final ProjectCache projectCache; + private final AgreementJson agreementJson; private final boolean agreementsEnabled; @Inject GetAgreements(Provider<CurrentUser> self, ProjectCache projectCache, + AgreementJson agreementJson, @GerritServerConfig Config config) { this.self = self; this.projectCache = projectCache; + this.agreementJson = agreementJson; this.agreementsEnabled = config.getBoolean("auth", "contributorAgreements", false); } @@ -85,17 +89,13 @@ groupIds.add(rule.getGroup().getUUID()); } else { log.warn("group \"" + rule.getGroup().getName() + "\" does not " + - " exist, referenced in CLA \"" + ca.getName() + "\""); + "exist, referenced in CLA \"" + ca.getName() + "\""); } } } if (user.getEffectiveGroups().containsAnyOf(groupIds)) { - AgreementInfo info = new AgreementInfo(); - info.name = ca.getName(); - info.description = ca.getDescription(); - info.url = ca.getAgreementUrl(); - results.add(info); + results.add(agreementJson.format(ca)); } } return results;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java index 9197011..239b954 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAccount.java
@@ -15,7 +15,9 @@ package com.google.gerrit.server.account; import com.google.gerrit.extensions.api.accounts.AccountInput; +import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.inject.Singleton; @@ -23,7 +25,7 @@ public class PutAccount implements RestModifyView<AccountResource, AccountInput> { @Override - public Object apply(AccountResource resource, AccountInput input) + public Response<AccountInfo> apply(AccountResource resource, AccountInput input) throws ResourceConflictException { throw new ResourceConflictException("account exists"); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAgreement.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAgreement.java index 2fdf666..b8b902f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAgreement.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutAgreement.java
@@ -22,6 +22,7 @@ import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; 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; @@ -68,7 +69,7 @@ } @Override - public Object apply(AccountResource resource, AgreementInput input) + public Response<String> apply(AccountResource resource, AgreementInput input) throws IOException, OrmException, RestApiException { if (!agreementsEnabled) { throw new MethodNotAllowedException("contributor agreements disabled"); @@ -103,7 +104,7 @@ addMembers.addMembers(group.getId(), ImmutableList.of(account.getId())); agreementSignup.fire(account, agreementName); - return agreementName; + return Response.ok(agreementName); } }
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 e0b69a6..74c07e8 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
@@ -15,6 +15,7 @@ package com.google.gerrit.server.account; import com.google.common.base.Strings; +import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.DefaultInput; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; @@ -22,7 +23,6 @@ import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.Account.FieldName; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.IdentifiedUser; @@ -74,7 +74,7 @@ input = new Input(); } - if (!realm.allowsEdit(FieldName.FULL_NAME)) { + if (!realm.allowsEdit(AccountFieldName.FULL_NAME)) { throw new MethodNotAllowedException("realm does not allow editing name"); }
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 e9dc393..29168ed 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
@@ -15,13 +15,13 @@ package com.google.gerrit.server.account; import com.google.gerrit.common.errors.NameAlreadyUsedException; +import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.DefaultInput; import com.google.gerrit.extensions.restapi.MethodNotAllowedException; 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.client.Account; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.account.PutUsername.Input; @@ -64,7 +64,7 @@ throw new AuthException("not allowed to set username"); } - if (!realm.allowsEdit(Account.FieldName.USER_NAME)) { + if (!realm.allowsEdit(AccountFieldName.USER_NAME)) { throw new MethodNotAllowedException("realm does not allow editing username"); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java index 85fde4e..627f529 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.account; +import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.server.IdentifiedUser; @@ -21,10 +22,10 @@ public interface Realm { /** Can the end-user modify this field of their own account? */ - boolean allowsEdit(Account.FieldName field); + boolean allowsEdit(AccountFieldName field); /** Returns the account fields that the end-user can modify. */ - Set<Account.FieldName> getEditableFields(); + Set<AccountFieldName> getEditableFields(); AuthRequest authenticate(AuthRequest who) throws AccountException;
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 2af9f1d..3533fe8 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,6 +14,8 @@ package com.google.gerrit.server.api.accounts; +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; @@ -31,6 +33,7 @@ import com.google.gerrit.extensions.common.GpgKeyInfo; import com.google.gerrit.extensions.common.SshKeyInfo; import com.google.gerrit.extensions.restapi.IdString; +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; @@ -38,8 +41,10 @@ import com.google.gerrit.server.account.AccountResource; import com.google.gerrit.server.account.AddSshKey; import com.google.gerrit.server.account.CreateEmail; +import com.google.gerrit.server.account.DeleteActive; import com.google.gerrit.server.account.DeleteSshKey; import com.google.gerrit.server.account.DeleteWatchedProjects; +import com.google.gerrit.server.account.GetActive; import com.google.gerrit.server.account.GetAgreements; import com.google.gerrit.server.account.GetAvatar; import com.google.gerrit.server.account.GetDiffPreferences; @@ -48,6 +53,7 @@ import com.google.gerrit.server.account.GetSshKeys; import com.google.gerrit.server.account.GetWatchedProjects; import com.google.gerrit.server.account.PostWatchedProjects; +import com.google.gerrit.server.account.PutActive; import com.google.gerrit.server.account.PutAgreement; import com.google.gerrit.server.account.SetDiffPreferences; import com.google.gerrit.server.account.SetEditPreferences; @@ -99,6 +105,9 @@ private final SshKeys sshKeys; private final GetAgreements getAgreements; private final PutAgreement putAgreement; + private final GetActive getActive; + private final PutActive putActive; + private final DeleteActive deleteActive; @Inject AccountApiImpl(AccountLoader.Factory ailf, @@ -126,6 +135,9 @@ SshKeys sshKeys, GetAgreements getAgreements, PutAgreement putAgreement, + GetActive getActive, + PutActive putActive, + DeleteActive deleteActive, @Assisted AccountResource account) { this.account = account; this.accountLoaderFactory = ailf; @@ -153,6 +165,9 @@ this.gpgApiAdapter = gpgApiAdapter; this.getAgreements = getAgreements; this.putAgreement = putAgreement; + this.getActive = getActive; + this.putActive = putActive; + this.deleteActive = deleteActive; } @Override @@ -169,6 +184,25 @@ } @Override + public boolean getActive() throws RestApiException { + Response<String> result = getActive.apply(account); + return result.statusCode() == SC_OK && result.value().equals("ok"); + } + + @Override + public void setActive(boolean active) throws RestApiException { + try { + if (active) { + putActive.apply(account, new PutActive.Input()); + } else { + deleteActive.apply(account, new DeleteActive.Input()); + } + } catch (OrmException | IOException e) { + throw new RestApiException("Cannot set active", e); + } + } + + @Override public String getAvatarUrl(int size) throws RestApiException { getAvatar.setSize(size); return getAvatar.apply(account).location();
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 a18c575..afda5fa 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,7 @@ package com.google.gerrit.server.api.changes; +import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; import com.google.gerrit.extensions.api.changes.DeleteVoteInput; import com.google.gerrit.extensions.api.changes.ReviewerApi; import com.google.gerrit.extensions.restapi.RestApiException; @@ -79,8 +80,13 @@ @Override public void remove() throws RestApiException { + remove(new DeleteReviewerInput()); + } + + @Override + public void remove(DeleteReviewerInput input) throws RestApiException { try { - deleteReviewer.apply(reviewer, null); + deleteReviewer.apply(reviewer, input); } catch (UpdateException e) { throw new RestApiException("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 6b5e83c..2c99318 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
@@ -30,6 +30,7 @@ import com.google.gerrit.extensions.client.SubmitType; 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.FileInfo; import com.google.gerrit.extensions.common.MergeableInfo; import com.google.gerrit.extensions.common.TestSubmitRuleInput; @@ -44,6 +45,7 @@ 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.GetMergeList; import com.google.gerrit.server.change.GetPatch; import com.google.gerrit.server.change.GetRevisionActions; import com.google.gerrit.server.change.ListRevisionComments; @@ -59,9 +61,11 @@ import com.google.gerrit.server.change.TestSubmitType; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.UpdateException; +import com.google.gerrit.server.patch.PatchListNotAvailableException; import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; +import com.google.inject.Provider; import com.google.inject.assistedinject.Assisted; import org.eclipse.jgit.lib.Repository; @@ -104,6 +108,7 @@ private final GetRevisionActions revisionActions; private final TestSubmitType testSubmitType; private final TestSubmitType.Get getSubmitType; + private final Provider<GetMergeList> getMergeList; @Inject RevisionApiImpl(GitRepositoryManager repoManager, @@ -132,6 +137,7 @@ GetRevisionActions revisionActions, TestSubmitType testSubmitType, TestSubmitType.Get getSubmitType, + Provider<GetMergeList> getMergeList, @Assisted RevisionResource r) { this.repoManager = repoManager; this.changes = changes; @@ -159,6 +165,7 @@ this.revisionActions = revisionActions; this.testSubmitType = testSubmitType; this.getSubmitType = getSubmitType; + this.getMergeList = getMergeList; this.revision = r; } @@ -264,7 +271,7 @@ return ImmutableSet.copyOf((Iterable<String>) listFiles .setReviewed(true) .apply(revision).value()); - } catch (OrmException | IOException e) { + } catch (OrmException | IOException | PatchListNotAvailableException e) { throw new RestApiException("Cannot list reviewed files", e); } } @@ -293,7 +300,7 @@ public Map<String, FileInfo> files() throws RestApiException { try { return (Map<String, FileInfo>)listFiles.apply(revision).value(); - } catch (OrmException | IOException e) { + } catch (OrmException | IOException | PatchListNotAvailableException e) { throw new RestApiException("Cannot retrieve files", e); } } @@ -304,7 +311,7 @@ try { return (Map<String, FileInfo>) listFiles.setBase(base) .apply(revision).value(); - } catch (OrmException | IOException e) { + } catch (OrmException | IOException | PatchListNotAvailableException e) { throw new RestApiException("Cannot retrieve files", e); } } @@ -315,7 +322,7 @@ try { return (Map<String, FileInfo>) listFiles.setParent(parentNum) .apply(revision).value(); - } catch (OrmException | IOException e) { + } catch (OrmException | IOException | PatchListNotAvailableException e) { throw new RestApiException("Cannot retrieve files", e); } } @@ -427,4 +434,21 @@ throw new RestApiException("Cannot test submit type", e); } } + + @Override + public MergeListRequest getMergeList() throws RestApiException { + return new MergeListRequest() { + @Override + public List<CommitInfo> get() throws RestApiException { + try { + GetMergeList gml = getMergeList.get(); + gml.setUninterestingParent(getUninterestingParent()); + gml.setAddLinks(getAddLinks()); + return gml.apply(revision).value(); + } catch (IOException e) { + throw new RestApiException("Cannot get merge list", 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 8339ecf..f433d2b 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
@@ -18,10 +18,12 @@ 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.ConfigResource; import com.google.gerrit.server.config.GetDiffPreferences; import com.google.gerrit.server.config.GetPreferences; +import com.google.gerrit.server.config.GetServerInfo; import com.google.gerrit.server.config.SetDiffPreferences; import com.google.gerrit.server.config.SetPreferences; import com.google.inject.Inject; @@ -37,16 +39,19 @@ private final SetPreferences setPreferences; private final GetDiffPreferences getDiffPreferences; private final SetDiffPreferences setDiffPreferences; + private final GetServerInfo getServerInfo; @Inject ServerImpl(GetPreferences getPreferences, SetPreferences setPreferences, GetDiffPreferences getDiffPreferences, - SetDiffPreferences setDiffPreferences) { + SetDiffPreferences setDiffPreferences, + GetServerInfo getServerInfo) { this.getPreferences = getPreferences; this.setPreferences = setPreferences; this.getDiffPreferences = getDiffPreferences; this.setDiffPreferences = setDiffPreferences; + this.getServerInfo = getServerInfo; } @Override @@ -55,6 +60,15 @@ } @Override + public ServerInfo getInfo() throws RestApiException { + try { + return getServerInfo.apply(new ConfigResource()); + } catch (IOException e) { + throw new RestApiException("Cannot get server info", e); + } + } + + @Override public GeneralPreferencesInfo getDefaultPreferences() throws RestApiException { try {
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 7f5f2d2..4cb96b3 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
@@ -14,8 +14,8 @@ package com.google.gerrit.server.args4j; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java index 8dc7177..3dddf4d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
@@ -14,7 +14,7 @@ package com.google.gerrit.server.auth.ldap; -import com.google.gerrit.reviewdb.client.AuthType; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.auth.AuthBackend; import com.google.gerrit.server.auth.AuthException;
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 30b08a6..603efe0 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
@@ -21,10 +21,11 @@ import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.gerrit.common.data.ParameterizedString; +import com.google.gerrit.extensions.client.AccountFieldName; +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.reviewdb.client.AccountGroup; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.account.AbstractRealm; import com.google.gerrit.server.account.AccountException; @@ -67,7 +68,7 @@ private final AuthConfig authConfig; private final EmailExpander emailExpander; private final LoadingCache<String, Optional<Account.Id>> usernameCache; - private final Set<Account.FieldName> readOnlyAccountFields; + private final Set<AccountFieldName> readOnlyAccountFields; private final boolean fetchMemberOfEagerly; private final Config config; @@ -91,13 +92,13 @@ this.readOnlyAccountFields = new HashSet<>(); if (optdef(config, "accountFullName", "DEFAULT") != null) { - readOnlyAccountFields.add(Account.FieldName.FULL_NAME); + readOnlyAccountFields.add(AccountFieldName.FULL_NAME); } if (optdef(config, "accountSshUserName", "DEFAULT") != null) { - readOnlyAccountFields.add(Account.FieldName.USER_NAME); + readOnlyAccountFields.add(AccountFieldName.USER_NAME); } if (!authConfig.isAllowRegisterNewEmail()) { - readOnlyAccountFields.add(Account.FieldName.REGISTER_NEW_EMAIL); + readOnlyAccountFields.add(AccountFieldName.REGISTER_NEW_EMAIL); } fetchMemberOfEagerly = optional(config, "fetchMemberOfEagerly", true); @@ -196,7 +197,7 @@ } @Override - public boolean allowsEdit(final Account.FieldName field) { + public boolean allowsEdit(final AccountFieldName field) { return !readOnlyAccountFields.contains(field); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java index cf9000d..94a3ac2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/oauth/OAuthRealm.java
@@ -17,9 +17,9 @@ import com.google.common.base.Strings; import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider; import com.google.gerrit.extensions.auth.oauth.OAuthUserInfo; +import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.Account.FieldName; import com.google.gerrit.server.account.AbstractRealm; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager; @@ -37,7 +37,7 @@ @Singleton public class OAuthRealm extends AbstractRealm { private final DynamicMap<OAuthLoginProvider> loginProviders; - private final Set<FieldName> editableAccountFields; + private final Set<AccountFieldName> editableAccountFields; @Inject OAuthRealm(DynamicMap<OAuthLoginProvider> loginProviders, @@ -45,15 +45,15 @@ this.loginProviders = loginProviders; this.editableAccountFields = new HashSet<>(); if (config.getBoolean("oauth", null, "allowEditFullName", false)) { - editableAccountFields.add(FieldName.FULL_NAME); + editableAccountFields.add(AccountFieldName.FULL_NAME); } if (config.getBoolean("oauth", null, "allowRegisterNewEmail", false)) { - editableAccountFields.add(FieldName.REGISTER_NEW_EMAIL); + editableAccountFields.add(AccountFieldName.REGISTER_NEW_EMAIL); } } @Override - public boolean allowsEdit(FieldName field) { + public boolean allowsEdit(AccountFieldName field) { return editableAccountFields.contains(field); } @@ -105,12 +105,12 @@ } if (!Strings.isNullOrEmpty(userInfo.getEmailAddress()) && (Strings.isNullOrEmpty(who.getUserName()) - || !allowsEdit(FieldName.REGISTER_NEW_EMAIL))) { + || !allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL))) { who.setEmailAddress(userInfo.getEmailAddress()); } if (!Strings.isNullOrEmpty(userInfo.getDisplayName()) && (Strings.isNullOrEmpty(who.getDisplayName()) - || !allowsEdit(FieldName.FULL_NAME))) { + || !allowsEdit(AccountFieldName.FULL_NAME))) { who.setDisplayName(userInfo.getDisplayName()); } return who;
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 adbcf22..c4bd68d 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
@@ -28,6 +28,7 @@ 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.Project; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.ChangeUtil; @@ -50,6 +51,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Collection; + @Singleton public class Abandon implements RestModifyView<ChangeResource, AbandonInput>, UiAction<ChangeResource> { @@ -91,6 +94,11 @@ return json.create(ChangeJson.NO_OPTIONS).format(change); } + public Change abandon(ChangeControl control) + throws RestApiException, UpdateException { + return abandon(control, "", NotifyHandling.ALL); + } + public Change abandon(ChangeControl control, String msgTxt) throws RestApiException, UpdateException { return abandon(control, msgTxt, NotifyHandling.ALL); @@ -98,31 +106,76 @@ public Change abandon(ChangeControl control, String msgTxt, NotifyHandling notifyHandling) throws RestApiException, UpdateException { - CurrentUser user = control.getUser(); - Account account = user.isIdentifiedUser() - ? user.asIdentifiedUser().getAccount() - : null; - Op op = new Op(msgTxt, account, notifyHandling); - try (BatchUpdate u = batchUpdateFactory.create(dbProvider.get(), - control.getProject().getNameKey(), user, TimeUtil.nowTs())) { + Op op = new Op(control.getUser(), msgTxt, notifyHandling); + try (BatchUpdate u = + batchUpdateFactory.create( + dbProvider.get(), + control.getProject().getNameKey(), + control.getUser(), + TimeUtil.nowTs())) { u.addOp(control.getId(), op).execute(); } return op.change; } + /** + * If an extension has more than one changes to abandon that belong to the + * same project, they should use the batch instead of abandoning one by one. + * <p> + * It's the caller's responsibility to ensure that all jobs inside the same + * batch have the matching project from its ChangeControl. Violations will + * result in a ResourceConflictException. + */ + public void batchAbandon(Project.NameKey project, CurrentUser user, + Collection<ChangeControl> controls, String msgTxt, + NotifyHandling notifyHandling) throws RestApiException, UpdateException { + if (controls.isEmpty()) { + return; + } + try (BatchUpdate u = batchUpdateFactory.create( + dbProvider.get(), project, user, TimeUtil.nowTs())) { + for (ChangeControl control : controls) { + if (!project.equals(control.getProject().getNameKey())) { + throw new ResourceConflictException( + String.format( + "Project name \"%s\" doesn't match \"%s\"", + control.getProject().getNameKey().get(), + project.get())); + } + u.addOp( + control.getId(), new Op(control.getUser(), msgTxt, notifyHandling)); + } + u.execute(); + } + } + + public void batchAbandon(Project.NameKey project, CurrentUser user, + Collection<ChangeControl> controls, String msgTxt) + throws RestApiException, UpdateException { + batchAbandon(project, user, controls, msgTxt, NotifyHandling.ALL); + } + + public void batchAbandon(Project.NameKey project, CurrentUser user, + Collection<ChangeControl> controls) + throws RestApiException, UpdateException { + batchAbandon(project, user, controls, "", NotifyHandling.ALL); + } + private class Op extends BatchUpdate.Op { - private final Account account; private final String msgTxt; + private final NotifyHandling notifyHandling; + private final Account account; private Change change; private PatchSet patchSet; private ChangeMessage message; - private NotifyHandling notifyHandling; - private Op(String msgTxt, Account account, NotifyHandling notifyHandling) { - this.account = account; + private Op(CurrentUser user, String msgTxt, NotifyHandling notifyHandling) { this.msgTxt = msgTxt; this.notifyHandling = notifyHandling; + account = user.isIdentifiedUser() + ? user.asIdentifiedUser().getAccount() + : null; } @Override
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 60d9c08..205d959 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
@@ -14,7 +14,9 @@ package com.google.gerrit.server.change; -import com.google.gerrit.extensions.restapi.ResourceConflictException; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.collect.Multimap; +import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.InternalUser; import com.google.gerrit.server.config.ChangeCleanupConfig; import com.google.gerrit.server.project.ChangeControl; @@ -29,6 +31,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; @@ -37,10 +40,10 @@ private static final Logger log = LoggerFactory.getLogger(AbandonUtil.class); private final ChangeCleanupConfig cfg; - private final InternalUser.Factory internalUserFactory; private final ChangeQueryProcessor queryProcessor; private final ChangeQueryBuilder queryBuilder; private final Abandon abandon; + private final InternalUser internalUser; @Inject AbandonUtil( @@ -50,10 +53,10 @@ ChangeQueryBuilder queryBuilder, Abandon abandon) { this.cfg = cfg; - this.internalUserFactory = internalUserFactory; this.queryProcessor = queryProcessor; this.queryBuilder = queryBuilder; this.abandon = abandon; + internalUser = internalUserFactory.create(); } public void abandonInactiveOpenChanges() { @@ -68,42 +71,42 @@ if (!cfg.getAbandonIfMergeable()) { query += " -is:mergeable"; } - List<ChangeData> changesToAbandon = queryProcessor.enforceVisibility(false) - .query(queryBuilder.parse(query)).entities(); - int count = 0; + + List<ChangeData> changesToAbandon = + queryProcessor + .enforceVisibility(false) + .query(queryBuilder.parse(query)) + .entities(); + ImmutableMultimap.Builder<Project.NameKey, ChangeControl> builder = + ImmutableMultimap.builder(); for (ChangeData cd : changesToAbandon) { + ChangeControl control = cd.changeControl(internalUser); + builder.put(control.getProject().getNameKey(), control); + } + + int count = 0; + Multimap<Project.NameKey, ChangeControl> abandons = builder.build(); + String message = cfg.getAbandonMessage(); + for (Project.NameKey project : abandons.keySet()) { + Collection<ChangeControl> changes = abandons.get(project); try { - if (noNeedToAbandon(cd, query)){ - log.debug("Change data \"{}\" does not satisfy the query \"{}\" any" - + " more, hence skipping it in clean up", cd, query); - continue; - } - abandon.abandon(changeControl(cd), cfg.getAbandonMessage()); - count++; - } catch (ResourceConflictException e) { - // Change was already merged or abandoned. + abandon.batchAbandon(project, internalUser, changes, message); + count += changes.size(); } catch (Throwable e) { - log.error(String.format( - "Failed to auto-abandon inactive open change %d.", - cd.getId().get()), e); + StringBuilder msg = + new StringBuilder("Failed to auto-abandon inactive change(s):"); + for (ChangeControl change : changes) { + msg.append(" ").append(change.getId().get()); + } + msg.append("."); + log.error(msg.toString(), e); } } log.info(String.format("Auto-Abandoned %d of %d changes.", count, changesToAbandon.size())); } catch (QueryParseException | OrmException e) { - log.error("Failed to query inactive open changes for auto-abandoning.", e); + log.error( + "Failed to query inactive open changes for auto-abandoning.", e); } } - - private boolean noNeedToAbandon(ChangeData cd, String query) - throws OrmException, QueryParseException { - String newQuery = query + " change:" + cd.getId(); - List<ChangeData> changesToAbandon = queryProcessor.enforceVisibility(false) - .query(queryBuilder.parse(newQuery)).entities(); - return changesToAbandon.isEmpty(); - } - - private ChangeControl changeControl(ChangeData cd) throws OrmException { - return cd.changeControl(internalUserFactory.create()); - } }
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 6333809..a3c992f 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
@@ -19,6 +19,8 @@ import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID; import com.google.common.base.MoreObjects; +import com.google.common.base.Predicate; +import com.google.common.collect.Sets; import com.google.gerrit.common.FooterConstants; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.LabelTypes; @@ -34,11 +36,11 @@ import com.google.gerrit.server.ApprovalsUtil; 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.events.CommitReceivedEvent; import com.google.gerrit.server.extensions.events.CommentAdded; import com.google.gerrit.server.extensions.events.RevisionCreated; -import com.google.gerrit.server.git.BanCommit; import com.google.gerrit.server.git.BatchUpdate; import com.google.gerrit.server.git.BatchUpdate.ChangeContext; import com.google.gerrit.server.git.BatchUpdate.Context; @@ -48,6 +50,7 @@ import com.google.gerrit.server.git.validators.CommitValidationException; import com.google.gerrit.server.git.validators.CommitValidators; import com.google.gerrit.server.mail.CreateChangeSender; +import com.google.gerrit.server.notedb.ChangeNotes; import com.google.gerrit.server.notedb.ChangeUpdate; import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.project.ChangeControl; @@ -62,7 +65,6 @@ import com.google.inject.assistedinject.Assisted; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.notes.NoteMap; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.ReceiveCommand; import org.eclipse.jgit.util.ChangeIdUtil; @@ -86,6 +88,7 @@ LoggerFactory.getLogger(ChangeInserter.class); private final ProjectControl.GenericFactory projectControlFactory; + private final IdentifiedUser.GenericFactory userFactory; private final ChangeControl.GenericFactory changeControlFactory; private final PatchSetInfoFactory patchSetInfoFactory; private final PatchSetUtil psUtil; @@ -128,6 +131,7 @@ @Inject ChangeInserter(ProjectControl.GenericFactory projectControlFactory, + IdentifiedUser.GenericFactory userFactory, ChangeControl.GenericFactory changeControlFactory, PatchSetInfoFactory patchSetInfoFactory, PatchSetUtil psUtil, @@ -142,6 +146,7 @@ @Assisted RevCommit commit, @Assisted String refName) { this.projectControlFactory = projectControlFactory; + this.userFactory = userFactory; this.changeControlFactory = changeControlFactory; this.patchSetInfoFactory = patchSetInfoFactory; this.psUtil = psUtil; @@ -353,8 +358,10 @@ update.fixStatus(change.getStatus()); LabelTypes labelTypes = ctl.getProjectControl().getLabelTypes(); - approvalsUtil.addReviewers(db, update, labelTypes, change, - patchSet, patchSetInfo, reviewers, Collections.<Account.Id> emptySet()); + approvalsUtil.addReviewers(db, update, labelTypes, change, patchSet, + patchSetInfo, + filterOnChangeVisibility(db, ctx.getNotes(), reviewers), + Collections.<Account.Id> emptySet()); approvalsUtil.addApprovals(db, update, labelTypes, patchSet, ctx.getControl(), approvals); if (message != null) { @@ -368,6 +375,25 @@ return true; } + private Set<Account.Id> filterOnChangeVisibility(final ReviewDb db, + final ChangeNotes notes, Set<Account.Id> accounts) { + return Sets.filter(accounts, new Predicate<Account.Id>() { + @Override + public boolean apply(Account.Id accountId) { + try { + IdentifiedUser user = userFactory.create(accountId); + return changeControlFactory.controlFor(notes, user).isVisible(db); + } catch (OrmException | NoSuchChangeException e) { + log.warn( + String.format("Failed to check if account %d can see change %d", + accountId.get(), notes.getChangeId().get()), + e); + return false; + } + } + }); + } + @Override public void postUpdate(Context ctx) throws OrmException, NoSuchChangeException { if (sendMail) { @@ -440,9 +466,6 @@ try { RefControl refControl = projectControlFactory .controlFor(ctx.getProject(), ctx.getUser()).controlForRef(refName); - CommitValidators cv = commitValidatorsFactory.create( - refControl, new NoSshInfo(), ctx.getRepository()); - String refName = psId.toRefName(); CommitReceivedEvent event = new CommitReceivedEvent( new ReceiveCommand( @@ -453,19 +476,10 @@ change.getDest().get(), commit, ctx.getIdentifiedUser()); - - switch (validatePolicy) { - case RECEIVE_COMMITS: - NoteMap rejectCommits = BanCommit.loadRejectCommitsMap( - ctx.getRepository(), ctx.getRevWalk()); - cv.validateForReceiveCommits(event, rejectCommits); - break; - case GERRIT: - cv.validateForGerritCommits(event); - break; - case NONE: - break; - } + commitValidatorsFactory + .create( + validatePolicy, refControl, new NoSshInfo(), ctx.getRepository()) + .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 07714dc..e3a56b6 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
@@ -663,7 +663,7 @@ // - They are an explicit reviewer. // - They ever voted on this change. Set<Account.Id> allUsers = new HashSet<>(); - allUsers.addAll(cd.reviewers().all()); + allUsers.addAll(cd.reviewers().byState(ReviewerStateInternal.REVIEWER)); for (PatchSetApproval psa : cd.approvals().values()) { allUsers.add(psa.getAccountId()); } @@ -926,12 +926,19 @@ .toSortedList(AccountInfoComparator.ORDER_NULLS_FIRST); } + @Nullable + private Repository openRepoIfNecessary(ChangeControl ctl) throws IOException { + if (has(ALL_COMMITS) || has(CURRENT_COMMIT) || has(COMMIT_FOOTERS)) { + return repoManager.openRepository(ctl.getProject().getNameKey()); + } + return null; + } + private Map<String, RevisionInfo> revisions(ChangeControl ctl, ChangeData cd, Map<PatchSet.Id, PatchSet> map) throws PatchListNotAvailableException, GpgException, OrmException, IOException { Map<String, RevisionInfo> res = new LinkedHashMap<>(); - try (Repository repo = - repoManager.openRepository(ctl.getProject().getNameKey())) { + try (Repository repo = openRepoIfNecessary(ctl)) { for (PatchSet in : map.values()) { if ((has(ALL_REVISIONS) || in.getId().equals(ctl.getChange().currentPatchSetId())) @@ -975,8 +982,7 @@ throws PatchListNotAvailableException, GpgException, OrmException, IOException { accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS)); - try (Repository repo = - repoManager.openRepository(ctl.getProject().getNameKey())) { + try (Repository repo = openRepoIfNecessary(ctl)) { RevisionInfo rev = toRevisionInfo( ctl, changeDataFactory.create(db.get(), ctl), in, repo, true); accountLoader.fill(); @@ -985,7 +991,7 @@ } private RevisionInfo toRevisionInfo(ChangeControl ctl, ChangeData cd, - PatchSet in, Repository repo, boolean fillCommit) + PatchSet in, @Nullable Repository repo, boolean fillCommit) throws PatchListNotAvailableException, GpgException, OrmException, IOException { Change c = ctl.getChange();
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 2302b70..f0075ee 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
@@ -14,6 +14,7 @@ package com.google.gerrit.server.change; +import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.client.ChangeKind; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.PatchSet; @@ -31,10 +32,11 @@ * implementation changes, which might invalidate old entries). */ public interface ChangeKindCache { - ChangeKind getChangeKind(ProjectState project, Repository repo, + ChangeKind getChangeKind(ProjectState project, @Nullable Repository repo, ObjectId prior, ObjectId next); ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch); - ChangeKind getChangeKind(Repository repo, ChangeData cd, PatchSet patch); + ChangeKind getChangeKind(@Nullable Repository repo, 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 edc1b12..b23bcf8 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
@@ -22,9 +22,11 @@ import com.google.common.cache.Cache; import com.google.common.cache.Weigher; import com.google.common.collect.FluentIterable; +import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.client.ChangeKind; 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.cache.CacheModule; import com.google.gerrit.server.config.GerritServerConfig; @@ -100,11 +102,13 @@ } @Override - public ChangeKind getChangeKind(ProjectState project, Repository repo, - ObjectId prior, ObjectId next) { + public ChangeKind getChangeKind(ProjectState project, + @Nullable Repository repo, ObjectId prior, ObjectId next) { try { Key key = new Key(prior, next, useRecursiveMerge); - return new Loader(key, repo).call(); + return new Loader( + key, repoManager, project.getProject().getNameKey(), repo) + .call(); } catch (IOException e) { log.warn("Cannot check trivial rebase of new patch set " + next.name() + " in " + project.getProject().getName(), e); @@ -120,7 +124,7 @@ } @Override - public ChangeKind getChangeKind(Repository repo, ChangeData cd, + public ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd, PatchSet patch) { return getChangeKindInternal(this, repo, cd, patch, projectCache); } @@ -191,11 +195,16 @@ private static class Loader implements Callable<ChangeKind> { private final Key key; - private final Repository repo; + private final GitRepositoryManager repoManager; + private final Project.NameKey projectName; + private final Repository alreadyOpenRepo; - private Loader(Key key, Repository repo) { + private Loader(Key key, GitRepositoryManager repoManager, + Project.NameKey projectName, @Nullable Repository alreadyOpenRepo) { this.key = key; - this.repo = repo; + this.repoManager = repoManager; + this.projectName = projectName; + this.alreadyOpenRepo = alreadyOpenRepo; } @Override @@ -204,6 +213,12 @@ return ChangeKind.NO_CODE_CHANGE; } + Repository repo = alreadyOpenRepo; + boolean close = false; + if (repo == null) { + repo = repoManager.openRepository(projectName); + close = true; + } try (RevWalk walk = new RevWalk(repo)) { RevCommit prior = walk.parseCommit(key.prior); walk.parseBody(prior); @@ -246,6 +261,10 @@ // it was a rework. } return ChangeKind.REWORK; + } finally { + if (close) { + repo.close(); + } } } @@ -321,11 +340,14 @@ } @Override - public ChangeKind getChangeKind(ProjectState project, Repository repo, - ObjectId prior, ObjectId next) { + public ChangeKind getChangeKind(ProjectState project, + @Nullable Repository repo, ObjectId prior, ObjectId next) { try { Key key = new Key(prior, next, useRecursiveMerge); - return cache.get(key, new Loader(key, repo)); + return cache.get( + key, + new Loader( + key, repoManager, project.getProject().getNameKey(), repo)); } catch (ExecutionException e) { log.warn("Cannot check trivial rebase of new patch set " + next.name() + " in " + project.getProject().getName(), e); @@ -340,14 +362,14 @@ } @Override - public ChangeKind getChangeKind(Repository repo, ChangeData cd, + public ChangeKind getChangeKind(@Nullable Repository repo, ChangeData cd, PatchSet patch) { return getChangeKindInternal(this, repo, cd, patch, projectCache); } private static ChangeKind getChangeKindInternal( ChangeKindCache cache, - Repository repo, + @Nullable Repository repo, ChangeData change, PatchSet patch, ProjectCache projectCache) {
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 db18ba2..0c620e2 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
@@ -17,6 +17,7 @@ import com.google.common.base.Strings; 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.restapi.MergeConflictException; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.reviewdb.client.Branch; @@ -236,7 +237,7 @@ bu.addOp(destChange.getId(), inserter .setMessage("Uploaded patch set " + newPatchSetId.get() + ".") .setDraft(current.isDraft()) - .setSendMail(false)); + .setNotify(NotifyHandling.NONE)); return destChange.getId(); }
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 287c3ed..66ef731a 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
@@ -32,6 +32,7 @@ import com.google.gerrit.common.Nullable; import com.google.gerrit.common.TimeUtil; import com.google.gerrit.extensions.api.changes.FixInput; +import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.common.ProblemInfo; import com.google.gerrit.extensions.common.ProblemInfo.Status; import com.google.gerrit.extensions.registration.DynamicItem; @@ -530,7 +531,7 @@ bu.addOp(ctl.getId(), inserter .setValidatePolicy(CommitValidators.Policy.NONE) .setFireRevisionCreated(false) - .setSendMail(false) + .setNotify(NotifyHandling.NONE) .setAllowClosed(true) .setMessage( "Patch set for merged commit inserted by consistency checker"));
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 bdefa93..18b7023 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
@@ -20,6 +20,8 @@ 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; @@ -38,7 +40,6 @@ import com.google.gerrit.server.ChangeUtil; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PatchSetUtil; -import com.google.gerrit.server.change.DeleteReviewer.Input; import com.google.gerrit.server.extensions.events.ReviewerDeleted; import com.google.gerrit.server.git.BatchUpdate; import com.google.gerrit.server.git.BatchUpdate.ChangeContext; @@ -62,13 +63,11 @@ import java.util.Map; @Singleton -public class DeleteReviewer implements RestModifyView<ReviewerResource, Input> { +public class DeleteReviewer + implements RestModifyView<ReviewerResource, DeleteReviewerInput> { private static final Logger log = LoggerFactory .getLogger(DeleteReviewer.class); - public static class Input { - } - private final Provider<ReviewDb> dbProvider; private final ApprovalsUtil approvalsUtil; private final PatchSetUtil psUtil; @@ -104,12 +103,19 @@ } @Override - public Response<?> apply(ReviewerResource rsrc, Input input) + public Response<?> apply(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(dbProvider.get(), rsrc.getChangeResource().getProject(), rsrc.getChangeResource().getUser(), TimeUtil.nowTs())) { - Op op = new Op(rsrc.getReviewerUser().getAccount()); + Op op = new Op(rsrc.getReviewerUser().getAccount(), input); bu.addOp(rsrc.getChange().getId(), op); bu.execute(); } @@ -119,6 +125,7 @@ private class Op extends BatchUpdate.Op { private final Account reviewer; + private final DeleteReviewerInput input; ChangeMessage changeMessage; Change currChange; PatchSet currPs; @@ -126,8 +133,9 @@ Map<String, Short> newApprovals = new HashMap<>(); Map<String, Short> oldApprovals = new HashMap<>(); - Op(Account reviewerAccount) { + Op(Account reviewerAccount, DeleteReviewerInput input) { this.reviewer = reviewerAccount; + this.input = input; } @Override @@ -148,52 +156,56 @@ } StringBuilder msg = new StringBuilder(); + msg.append("Removed reviewer " + reviewer.getFullName()); + StringBuilder removedVotesMsg = new StringBuilder(); + removedVotesMsg.append(" with the following votes:\n\n"); + 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()); - if (msg.length() == 0) { - msg.append("Removed reviewer ").append(reviewer.getFullName()) - .append(" with the following votes:\n\n"); - } - msg.append("* ").append(a.getLabel()) + 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); - if (msg.length() > 0) { - changeMessage = new ChangeMessage( - new ChangeMessage.Key(currChange.getId(), - ChangeUtil.messageUUID(ctx.getDb())), - ctx.getAccountId(), ctx.getWhen(), currPs.getId()); - changeMessage.setMessage(msg.toString()); - cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage); - } + changeMessage = new ChangeMessage( + new ChangeMessage.Key(currChange.getId(), + ChangeUtil.messageUUID(ctx.getDb())), + ctx.getAccountId(), ctx.getWhen(), currPs.getId()); + changeMessage.setMessage(msg.toString()); + cmUtil.addChangeMessage(ctx.getDb(), update, changeMessage); return true; } @Override public void postUpdate(Context ctx) { - if (changeMessage == null) { - return; + if (input.notify.compareTo(NotifyHandling.NONE) > 0) { + emailReviewers(ctx.getProject(), currChange, del, changeMessage); } - - emailReviewers(ctx.getProject(), currChange, del, changeMessage); reviewerDeleted.fire(currChange, currPs, reviewer, ctx.getAccount(), changeMessage.getMessage(), newApprovals, oldApprovals, + input.notify, ctx.getWhen()); } @@ -233,30 +245,31 @@ } return Short.toString(value); } - } - private void emailReviewers(Project.NameKey projectName, Change change, - List<PatchSetApproval> dels, ChangeMessage changeMessage) { + private void emailReviewers(Project.NameKey projectName, Change change, + List<PatchSetApproval> dels, ChangeMessage changeMessage) { - // The user knows they removed themselves, don't bother emailing them. - List<Account.Id> toMail = Lists.newArrayListWithCapacity(dels.size()); - Account.Id userId = user.get().getAccountId(); - for (PatchSetApproval psa : dels) { - if (!psa.getAccountId().equals(userId)) { - toMail.add(psa.getAccountId()); + // The user knows they removed themselves, don't bother emailing them. + List<Account.Id> toMail = Lists.newArrayListWithCapacity(dels.size()); + Account.Id userId = user.get().getAccountId(); + for (PatchSetApproval psa : dels) { + if (!psa.getAccountId().equals(userId)) { + toMail.add(psa.getAccountId()); + } } - } - if (!toMail.isEmpty()) { - try { - DeleteReviewerSender cm = - deleteReviewerSenderFactory.create(projectName, change.getId()); - cm.setFrom(userId); - cm.addReviewers(toMail); - cm.setChangeMessage(changeMessage.getMessage(), - changeMessage.getWrittenOn()); - cm.send(); - } catch (Exception err) { - log.error("Cannot email update for change " + change.getId(), err); + if (!toMail.isEmpty()) { + try { + DeleteReviewerSender cm = + deleteReviewerSenderFactory.create(projectName, change.getId()); + cm.setFrom(userId); + cm.addReviewers(toMail); + cm.setChangeMessage(changeMessage.getMessage(), + changeMessage.getWrittenOn()); + cm.setNotify(input.notify); + 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 f1bdba5..e25e2292 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
@@ -14,8 +14,9 @@ package com.google.gerrit.server.change; +import static com.google.common.base.Preconditions.checkNotNull; + 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.DeleteVoteInput; import com.google.gerrit.extensions.api.changes.NotifyHandling; @@ -28,6 +29,7 @@ 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.LabelId; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.server.ReviewDb; @@ -44,6 +46,7 @@ import com.google.gerrit.server.mail.DeleteVoteSender; import com.google.gerrit.server.mail.ReplyToChangeSender; import com.google.gerrit.server.project.ChangeControl; +import com.google.gerrit.server.util.LabelVote; import com.google.gwtorm.server.OrmException; import com.google.inject.Inject; import com.google.inject.Provider; @@ -137,64 +140,66 @@ PatchSet.Id psId = change.currentPatchSetId(); ps = psUtil.current(db.get(), ctl.getNotes()); - PatchSetApproval psa = null; - StringBuilder msg = new StringBuilder(); - - // get all of the current approvals + boolean found = false; LabelTypes labelTypes = ctx.getControl().getLabelTypes(); - Map<String, Short> currentApprovals = new HashMap<>(); - for (LabelType lt : labelTypes.getLabelTypes()) { - currentApprovals.put(lt.getName(), (short) 0); - for (PatchSetApproval a : approvalsUtil.byPatchSetUser( - ctx.getDb(), ctl, psId, accountId)) { - if (lt.getLabelId().equals(a.getLabelId())) { - currentApprovals.put(lt.getName(), a.getValue()); - } - } - } - // removing votes so we need to determine the new set of approval scores - newApprovals.putAll(currentApprovals); + for (PatchSetApproval a : approvalsUtil.byPatchSetUser( - ctx.getDb(), ctl, psId, accountId)) { - if (ctl.canRemoveReviewer(a)) { - if (a.getLabel().equals(label)) { - // set the approval to 0 if vote is being removed - newApprovals.put(a.getLabel(), (short) 0); - // set old value only if the vote changed - oldApprovals.put(a.getLabel(), a.getValue()); - msg.append("Removed ") - .append(a.getLabel()).append(formatLabelValue(a.getValue())) - .append(" by ").append(userFactory.create(a.getAccountId()) - .getNameEmail()) - .append("\n"); - psa = a; - a.setValue((short)0); - ctx.getUpdate(psId).removeApprovalFor(a.getAccountId(), label); - break; - } - } else { + ctx.getDb(), ctl, psId, accountId)) { + if (labelTypes.byLabel(a.getLabelId()) == null) { + continue; // Ignore undefined labels. + } else if (!a.getLabel().equals(label)) { + // Populate map for non-matching labels, needed by VoteDeleted. + newApprovals.put(a.getLabel(), a.getValue()); + continue; + } else if (!ctl.canRemoveReviewer(a)) { throw new AuthException("delete vote not permitted"); } + // Set the approval to 0 if vote is being removed. + newApprovals.put(a.getLabel(), (short) 0); + found = true; + + // Set old value, as required by VoteDeleted. + oldApprovals.put(a.getLabel(), a.getValue()); + break; } - if (psa == null) { + if (!found) { throw new ResourceNotFoundException(); } - ctx.getDb().patchSetApprovals().update(Collections.singleton(psa)); - if (msg.length() > 0) { - changeMessage = - new ChangeMessage(new ChangeMessage.Key(change.getId(), - ChangeUtil.messageUUID(ctx.getDb())), - ctx.getAccountId(), - ctx.getWhen(), - change.currentPatchSetId()); - changeMessage.setMessage(msg.toString()); - cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), - changeMessage); - } + ctx.getUpdate(psId).removeApprovalFor(accountId, label); + ctx.getDb().patchSetApprovals().upsert( + Collections.singleton(deletedApproval(ctx))); + + changeMessage = + new ChangeMessage(new ChangeMessage.Key(change.getId(), + ChangeUtil.messageUUID(ctx.getDb())), + ctx.getAccountId(), + ctx.getWhen(), + change.currentPatchSetId()); + StringBuilder msg = new StringBuilder(); + msg.append("Removed "); + LabelVote.appendTo(msg, label, checkNotNull(oldApprovals.get(label))); + changeMessage.setMessage( + msg.append(" by ") + .append(userFactory.create(accountId).getNameEmail()) + .append("\n") + .toString()); + cmUtil.addChangeMessage(ctx.getDb(), ctx.getUpdate(psId), + changeMessage); + return true; } + private PatchSetApproval deletedApproval(ChangeContext ctx) { + return new PatchSetApproval( + new PatchSetApproval.Key( + ps.getId(), + accountId, + new LabelId(label)), + (short) 0, + ctx.getWhen()); + } + @Override public void postUpdate(Context ctx) { if (changeMessage == null) { @@ -220,11 +225,4 @@ user.getAccount(), ctx.getWhen()); } } - - private static String formatLabelValue(short value) { - if (value > 0) { - return "+" + value; - } - return Short.toString(value); - } }
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 e0591f4..8e55df5 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
@@ -14,8 +14,6 @@ package com.google.gerrit.server.change; -import static com.google.gerrit.server.util.GitUtil.getParent; - import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace; import com.google.gerrit.extensions.common.FileInfo; @@ -23,7 +21,6 @@ import com.google.gerrit.reviewdb.client.Patch; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.RevId; -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.PatchListEntry; @@ -32,24 +29,18 @@ import com.google.inject.Inject; import com.google.inject.Singleton; -import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Repository; -import java.io.IOException; import java.util.Map; import java.util.TreeMap; @Singleton public class FileInfoJson { private final PatchListCache patchListCache; - private final GitRepositoryManager repoManager; @Inject FileInfoJson( - PatchListCache patchListCache, - GitRepositoryManager repoManager) { - this.repoManager = repoManager; + PatchListCache patchListCache) { this.patchListCache = patchListCache; } @@ -64,24 +55,19 @@ ? null : ObjectId.fromString(base.getRevision().get()); ObjectId b = ObjectId.fromString(revision.get()); - return toFileInfoMap(change, a, b); + return toFileInfoMap(change, new PatchListKey(a, b, Whitespace.IGNORE_NONE)); } Map<String, FileInfo> toFileInfoMap(Change change, RevId revision, int parent) - throws RepositoryNotFoundException, IOException, - PatchListNotAvailableException { + throws PatchListNotAvailableException { ObjectId b = ObjectId.fromString(revision.get()); - ObjectId a; - try (Repository git = repoManager.openRepository(change.getProject())) { - a = getParent(git, b, parent); - } - return toFileInfoMap(change, a, b); + return toFileInfoMap(change, + PatchListKey.againstParentNum(parent + 1, b, Whitespace.IGNORE_NONE)); } private Map<String, FileInfo> toFileInfoMap(Change change, - ObjectId a, ObjectId b) throws PatchListNotAvailableException { - PatchList list = patchListCache.get( - new PatchListKey(a, b, Whitespace.IGNORE_NONE), change.getProject()); + PatchListKey key) throws PatchListNotAvailableException { + PatchList list = patchListCache.get(key, change.getProject()); Map<String, FileInfo> files = new TreeMap<>(); for (PatchListEntry e : list.getPatches()) {
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 35dbec1..c077bbb 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
@@ -138,9 +138,10 @@ } @Override - public Response<?> apply(RevisionResource resource) throws AuthException, - BadRequestException, ResourceNotFoundException, OrmException, - RepositoryNotFoundException, IOException { + public Response<?> apply(RevisionResource resource) + throws AuthException, BadRequestException, ResourceNotFoundException, + OrmException, RepositoryNotFoundException, IOException, + PatchListNotAvailableException { checkOptions(); if (reviewed) { return Response.ok(reviewed(resource)); @@ -149,26 +150,22 @@ } Response<Map<String, FileInfo>> r; - try { - if (base != null) { - RevisionResource baseResource = revisions.parse( - resource.getChangeResource(), IdString.fromDecoded(base)); - r = Response.ok(fileInfoJson.toFileInfoMap( - resource.getChange(), - resource.getPatchSet().getRevision(), - baseResource.getPatchSet())); - } else if (parentNum > 0) { - r = Response.ok(fileInfoJson.toFileInfoMap( - resource.getChange(), - resource.getPatchSet().getRevision(), - parentNum - 1)); - } else { - r = Response.ok(fileInfoJson.toFileInfoMap( - resource.getChange(), - resource.getPatchSet())); - } - } catch (PatchListNotAvailableException e) { - throw new ResourceNotFoundException(e.getMessage()); + if (base != null) { + RevisionResource baseResource = revisions.parse( + resource.getChangeResource(), IdString.fromDecoded(base)); + r = Response.ok(fileInfoJson.toFileInfoMap( + resource.getChange(), + resource.getPatchSet().getRevision(), + baseResource.getPatchSet())); + } else if (parentNum > 0) { + r = Response.ok(fileInfoJson.toFileInfoMap( + resource.getChange(), + resource.getPatchSet().getRevision(), + parentNum - 1)); + } else { + r = Response.ok(fileInfoJson.toFileInfoMap( + resource.getChange(), + resource.getPatchSet())); } if (resource.isCacheable()) {
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 8c9a0ad..e51d37b 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
@@ -35,7 +35,7 @@ private final GitRepositoryManager repoManager; private final ChangeJson.Factory json; - @Option(name = "--links", usage = "Add weblinks") + @Option(name = "--links", usage = "Include weblinks") private boolean addLinks; @Inject
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetMergeList.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetMergeList.java new file mode 100644 index 0000000..4e94935 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetMergeList.java
@@ -0,0 +1,105 @@ +// 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.change; + +import com.google.common.collect.ImmutableList; +import com.google.gerrit.extensions.common.CommitInfo; +import com.google.gerrit.extensions.restapi.BadRequestException; +import com.google.gerrit.extensions.restapi.CacheControl; +import com.google.gerrit.extensions.restapi.Response; +import com.google.gerrit.extensions.restapi.RestReadView; +import com.google.gerrit.reviewdb.client.Project; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.inject.Inject; + +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; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class GetMergeList implements RestReadView<RevisionResource> { + private final GitRepositoryManager repoManager; + private final ChangeJson.Factory json; + + @Option(name = "--parent", usage = "Uninteresting parent (1-based, default = 1)") + private int uninterestingParent = 1; + + @Option(name = "--links", usage = "Include weblinks") + private boolean addLinks; + + @Inject + GetMergeList(GitRepositoryManager repoManager, ChangeJson.Factory json) { + this.repoManager = repoManager; + this.json = json; + } + + public void setUninterestingParent(int uninterestingParent) { + this.uninterestingParent = uninterestingParent; + } + + public void setAddLinks(boolean addLinks) { + this.addLinks = addLinks; + } + + @Override + public Response<List<CommitInfo>> apply(RevisionResource rsrc) + throws BadRequestException, IOException { + List<CommitInfo> result = new ArrayList<>(); + Project.NameKey p = rsrc.getChange().getProject(); + try (Repository repo = repoManager.openRepository(p); + RevWalk rw = new RevWalk(repo)) { + String rev = rsrc.getPatchSet().getRevision().get(); + RevCommit commit = rw.parseCommit(ObjectId.fromString(rev)); + rw.parseBody(commit); + + if (uninterestingParent < 1 + || uninterestingParent > commit.getParentCount()) { + throw new BadRequestException("No such parent: " + uninterestingParent); + } + + if (commit.getParentCount() < 2) { + return Response.<List<CommitInfo>> ok(ImmutableList.<CommitInfo> of()); + } + + for (int parent = 0; parent < commit.getParentCount(); parent++) { + if (parent == uninterestingParent - 1) { + rw.markUninteresting(commit.getParent(parent)); + } else { + rw.markStart(commit.getParent(parent)); + } + } + + ChangeJson changeJson = json.create(ChangeJson.NO_OPTIONS); + RevCommit c; + while ((c = rw.next()) != null) { + CommitInfo info = + changeJson.toCommit(rsrc.getControl(), rw, c, addLinks, true); + result.add(info); + } + } + + Response<List<CommitInfo>> r = Response.ok(result); + if (rsrc.isCacheable()) { + r.caching(CacheControl.PRIVATE(7, TimeUnit.DAYS)); + } + return r; + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java index 12e4276..0a7452b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -92,6 +92,9 @@ PatchSet basePs = isEdit ? rsrc.getEdit().get().getBasePatchSet() : rsrc.getPatchSet(); + + reloadChangeIfStale(cds, basePs); + for (PatchSetData d : sorter.sort(cds, basePs)) { PatchSet ps = d.patchSet(); RevCommit commit; @@ -123,6 +126,17 @@ return result; } + private void reloadChangeIfStale(List<ChangeData> cds, PatchSet wantedPs) + throws OrmException { + for (ChangeData cd : cds) { + if (cd.getId().equals(wantedPs.getId().getParentKey())) { + if (cd.patchSet(wantedPs.getId()) == null) { + cd.reloadChange(); + } + } + } + } + public static class RelatedInfo { public List<ChangeAndCommit> changes; }
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 6de7deb..7f630ba 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
@@ -78,6 +78,7 @@ child(CHANGE_KIND, "reviewers").to(Reviewers.class); get(REVIEWER_KIND).to(GetReviewer.class); delete(REVIEWER_KIND).to(DeleteReviewer.class); + post(REVIEWER_KIND, "delete").to(DeleteReviewer.class); child(REVIEWER_KIND, "votes").to(Votes.class); delete(VOTE_KIND).to(DeleteVote.class); post(VOTE_KIND, "delete").to(DeleteVote.class); @@ -99,6 +100,7 @@ post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class); post(REVISION_KIND, "test.submit_type").to(TestSubmitType.class); get(REVISION_KIND, "archive").to(GetArchive.class); + get(REVISION_KIND, "mergelist").to(GetMergeList.class); child(REVISION_KIND, "drafts").to(DraftComments.class); put(REVISION_KIND, "drafts").to(CreateDraftComment.class);
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 508d380..3f38fc3 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
@@ -35,7 +35,6 @@ import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.events.CommitReceivedEvent; import com.google.gerrit.server.extensions.events.RevisionCreated; -import com.google.gerrit.server.git.BanCommit; import com.google.gerrit.server.git.BatchUpdate; import com.google.gerrit.server.git.BatchUpdate.ChangeContext; import com.google.gerrit.server.git.BatchUpdate.Context; @@ -47,13 +46,11 @@ import com.google.gerrit.server.patch.PatchSetInfoFactory; import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.ssh.NoSshInfo; -import com.google.gerrit.server.ssh.SshInfo; import com.google.gwtorm.server.OrmException; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.notes.NoteMap; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.transport.ReceiveCommand; import org.slf4j.Logger; @@ -91,14 +88,13 @@ private final ChangeControl origCtl; // Fields exposed as setters. - private SshInfo sshInfo; private String message; private CommitValidators.Policy validatePolicy = CommitValidators.Policy.GERRIT; private boolean draft; private List<String> groups = Collections.emptyList(); private boolean fireRevisionCreated = true; - private boolean sendMail = true; + private NotifyHandling notify = NotifyHandling.ALL; private boolean allowClosed; private boolean copyApprovals = true; @@ -144,11 +140,6 @@ return this; } - public PatchSetInserter setSshInfo(SshInfo sshInfo) { - this.sshInfo = sshInfo; - return this; - } - public PatchSetInserter setValidatePolicy(CommitValidators.Policy validate) { this.validatePolicy = checkNotNull(validate); return this; @@ -170,8 +161,8 @@ return this; } - public PatchSetInserter setSendMail(boolean sendMail) { - this.sendMail = sendMail; + public PatchSetInserter setNotify(NotifyHandling notify) { + this.notify = notify; return this; } @@ -198,7 +189,6 @@ @Override public void updateRepo(RepoContext ctx) throws AuthException, ResourceConflictException, IOException, OrmException { - init(); validate(ctx); ctx.addRefUpdate(new ReceiveCommand(ObjectId.zeroId(), commit, getPatchSetId().toRefName(), ReceiveCommand.Type.CREATE)); @@ -230,7 +220,7 @@ patchSet = psUtil.insert(db, ctx.getRevWalk(), ctx.getUpdate(psId), psId, commit, draft, newGroups, null); - if (sendMail) { + if (notify != NotifyHandling.NONE) { oldReviewers = approvalsUtil.getReviewers(db, ctl.getNotes()); } @@ -257,7 +247,7 @@ @Override public void postUpdate(Context ctx) throws OrmException { - if (sendMail) { + if (notify != NotifyHandling.NONE) { try { ReplacePatchSetSender cm = replacePatchSetFactory.create( ctx.getProject(), change.getId()); @@ -266,6 +256,7 @@ cm.setChangeMessage(changeMessage.getMessage(), ctx.getWhen()); cm.addReviewers(oldReviewers.byState(REVIEWER)); cm.addExtraCC(oldReviewers.byState(CC)); + cm.setNotify(notify); cm.send(); } catch (Exception err) { log.error("Cannot send email for new patch set on change " @@ -273,30 +264,21 @@ } } - NotifyHandling notify = sendMail - ? NotifyHandling.ALL - : NotifyHandling.NONE; if (fireRevisionCreated) { revisionCreated.fire(change, patchSet, ctx.getAccountId(), ctx.getWhen(), notify); } } - private void init() { - if (sshInfo == null) { - sshInfo = new NoSshInfo(); - } - } - private void validate(RepoContext ctx) throws AuthException, ResourceConflictException, IOException, OrmException { - CommitValidators cv = commitValidatorsFactory.create( - origCtl.getRefControl(), sshInfo, ctx.getRepository()); - if (!origCtl.canAddPatchSet(ctx.getDb())) { throw new AuthException("cannot add patch set"); } + if (validatePolicy == CommitValidators.Policy.NONE) { + return; + } String refName = getPatchSetId().toRefName(); CommitReceivedEvent event = new CommitReceivedEvent( @@ -309,18 +291,11 @@ commit, ctx.getIdentifiedUser()); try { - switch (validatePolicy) { - case RECEIVE_COMMITS: - NoteMap rejectCommits = BanCommit.loadRejectCommitsMap( - ctx.getRepository(), ctx.getRevWalk()); - cv.validateForReceiveCommits(event, rejectCommits); - break; - case GERRIT: - cv.validateForGerritCommits(event); - break; - case NONE: - break; - } + commitValidatorsFactory + .create( + validatePolicy, origCtl.getRefControl(), new NoSshInfo(), + ctx.getRepository()) + .validate(event); } catch (CommitValidationException e) { throw new ResourceConflictException(e.getFullMessage()); }
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 aa35da8..3828041 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
@@ -67,6 +67,7 @@ import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.PatchLineCommentsUtil; import com.google.gerrit.server.PatchSetUtil; +import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.account.AccountsCollection; import com.google.gerrit.server.extensions.events.CommentAdded; import com.google.gerrit.server.git.BatchUpdate; @@ -212,7 +213,7 @@ } bu.addOp( revision.getChange().getId(), - new Op(revision.getPatchSet().getId(), input)); + new Op(revision.getPatchSet().getId(), input, reviewerResults)); bu.execute(); for (PostReviewers.Addition reviewerResult : reviewerResults) { @@ -387,6 +388,7 @@ private class Op extends BatchUpdate.Op { private final PatchSet.Id psId; private final ReviewInput in; + private final List<PostReviewers.Addition> reviewerResults; private IdentifiedUser user; private ChangeNotes notes; @@ -397,9 +399,11 @@ private Map<String, Short> approvals = new HashMap<>(); private Map<String, Short> oldApprovals = new HashMap<>(); - private Op(PatchSet.Id psId, ReviewInput in) { + private Op(PatchSet.Id psId, ReviewInput in, + List<PostReviewers.Addition> reviewerResults) { this.psId = psId; this.in = in; + this.reviewerResults = reviewerResults; } @Override @@ -615,6 +619,28 @@ return previous; } + private boolean isReviewer(ChangeContext ctx) throws OrmException { + 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())) { + return true; + } + return false; + } + private boolean updateLabels(ChangeContext ctx) throws OrmException, ResourceConflictException { Map<String, Short> inLabels = MoreObjects.firstNonNull(in.labels, @@ -689,6 +715,14 @@ && ctx.getChange().getStatus().isClosed()) { throw new ResourceConflictException("change is closed"); } + + // Return early if user is not a reviewer and not posting any labels. + // This allows us to preserve their CC status. + if (current.isEmpty() && del.isEmpty() && ups.isEmpty() && + !isReviewer(ctx)) { + return false; + } + forceCallerAsReviewer(ctx, current, ups, del); ctx.getDb().patchSetApprovals().delete(del); ctx.getDb().patchSetApprovals().upsert(ups);
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 fb37d9d..be94d5d 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
@@ -17,6 +17,7 @@ import static com.google.gerrit.extensions.client.ReviewerState.CC; import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER; +import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; @@ -40,7 +41,6 @@ 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.account.AccountLoader; import com.google.gerrit.server.account.AccountsCollection; import com.google.gerrit.server.account.GroupMembers; @@ -96,7 +96,6 @@ private final Provider<IdentifiedUser> user; private final IdentifiedUser.GenericFactory identifiedUserFactory; private final Config cfg; - private final AccountCache accountCache; private final ReviewerJson json; private final ReviewerAdded reviewerAdded; private final NotesMigration migration; @@ -115,7 +114,6 @@ Provider<IdentifiedUser> user, IdentifiedUser.GenericFactory identifiedUserFactory, @GerritServerConfig Config cfg, - AccountCache accountCache, ReviewerJson json, ReviewerAdded reviewerAdded, NotesMigration migration) { @@ -132,7 +130,6 @@ this.user = user; this.identifiedUserFactory = identifiedUserFactory; this.cfg = cfg; - this.accountCache = accountCache; this.json = json; this.reviewerAdded = reviewerAdded; this.migration = migration; @@ -184,7 +181,12 @@ return new Addition(reviewer, rsrc.getChangeResource(), ImmutableMap.of(member.getId(), control), state); } - throw new UnprocessableEntityException("Change not visible to " + reviewer); + if (member.isActive()) { + throw new UnprocessableEntityException( + String.format("Change not visible to %s", reviewer)); + } + throw new UnprocessableEntityException( + String.format("Account of %s is inactive.", reviewer)); } private Addition putGroup(ChangeResource rsrc, AddReviewerInput input) @@ -355,11 +357,15 @@ } emailReviewers(rsrc.getChange(), addedReviewers, addedCCs); if (!addedReviewers.isEmpty()) { - for (PatchSetApproval psa : addedReviewers) { - Account account = accountCache.get(psa.getAccountId()).getAccount(); - reviewerAdded.fire(rsrc.getChange(), patchSet, account, + List<Account.Id> reviewers = Lists.transform(addedReviewers, + new Function<PatchSetApproval, Account.Id>() { + @Override + public Account.Id apply(PatchSetApproval psa) { + return psa.getAccountId(); + } + }); + reviewerAdded.fire(rsrc.getChange(), patchSet, reviewers, ctx.getAccount(), ctx.getWhen()); - } } } }
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 c86e98f..76ff15e 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
@@ -16,6 +16,7 @@ import com.google.common.base.Optional; 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; @@ -71,9 +72,8 @@ } @Singleton - public static class Publish implements RestModifyView<ChangeResource, Publish.Input> { - public static class Input { - } + public static class Publish + implements RestModifyView<ChangeResource, PublishChangeEditInput> { private final ChangeEditUtil editUtil; @@ -83,7 +83,7 @@ } @Override - public Response<?> apply(ChangeResource rsrc, Publish.Input in) + public Response<?> apply(ChangeResource rsrc, PublishChangeEditInput in) throws NoSuchChangeException, IOException, OrmException, RestApiException, UpdateException { Capable r = @@ -98,7 +98,10 @@ "no edit exists for change %s", rsrc.getChange().getChangeId())); } - editUtil.publish(edit.get()); + if (in == null) { + in = new PublishChangeEditInput(); + } + editUtil.publish(edit.get(), in.notify); return Response.none(); } }
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 8909e60..d3c300e 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
@@ -17,6 +17,7 @@ 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; import com.google.gerrit.extensions.restapi.RestApiException; @@ -150,7 +151,7 @@ patchSetInserter = patchSetInserterFactory .create(ctl, rebasedPatchSetId, rebasedCommit) .setDraft(originalPatchSet.isDraft()) - .setSendMail(false) + .setNotify(NotifyHandling.NONE) .setFireRevisionCreated(fireRevisionCreated) .setCopyApprovals(copyApprovals) .setMessage(
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 4750197..ba12fed 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
@@ -134,7 +134,7 @@ private final ChangeMessagesUtil cmUtil; private final ChangeNotes.Factory changeNotesFactory; private final Provider<MergeOp> mergeOpProvider; - private final MergeSuperSet mergeSuperSet; + private final Provider<MergeSuperSet> mergeSuperSet; private final AccountsCollection accounts; private final ChangesCollection changes; private final String label; @@ -154,7 +154,7 @@ ChangeMessagesUtil cmUtil, ChangeNotes.Factory changeNotesFactory, Provider<MergeOp> mergeOpProvider, - MergeSuperSet mergeSuperSet, + Provider<MergeSuperSet> mergeSuperSet, AccountsCollection accounts, ChangesCollection changes, @GerritServerConfig Config cfg, @@ -345,7 +345,7 @@ ChangeSet cs; try { - cs = mergeSuperSet.completeChangeSet( + cs = mergeSuperSet.get().completeChangeSet( db, cd.change(), resource.getControl().getUser()); } catch (OrmException | IOException e) { throw new OrmRuntimeException("Could not determine complete set of " +
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java index c4c0e98..1afe960 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SubmittedTogether.java
@@ -54,7 +54,7 @@ private final ChangeJson.Factory json; private final Provider<ReviewDb> dbProvider; private final Provider<InternalChangeQuery> queryProvider; - private final MergeSuperSet mergeSuperSet; + private final Provider<MergeSuperSet> mergeSuperSet; private final Provider<WalkSorter> sorter; @Option(name = "-o", usage = "Output options") @@ -66,7 +66,7 @@ SubmittedTogether(ChangeJson.Factory json, Provider<ReviewDb> dbProvider, Provider<InternalChangeQuery> queryProvider, - MergeSuperSet mergeSuperSet, + Provider<MergeSuperSet> mergeSuperSet, Provider<WalkSorter> sorter) { this.json = json; this.dbProvider = dbProvider; @@ -96,7 +96,7 @@ if (c.getStatus().isOpen()) { ChangeSet cs = - mergeSuperSet.completeChangeSet( + mergeSuperSet.get().completeChangeSet( dbProvider.get(), c, resource.getControl().getUser()); cds = cs.changes().asList(); hidden = cs.nonVisibleChanges().size();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AgreementJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AgreementJson.java new file mode 100644 index 0000000..3ababbc --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AgreementJson.java
@@ -0,0 +1,74 @@ +// 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.config; + +import com.google.gerrit.common.data.ContributorAgreement; +import com.google.gerrit.common.data.GroupReference; +import com.google.gerrit.common.errors.NoSuchGroupException; +import com.google.gerrit.extensions.common.AgreementInfo; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.account.GroupControl; +import com.google.gerrit.server.group.GroupJson; +import com.google.gerrit.server.group.GroupResource; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AgreementJson { + private static final Logger log = + LoggerFactory.getLogger(AgreementJson.class); + + private final Provider<CurrentUser> self; + private final IdentifiedUser.GenericFactory identifiedUserFactory; + private final GroupControl.GenericFactory genericGroupControlFactory; + private final GroupJson groupJson; + + @Inject + AgreementJson(Provider<CurrentUser> self, + IdentifiedUser.GenericFactory identifiedUserFactory, + GroupControl.GenericFactory genericGroupControlFactory, + GroupJson groupJson) { + this.self = self; + this.identifiedUserFactory = identifiedUserFactory; + this.genericGroupControlFactory = genericGroupControlFactory; + this.groupJson = groupJson; + } + + public AgreementInfo format(ContributorAgreement ca) { + AgreementInfo info = new AgreementInfo(); + info.name = ca.getName(); + info.description = ca.getDescription(); + info.url = ca.getAgreementUrl(); + GroupReference autoVerifyGroup = ca.getAutoVerify(); + if (autoVerifyGroup != null && self.get().isIdentifiedUser()) { + IdentifiedUser user = + identifiedUserFactory.create(self.get().getAccountId()); + try { + GroupControl gc = genericGroupControlFactory.controlFor( + user, autoVerifyGroup.getUUID()); + GroupResource group = new GroupResource(gc); + info.autoVerifyGroup = groupJson.format(group); + } catch (NoSuchGroupException | OrmException e) { + log.warn("autoverify group \"" + autoVerifyGroup.getName() + + "\" does not exist, referenced in CLA \"" + ca.getName() + "\""); + } + } + return info; + } +}
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 f2fc94e..5a40a31 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,8 +14,8 @@ package com.google.gerrit.server.config; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.reviewdb.client.AccountExternalId; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.server.auth.openid.OpenIdProviderPattern; import com.google.gwtjsonrpc.server.SignedToken; import com.google.gwtjsonrpc.server.XsrfException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java index 8e181a9..5b0f73d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthModule.java
@@ -14,8 +14,8 @@ package com.google.gerrit.server.config; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.extensions.registration.DynamicSet; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.server.account.DefaultRealm; import com.google.gerrit.server.account.Realm; import com.google.gerrit.server.auth.AuthBackend;
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 da1f9a6..c91ca3b 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
@@ -1,4 +1,4 @@ -// Copyright (C) 2009 The Android Open Source Project +// 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. @@ -116,7 +116,6 @@ import com.google.gerrit.server.git.TransferConfig; import com.google.gerrit.server.git.strategy.SubmitStrategy; import com.google.gerrit.server.git.validators.CommitValidationListener; -import com.google.gerrit.server.git.validators.CommitValidators; import com.google.gerrit.server.git.validators.MergeValidationListener; import com.google.gerrit.server.git.validators.MergeValidators; import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator; @@ -134,6 +133,8 @@ import com.google.gerrit.server.mail.EmailModule; import com.google.gerrit.server.mail.FromAddressGenerator; import com.google.gerrit.server.mail.FromAddressGeneratorProvider; +import com.google.gerrit.server.mail.MailSoyTofuProvider; +import com.google.gerrit.server.mail.MailTemplates; import com.google.gerrit.server.mail.MergedSender; import com.google.gerrit.server.mail.RegisterNewEmailSender; import com.google.gerrit.server.mail.ReplacePatchSetSender; @@ -170,6 +171,7 @@ import com.google.inject.Inject; import com.google.inject.TypeLiteral; import com.google.inject.internal.UniqueAnnotations; +import com.google.template.soy.tofu.SoyTofu; import org.apache.velocity.runtime.RuntimeInstance; import org.eclipse.jgit.lib.Config; @@ -275,6 +277,9 @@ bind(RuntimeInstance.class) .toProvider(VelocityRuntimeProvider.class); + bind(SoyTofu.class) + .annotatedWith(MailTemplates.class) + .toProvider(MailSoyTofuProvider.class); bind(FromAddressGenerator.class).toProvider( FromAddressGeneratorProvider.class).in(SINGLETON); bind(Boolean.class).annotatedWith(DisableReverseDnsLookup.class) @@ -366,7 +371,6 @@ bind(AnonymousUser.class); - factory(CommitValidators.Factory.class); factory(RefOperationValidators.Factory.class); factory(MergeValidators.Factory.class); factory(ProjectConfigValidator.Factory.class);
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 1dc910c..9e2ad77 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
@@ -20,6 +20,18 @@ import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import com.google.gerrit.common.data.ContributorAgreement; +import com.google.gerrit.extensions.common.AuthInfo; +import com.google.gerrit.extensions.common.ChangeConfigInfo; +import com.google.gerrit.extensions.common.DownloadInfo; +import com.google.gerrit.extensions.common.DownloadSchemeInfo; +import com.google.gerrit.extensions.common.GerritInfo; +import com.google.gerrit.extensions.common.PluginConfigInfo; +import com.google.gerrit.extensions.common.ReceiveInfo; +import com.google.gerrit.extensions.common.ServerInfo; +import com.google.gerrit.extensions.common.SshdInfo; +import com.google.gerrit.extensions.common.SuggestInfo; +import com.google.gerrit.extensions.common.UserConfigInfo; import com.google.gerrit.extensions.config.CloneCommand; import com.google.gerrit.extensions.config.DownloadCommand; import com.google.gerrit.extensions.config.DownloadScheme; @@ -28,8 +40,6 @@ import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.webui.WebUiPlugin; -import com.google.gerrit.reviewdb.client.Account; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.server.EnableSignedPush; import com.google.gerrit.server.account.Realm; import com.google.gerrit.server.avatar.AvatarProvider; @@ -38,14 +48,15 @@ import com.google.gerrit.server.change.Submit; import com.google.gerrit.server.documentation.QueryDocumentationExecutor; import com.google.gerrit.server.notedb.NotesMigration; +import com.google.gerrit.server.project.ProjectCache; import com.google.inject.Inject; import org.eclipse.jgit.lib.Config; import java.net.MalformedURLException; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -69,6 +80,8 @@ private final boolean enableSignedPush; private final QueryDocumentationExecutor docSearcher; private final NotesMigration migration; + private final ProjectCache projectCache; + private final AgreementJson agreementJson; @Inject public GetServerInfo( @@ -86,7 +99,9 @@ DynamicItem<AvatarProvider> avatar, @EnableSignedPush boolean enableSignedPush, QueryDocumentationExecutor docSearcher, - NotesMigration migration) { + NotesMigration migration, + ProjectCache projectCache, + AgreementJson agreementJson) { this.config = config; this.authConfig = authConfig; this.realm = realm; @@ -102,6 +117,8 @@ this.enableSignedPush = enableSignedPush; this.docSearcher = docSearcher; this.migration = migration; + this.projectCache = projectCache; + this.agreementJson = agreementJson; } @Override @@ -134,6 +151,18 @@ info.switchAccountUrl = cfg.getSwitchAccountUrl(); info.isGitBasicAuth = toBoolean(cfg.isGitBasicAuth()); + if (info.useContributorAgreements != null) { + Collection<ContributorAgreement> agreements = + projectCache.getAllProjects().getConfig().getContributorAgreements(); + if (!agreements.isEmpty()) { + info.contributorAgreements = + Lists.newArrayListWithCapacity(agreements.size()); + for (ContributorAgreement agreement: agreements) { + info.contributorAgreements.add(agreementJson.format(agreement)); + } + } + } + switch (info.authType) { case LDAP: case LDAP_BIND: @@ -322,85 +351,4 @@ private static Boolean toBoolean(boolean v) { return v ? v : null; } - - public static class ServerInfo { - public AuthInfo auth; - public ChangeConfigInfo change; - public DownloadInfo download; - public GerritInfo gerrit; - public Boolean noteDbEnabled; - public PluginConfigInfo plugin; - public SshdInfo sshd; - public SuggestInfo suggest; - public Map<String, String> urlAliases; - public UserConfigInfo user; - public ReceiveInfo receive; - } - - public static class AuthInfo { - public AuthType authType; - public Boolean useContributorAgreements; - public List<Account.FieldName> editableAccountFields; - public String loginUrl; - public String loginText; - public String switchAccountUrl; - public String registerUrl; - public String registerText; - public String editFullNameUrl; - public String httpPasswordUrl; - public Boolean isGitBasicAuth; - } - - public static class ChangeConfigInfo { - public Boolean allowBlame; - public Boolean allowDrafts; - public int largeChange; - public String replyLabel; - public String replyTooltip; - public int updateDelay; - public Boolean submitWholeTopic; - } - - public static class DownloadInfo { - public Map<String, DownloadSchemeInfo> schemes; - public List<String> archives; - } - - public static class DownloadSchemeInfo { - public String url; - public Boolean isAuthRequired; - public Boolean isAuthSupported; - public Map<String, String> commands; - public Map<String, String> cloneCommands; - } - - public static class GerritInfo { - public String allProjects; - public String allUsers; - public Boolean docSearch; - public String docUrl; - public Boolean editGpgKeys; - public String reportBugUrl; - public String reportBugText; - } - - public static class PluginConfigInfo { - public Boolean hasAvatars; - public List<String> jsResourcePaths; - } - - public static class SshdInfo { - } - - public static class SuggestInfo { - public int from; - } - - public static class UserConfigInfo { - public String anonymousCowardName; - } - - public static class ReceiveInfo { - public Boolean enableSignedPush; - } }
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 33a458e..f7968c8 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
@@ -67,7 +67,7 @@ } @Override - public Object apply(ConfigResource rsrc, Input input) + public Response<String> apply(ConfigResource rsrc, Input input) throws AuthException, BadRequestException, UnprocessableEntityException { if (input == null || input.operation == null) { throw new BadRequestException("operation must be specified");
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 6811056..b09e0fa 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
@@ -18,6 +18,7 @@ import com.google.common.base.Optional; import com.google.gerrit.common.TimeUtil; +import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.client.ChangeKind; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.ResourceConflictException; @@ -34,6 +35,7 @@ import com.google.gerrit.server.change.ChangeKindCache; import com.google.gerrit.server.change.PatchSetInserter; import com.google.gerrit.server.git.BatchUpdate; +import com.google.gerrit.server.git.BatchUpdate.RepoContext; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.git.UpdateException; import com.google.gerrit.server.index.change.ChangeIndexer; @@ -168,24 +170,69 @@ * @throws UpdateException * @throws RestApiException */ - public void publish(ChangeEdit edit) throws NoSuchChangeException, - IOException, OrmException, RestApiException, UpdateException { + public void publish(final ChangeEdit edit, NotifyHandling notify) + throws NoSuchChangeException, IOException, OrmException, RestApiException, + UpdateException { Change change = edit.getChange(); try (Repository repo = gitManager.openRepository(change.getProject()); RevWalk rw = new RevWalk(repo); - ObjectInserter inserter = repo.newObjectInserter()) { + ObjectInserter oi = repo.newObjectInserter()) { PatchSet basePatchSet = edit.getBasePatchSet(); if (!basePatchSet.getId().equals(change.currentPatchSetId())) { throw new ResourceConflictException( "only edit for current patch set can be published"); } - Change updatedChange = - insertPatchSet(edit, change, repo, rw, inserter, basePatchSet, - squashEdit(rw, inserter, edit.getEditCommit(), basePatchSet)); - // TODO(davido): This should happen in the same BatchRefUpdate. - deleteRef(repo, edit); - indexer.index(db.get(), updatedChange); + 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 + .create(ctl, psId, squashed) + .setNotify(notify); + + StringBuilder message = new StringBuilder("Patch Set ") + .append(inserter.getPatchSetId().get()) + .append(": "); + + ProjectState project = projectCache.get(change.getDest().getParentKey()); + // Previously checked that the base patch set is the current patch set. + ObjectId prior = ObjectId.fromString(basePatchSet.getRevision().get()); + ChangeKind kind = changeKindCache.getChangeKind(project, repo, prior, squashed); + if (kind == ChangeKind.NO_CODE_CHANGE) { + message.append("Commit message was updated."); + } else { + message.append("Published edit on patch set ") + .append(basePatchSet.getPatchSetId()) + .append("."); + } + + try (BatchUpdate bu = updateFactory.create( + db.get(), change.getProject(), ctl.getUser(), + TimeUtil.nowTs())) { + bu.setRepository(repo, rw, oi); + bu.addOp(change.getId(), inserter + .setDraft(change.getStatus() == Status.DRAFT || + basePatchSet.isDraft()) + .setMessage(message.toString())); + bu.addOp(change.getId(), new BatchUpdate.Op() { + @Override + public void updateRepo(RepoContext ctx) throws Exception { + deleteRef(ctx.getRepository(), edit); + } + }); + 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 ref was updated"); + } + } + + indexer.index(db.get(), inserter.getChange()); } } @@ -230,47 +277,6 @@ return writeSquashedCommit(rw, inserter, parent, edit); } - private Change insertPatchSet(ChangeEdit edit, Change change, - Repository repo, RevWalk rw, ObjectInserter oi, PatchSet basePatchSet, - RevCommit squashed) throws NoSuchChangeException, RestApiException, - UpdateException, OrmException, IOException { - ChangeControl ctl = - changeControlFactory.controlFor(db.get(), change, edit.getUser()); - PatchSet.Id psId = - ChangeUtil.nextPatchSetId(repo, change.currentPatchSetId()); - PatchSetInserter inserter = - patchSetInserterFactory.create(ctl, psId, squashed); - - StringBuilder message = new StringBuilder("Patch Set ") - .append(inserter.getPatchSetId().get()) - .append(": "); - - ProjectState project = projectCache.get(change.getDest().getParentKey()); - // Previously checked that the base patch set is the current patch set. - ObjectId prior = ObjectId.fromString(basePatchSet.getRevision().get()); - ChangeKind kind = changeKindCache.getChangeKind(project, repo, prior, squashed); - if (kind == ChangeKind.NO_CODE_CHANGE) { - message.append("Commit message was updated."); - } else { - message.append("Published edit on patch set ") - .append(basePatchSet.getPatchSetId()) - .append("."); - } - - try (BatchUpdate bu = updateFactory.create( - db.get(), change.getProject(), ctl.getUser(), - TimeUtil.nowTs())) { - bu.setRepository(repo, rw, oi); - bu.addOp(change.getId(), inserter - .setDraft(change.getStatus() == Status.DRAFT || - basePatchSet.isDraft()) - .setMessage(message.toString())); - bu.execute(); - } - - return inserter.getChange(); - } - private static void deleteRef(Repository repo, ChangeEdit edit) throws IOException { String refName = edit.getRefName();
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 1368bf3..8d093f0 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
@@ -321,7 +321,7 @@ } @Override - public void onReviewerAdded(ReviewerAddedListener.Event ev) { + public void onReviewersAdded(ReviewerAddedListener.Event ev) { try { ChangeNotes notes = getNotes(ev.getChange()); Change change = notes.getChange(); @@ -330,9 +330,10 @@ event.change = changeAttributeSupplier(change); event.patchSet = patchSetAttributeSupplier(change, psUtil.current(db.get(), notes)); - event.reviewer = accountAttributeSupplier(ev.getReviewer()); - - dispatcher.get().postEvent(change, event); + for (AccountInfo reviewer : ev.getReviewers()) { + event.reviewer = accountAttributeSupplier(reviewer); + dispatcher.get().postEvent(change, event); + } } catch (OrmException e) { log.error("Failed to dispatch event", e); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java index dd49272..c76e76b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
@@ -83,22 +83,15 @@ private static class Event extends AbstractRevisionEvent implements ChangeAbandonedListener.Event { - private final AccountInfo abandoner; private final String reason; Event(ChangeInfo change, RevisionInfo revision, AccountInfo abandoner, String reason, Timestamp when, NotifyHandling notifyHandling) { super(change, revision, abandoner, when, notifyHandling); - this.abandoner = abandoner; this.reason = reason; } @Override - public AccountInfo getAbandoner() { - return abandoner; - } - - @Override public String getReason() { return reason; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java index 94df1d1..378f2b7 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
@@ -81,22 +81,15 @@ private static class Event extends AbstractRevisionEvent implements ChangeMergedListener.Event { - private final AccountInfo merger; private final String newRevisionId; Event(ChangeInfo change, RevisionInfo revision, AccountInfo merger, String newRevisionId, Timestamp when) { super(change, revision, merger, when, NotifyHandling.ALL); - this.merger = merger; this.newRevisionId = newRevisionId; } @Override - public AccountInfo getMerger() { - return merger; - } - - @Override public String getNewRevisionId() { return newRevisionId; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java index c853609..05e0d21 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
@@ -82,22 +82,15 @@ private static class Event extends AbstractRevisionEvent implements ChangeRestoredListener.Event { - private AccountInfo restorer; private String reason; Event(ChangeInfo change, RevisionInfo revision, AccountInfo restorer, String reason, Timestamp when) { super(change, revision, restorer, when, NotifyHandling.ALL); - this.restorer = restorer; this.reason = reason; } @Override - public AccountInfo getRestorer() { - return restorer; - } - - @Override public String getReason() { return reason; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java index 85ad8b6..8e27ce9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/CommentAdded.java
@@ -90,7 +90,6 @@ private static class Event extends AbstractRevisionEvent implements CommentAddedListener.Event { - private final AccountInfo author; private final String comment; private final Map<String, ApprovalInfo> approvals; private final Map<String, ApprovalInfo> oldApprovals; @@ -99,18 +98,12 @@ String comment, Map<String, ApprovalInfo> approvals, Map<String, ApprovalInfo> oldApprovals, Timestamp when) { super(change, revision, author, when, NotifyHandling.ALL); - this.author = author; this.comment = comment; this.approvals = approvals; this.oldApprovals = oldApprovals; } @Override - public AccountInfo getAuthor() { - return author; - } - - @Override public String getComment() { return comment; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java index 0895cb8..6b8ce3d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/DraftPublished.java
@@ -78,17 +78,10 @@ private static class Event extends AbstractRevisionEvent implements DraftPublishedListener.Event { - private final AccountInfo publisher; Event(ChangeInfo change, RevisionInfo revision, AccountInfo publisher, Timestamp when) { super(change, revision, publisher, when, NotifyHandling.ALL); - this.publisher = publisher; - } - - @Override - public AccountInfo getPublisher() { - return publisher; } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java index fe42d02..f18b963 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
@@ -81,7 +81,6 @@ private static class Event extends AbstractChangeEvent implements HashtagsEditedListener.Event { - private AccountInfo editor; private Collection<String> updatedHashtags; private Collection<String> addedHashtags; private Collection<String> removedHashtags; @@ -89,18 +88,12 @@ Event(ChangeInfo change, AccountInfo editor, Collection<String> updated, Collection<String> added, Collection<String> removed, Timestamp when) { super(change, editor, when, NotifyHandling.ALL); - this.editor = editor; this.updatedHashtags = updated; this.addedHashtags = added; this.removedHashtags = removed; } @Override - public AccountInfo getEditor() { - return editor; - } - - @Override public Collection<String> getHashtags() { return updatedHashtags; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java index 3ae8135..35b14dc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
@@ -14,6 +14,8 @@ package com.google.gerrit.server.extensions.events; +import com.google.common.base.Function; +import com.google.common.collect.Lists; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.common.ChangeInfo; @@ -33,6 +35,7 @@ import java.io.IOException; import java.sql.Timestamp; +import java.util.List; public class ReviewerAdded { private static final Logger log = @@ -49,29 +52,38 @@ } public void fire(ChangeInfo change, RevisionInfo revision, - AccountInfo reviewer, AccountInfo adder, Timestamp when) { + List<AccountInfo> reviewers, AccountInfo adder, Timestamp when) { if (!listeners.iterator().hasNext()) { return; } - Event event = new Event(change, revision, reviewer, adder, when); + Event event = new Event(change, revision, reviewers, adder, when); for (ReviewerAddedListener l : listeners) { try { - l.onReviewerAdded(event); + l.onReviewersAdded(event); } catch (Exception e) { log.warn("Error in event listener, e"); } } } - public void fire(Change change, PatchSet patchSet, Account account, + public void fire(Change change, PatchSet patchSet, List<Account.Id> reviewers, Account adder, Timestamp when) { - if (!listeners.iterator().hasNext()) { + if (!listeners.iterator().hasNext() || reviewers.isEmpty()) { return; } + + List<AccountInfo> transformed = Lists.transform(reviewers, + new Function<Account.Id, AccountInfo>() { + @Override + public AccountInfo apply(Account.Id account) { + return util.accountInfo(account); + } + }); + try { fire(util.changeInfo(change), util.revisionInfo(change.getProject(), patchSet), - util.accountInfo(account), + transformed, util.accountInfo(adder), when); } catch (PatchListNotAvailableException | GpgException | IOException @@ -82,17 +94,17 @@ private static class Event extends AbstractRevisionEvent implements ReviewerAddedListener.Event { - private final AccountInfo reviewer; + private final List<AccountInfo> reviewers; - Event(ChangeInfo change, RevisionInfo revision, AccountInfo reviewer, + Event(ChangeInfo change, RevisionInfo revision, List<AccountInfo> reviewers, AccountInfo adder, Timestamp when) { super(change, revision, adder, when, NotifyHandling.ALL); - this.reviewer = reviewer; + this.reviewers = reviewers; } @Override - public AccountInfo getReviewer() { - return reviewer; + public List<AccountInfo> getReviewers() { + return reviewers; } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java index 270eb35..b519c46 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
@@ -53,12 +53,13 @@ public void fire(ChangeInfo change, RevisionInfo revision, AccountInfo reviewer, AccountInfo remover, String message, Map<String, ApprovalInfo> newApprovals, - Map<String, ApprovalInfo> oldApprovals, Timestamp when) { + Map<String, ApprovalInfo> oldApprovals, NotifyHandling notify, + Timestamp when) { if (!listeners.iterator().hasNext()) { return; } Event event = new Event(change, revision, reviewer, remover, message, - newApprovals, oldApprovals, when); + newApprovals, oldApprovals, notify, when); for (ReviewerDeletedListener listener : listeners) { try { listener.onReviewerDeleted(event); @@ -69,9 +70,8 @@ } public void fire(Change change, PatchSet patchSet, Account reviewer, - Account remover, String message, - Map<String, Short> newApprovals, - Map<String, Short> oldApprovals, Timestamp when) { + Account remover, String message, Map<String, Short> newApprovals, + Map<String, Short> oldApprovals, NotifyHandling notify, Timestamp when) { if (!listeners.iterator().hasNext()) { return; } @@ -83,6 +83,7 @@ message, util.approvals(reviewer, newApprovals, when), util.approvals(reviewer, oldApprovals, when), + notify, when); } catch (PatchListNotAvailableException | GpgException | IOException | OrmException e) { @@ -101,8 +102,9 @@ Event(ChangeInfo change, RevisionInfo revision, AccountInfo reviewer, AccountInfo remover, String comment, Map<String, ApprovalInfo> newApprovals, - Map<String, ApprovalInfo> oldApprovals, Timestamp when) { - super(change, revision, remover, when, NotifyHandling.ALL); + Map<String, ApprovalInfo> oldApprovals, NotifyHandling notify, + Timestamp when) { + super(change, revision, remover, when, notify); this.reviewer = reviewer; this.comment = comment; this.newApprovals = newApprovals;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java index 98fa05e..71bc9ec 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -81,17 +81,10 @@ private static class Event extends AbstractRevisionEvent implements RevisionCreatedListener.Event { - private final AccountInfo uploader; Event(ChangeInfo change, RevisionInfo revision, AccountInfo uploader, Timestamp when, NotifyHandling notify) { super(change, revision, uploader, when, notify); - this.uploader = uploader; - } - - @Override - public AccountInfo getUploader() { - return uploader; } } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java index 0a0a8ca..77c1647 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/TopicEdited.java
@@ -75,22 +75,15 @@ private static class Event extends AbstractChangeEvent implements TopicEditedListener.Event { - private final AccountInfo editor; private final String oldTopic; Event(ChangeInfo change, AccountInfo editor, String oldTopic, Timestamp when) { super(change, editor, when, NotifyHandling.ALL); - this.editor = editor; this.oldTopic = oldTopic; } @Override - public AccountInfo getEditor() { - return editor; - } - - @Override public String getOldTopic() { return oldTopic; }
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 0e954f3..26c59c2 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
@@ -67,7 +67,7 @@ logDebug("Loading .gitmodules of {} for project {}", branch, project); OpenRepo or; try { - or = orm.openRepo(project, false); + or = orm.openRepo(project); } catch (NoSuchProjectException e) { throw new IOException(e); }
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 9d62721..9d4c811 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
@@ -727,7 +727,7 @@ throws IntegrationException { for (Project.NameKey project : projects) { try { - orm.openRepo(project, true); + orm.openRepo(project); } catch (NoSuchProjectException noProject) { logWarn("Project " + noProject.project() + " no longer exists, " + "abandoning open changes");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java index fb4c2d4..cd76aff 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -187,13 +187,8 @@ return or; } - public OpenRepo openRepo(Project.NameKey project, boolean abortIfOpen) + public OpenRepo openRepo(Project.NameKey project) throws NoSuchProjectException, IOException { - if (abortIfOpen) { - checkState(!openRepos.containsKey(project), - "repo already opened: %s", project); - } - if (openRepos.containsKey(project)) { return openRepos.get(project); }
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 284e9ed..315365b 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
@@ -32,13 +32,13 @@ import com.google.gerrit.server.change.Submit; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.index.change.ChangeField; +import com.google.gerrit.server.project.NoSuchChangeException; import com.google.gerrit.server.project.SubmitRuleEvaluator; import com.google.gerrit.server.query.change.ChangeData; 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.Singleton; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.MissingObjectException; @@ -68,7 +68,6 @@ * If change.submitWholeTopic is enabled, also all changes of the topic * and their parents are included. */ -@Singleton public class MergeSuperSet { private static final Logger log = LoggerFactory.getLogger(MergeOp.class); @@ -77,6 +76,7 @@ for (ChangeData cd : cs.changes()) { cd.reloadChange(); cd.setPatchSets(null); + cd.setMergeable(null); } } @@ -261,11 +261,19 @@ continue; } for (ChangeData topicCd : query().byTopicOpen(topic)) { - topicCd.changeControl(user); - if (topicCd.changeControl().isVisible(db, topicCd)) { - visibleChanges.add(topicCd); - } else { - nonVisibleChanges.add(topicCd); + try { + topicCd.changeControl(user); + if (topicCd.changeControl().isVisible(db, topicCd)) { + visibleChanges.add(topicCd); + } else { + nonVisibleChanges.add(topicCd); + } + } catch (OrmException e) { + if (e.getCause() instanceof NoSuchChangeException) { + // Ignore and skip this change + } else { + throw e; + } } } topicsSeen.add(topic); @@ -307,13 +315,15 @@ } private InternalChangeQuery query() { - // Request fields required for completing the ChangeSet without having to - // touch the database. This provides reasonable performance when loading the - // change screen; callers that care about reading the latest value of these - // fields should clear them explicitly using reloadChanges(). + // Request fields required for completing the ChangeSet and converting to + // ChangeInfo without having to touch the database or opening the repository + // more than necessary. This provides reasonable performance when loading + // the change screen; callers that care about reading the latest value of + // these fields should clear them explicitly using reloadChanges(). Set<String> fields = ImmutableSet.of( ChangeField.CHANGE.getName(), - ChangeField.PATCH_SET.getName()); + ChangeField.PATCH_SET.getName(), + ChangeField.MERGEABLE.getName()); return queryProvider.get().setRequestedFields(fields); }
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 1721ae2..1429079 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
@@ -91,7 +91,7 @@ private static final String PROJECT = "project"; private static final String KEY_DESCRIPTION = "description"; - private static final String ACCESS = "access"; + public static final String ACCESS = "access"; private static final String KEY_INHERIT_FROM = "inheritFrom"; private static final String KEY_GROUP_PERMISSIONS = "exclusiveGroupPermissions"; @@ -151,10 +151,13 @@ private static final String KEY_COPY_ALL_SCORES_IF_NO_CHANGE = "copyAllScoresIfNoChange"; private static final String KEY_VALUE = "value"; private static final String KEY_CAN_OVERRIDE = "canOverride"; - private static final String KEY_Branch = "branch"; + private static final String KEY_BRANCH = "branch"; private static final Set<String> LABEL_FUNCTIONS = ImmutableSet.of( "MaxWithBlock", "AnyWithBlock", "MaxNoBlock", "NoBlock", "NoOp", "PatchSetLock"); + private static final String LEGACY_PERMISSION_PUSH_TAG = "pushTag"; + private static final String LEGACY_PERMISSION_PUSH_SIGNED_TAG = "pushSignedTag"; + private static final String PLUGIN = "plugin"; private static final SubmitType defaultSubmitAction = @@ -180,6 +183,7 @@ private Map<String, Config> pluginConfigs; private boolean checkReceivedObjects; private Set<String> sectionsWithUnknownPermissions; + private boolean hasLegacyPermissions; public static ProjectConfig read(MetaDataUpdate update) throws IOException, ConfigInvalidException { @@ -627,6 +631,7 @@ for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) { for (String n : varName.split("[, \t]{1,}")) { + n = convertLegacyPermission(n); if (isPermission(n)) { as.getPermission(n, true).setExclusiveGroup(true); } @@ -634,10 +639,11 @@ } for (String varName : rc.getNames(ACCESS, refName)) { - if (isPermission(varName)) { - Permission perm = as.getPermission(varName, true); + String convertedName = convertLegacyPermission(varName); + if (isPermission(convertedName)) { + Permission perm = as.getPermission(convertedName, true); loadPermissionRules(rc, ACCESS, refName, varName, groupsByName, - perm, Permission.hasRange(varName)); + perm, Permission.hasRange(convertedName)); } else { sectionsWithUnknownPermissions.add(as.getName()); } @@ -805,7 +811,7 @@ label.setCanOverride( rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE, LabelType.DEF_CAN_OVERRIDE)); - label.setRefPatterns(getStringListOrNull(rc, LABEL, name, KEY_Branch)); + label.setRefPatterns(getStringListOrNull(rc, LABEL, name, KEY_BRANCH)); labelSections.put(name, label); } } @@ -1147,7 +1153,8 @@ } for (String varName : rc.getNames(ACCESS, refName)) { - if (isPermission(varName) && !have.contains(varName.toLowerCase())) { + if (isPermission(convertLegacyPermission(varName)) + && !have.contains(varName.toLowerCase())) { rc.unset(ACCESS, refName, varName); } } @@ -1282,4 +1289,21 @@ Collections.sort(r); return r; } + + public boolean hasLegacyPermissions() { + return hasLegacyPermissions; + } + + private String convertLegacyPermission(String permissionName) { + switch(permissionName) { + case LEGACY_PERMISSION_PUSH_TAG: + hasLegacyPermissions = true; + return Permission.CREATE_TAG; + case LEGACY_PERMISSION_PUSH_SIGNED_TAG: + hasLegacyPermissions = true; + return Permission.CREATE_SIGNED_TAG; + default: + return permissionName; + } + } }
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 7fadae0..53e8ee8 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
@@ -42,6 +42,7 @@ import com.google.common.collect.HashBiMap; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; @@ -315,6 +316,8 @@ private final RequestId receiveId; private MagicBranchInput magicBranch; private boolean newChangeForAllNotInTarget; + private final ListMultimap<String, String> pushOptions = + LinkedListMultimap.create(); private List<CreateRequest> newChanges = Collections.emptyList(); private final Map<Change.Id, ReplaceRequest> replaceByChange = @@ -490,6 +493,7 @@ advHooks.add(new HackPushNegotiateHook()); rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks)); rp.setPostReceiveHook(lazyPostReceive.get()); + rp.setAllowPushOptions(true); } public void init() { @@ -915,6 +919,18 @@ } private void parseCommands(Collection<ReceiveCommand> commands) { + List<String> optionList = rp.getPushOptions(); + if (optionList != null) { + for (String option : optionList) { + int e = option.indexOf('='); + if (e > 0) { + pushOptions.put(option.substring(0, e), option.substring(e + 1)); + } else { + pushOptions.put(option, ""); + } + } + } + logDebug("Parsing {} commands", commands.size()); for (ReceiveCommand cmd : commands) { if (cmd.getResult() != NOT_ATTEMPTED) { @@ -1305,14 +1321,14 @@ return new MailRecipients(reviewer, cc); } - String parse(CmdLineParser clp, Repository repo, Set<String> refs) - throws CmdLineException { + String parse(CmdLineParser clp, Repository repo, Set<String> refs, + ListMultimap<String, String> pushOptions) throws CmdLineException { String ref = RefNames.fullName( MagicBranch.getDestBranchName(cmd.getRefName())); + ListMultimap<String, String> options = LinkedListMultimap.create(pushOptions); int optionStart = ref.indexOf('%'); if (0 < optionStart) { - ListMultimap<String, String> options = LinkedListMultimap.create(); for (String s : COMMAS.split(ref.substring(optionStart + 1))) { int e = s.indexOf('='); if (0 < e) { @@ -1321,10 +1337,13 @@ options.put(s, ""); } } - clp.parseOptionMap(options); ref = ref.substring(0, optionStart); } + if (!options.isEmpty()) { + clp.parseOptionMap(options); + } + // Split the destination branch by branch and topic. The topic // suffix is entirely optional, so it might not even exist. String head = readHEAD(repo); @@ -1347,6 +1366,19 @@ } } + /** + * Gets an unmodifiable view of the pushOptions. + * <p> + * The collection is empty if the client does not support push options, or if + * the client did not send any options. + * + * @return an unmodifiable view of pushOptions. + */ + @Nullable + public ListMultimap<String, String> getPushOptions() { + return ImmutableListMultimap.copyOf(pushOptions); + } + private void parseMagicBranch(ReceiveCommand cmd) { // Permit exactly one new change request per push. if (magicBranch != null) { @@ -1362,8 +1394,10 @@ String ref; CmdLineParser clp = optionParserFactory.create(magicBranch); magicBranch.clp = clp; + try { - ref = magicBranch.parse(clp, repo, rp.getAdvertisedRefs().keySet()); + ref = magicBranch.parse( + clp, repo, rp.getAdvertisedRefs().keySet(), pushOptions); } catch (CmdLineException e) { if (!clp.wasHelpRequestedByOption()) { logDebug("Invalid branch syntax"); @@ -1428,7 +1462,7 @@ } if (magicBranch.submit && !projectControl.controlForRef( - MagicBranch.NEW_CHANGE + ref).canSubmit()) { + MagicBranch.NEW_CHANGE + ref).canSubmit(true)) { reject(cmd, "submit not allowed"); return; } @@ -1655,9 +1689,7 @@ Collection<Ref> existingRefs = existing.get(c); if (rejectImplicitMerges) { - for (RevCommit p : c.getParents()) { - mergedParents.add(p); - } + Collections.addAll(mergedParents, c.getParents()); mergedParents.remove(c); } @@ -1855,7 +1887,7 @@ } private void rejectImplicitMerges(Set<RevCommit> mergedParents) - throws MissingObjectException, IncorrectObjectTypeException, IOException { + throws IOException { if (!mergedParents.isEmpty()) { Ref targetRef = allRefs.get(magicBranch.ctl.getRefName()); if (targetRef != null) { @@ -2556,12 +2588,11 @@ rw.parseBody(c); CommitReceivedEvent receiveEvent = new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, user); - CommitValidators commitValidators = - commitValidatorsFactory.create(ctl, sshInfo, repo); + CommitValidators commitValidators = commitValidatorsFactory.create( + CommitValidators.Policy.RECEIVE_COMMITS, ctl, sshInfo, repo); try { - messages.addAll(commitValidators.validateForReceiveCommits( - receiveEvent, rejectCommits)); + messages.addAll(commitValidators.validate(receiveEvent)); } catch (CommitValidationException e) { logDebug("Commit validation failed on {}", c.name()); messages.addAll(e.getMessages());
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 fe568c8..a70fa7a 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
@@ -255,13 +255,15 @@ ChangeData cd = changeDataFactory.create(ctx.getDb(), ctx.getControl()); MailRecipients oldRecipients = getRecipientsFromReviewers(cd.reviewers()); - approvalCopier.copy(ctx.getDb(), ctx.getControl(), newPatchSet); + Iterable<PatchSetApproval> newApprovals = + approvalsUtil.addApprovals(ctx.getDb(), update, + projectControl.getLabelTypes(), newPatchSet, ctx.getControl(), + approvals); + approvalCopier.copy(ctx.getDb(), ctx.getControl(), newPatchSet, + newApprovals); approvalsUtil.addReviewers(ctx.getDb(), update, projectControl.getLabelTypes(), change, newPatchSet, info, recipients.getReviewers(), oldRecipients.getAll()); - approvalsUtil.addApprovals(ctx.getDb(), update, - projectControl.getLabelTypes(), newPatchSet, ctx.getControl(), - approvals); recipients.add(oldRecipients); String approvalMessage = ApprovalsUtil.renderMessageWithApprovals(
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 01d73ec..8e84ffb 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
@@ -251,7 +251,7 @@ } OpenRepo or; try { - or = orm.openRepo(s.getProject(), false); + or = orm.openRepo(s.getProject()); } catch (NoSuchProjectException e) { // A project listed a non existent project to be allowed // to subscribe to it. Allow this for now, i.e. no exception is @@ -290,7 +290,7 @@ for (Branch.NameKey targetBranch : branches) { Project.NameKey targetProject = targetBranch.getParentKey(); try { - OpenRepo or = orm.openRepo(targetProject, false); + OpenRepo or = orm.openRepo(targetProject); ObjectId id = or.repo.resolve(targetBranch.get()); if (id == null) { logDebug("The branch " + targetBranch + " doesn't exist."); @@ -327,7 +327,7 @@ if (dst.containsKey(project)) { superProjects.add(project); // get a new BatchUpdate for the super project - OpenRepo or = orm.openRepo(project, false); + OpenRepo or = orm.openRepo(project); for (Branch.NameKey branch : dst.get(project)) { addOp(or.getUpdate(), branch); } @@ -348,7 +348,7 @@ throws IOException, SubmoduleException { OpenRepo or; try { - or = orm.openRepo(subscriber.getParentKey(), false); + or = orm.openRepo(subscriber.getParentKey()); } catch (NoSuchProjectException | IOException e) { throw new SubmoduleException("Cannot access superproject", e); } @@ -404,7 +404,7 @@ throws IOException, SubmoduleException { OpenRepo or; try { - or = orm.openRepo(subscriber.getParentKey(), false); + or = orm.openRepo(subscriber.getParentKey()); } catch (NoSuchProjectException | IOException e) { throw new SubmoduleException("Cannot access superproject", e); } @@ -444,7 +444,7 @@ throws SubmoduleException, IOException { OpenRepo subOr; try { - subOr = orm.openRepo(s.getSubmodule().getParentKey(), false); + subOr = orm.openRepo(s.getSubmodule().getParentKey()); } catch (NoSuchProjectException | IOException e) { throw new SubmoduleException("Cannot access submodule", e); }
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 d4956ab..4e44a6c 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
@@ -16,8 +16,10 @@ 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 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; @@ -31,15 +33,15 @@ import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.events.CommitReceivedEvent; +import com.google.gerrit.server.git.BanCommit; import com.google.gerrit.server.git.ProjectConfig; -import com.google.gerrit.server.git.ReceiveCommits; import com.google.gerrit.server.git.ValidationError; import com.google.gerrit.server.project.ProjectControl; import com.google.gerrit.server.project.RefControl; import com.google.gerrit.server.ssh.SshInfo; import com.google.gerrit.server.util.MagicBranch; import com.google.inject.Inject; -import com.google.inject.assistedinject.Assisted; +import com.google.inject.Singleton; import com.jcraft.jsch.HostKey; @@ -51,6 +53,7 @@ import org.eclipse.jgit.revwalk.FooterKey; import org.eclipse.jgit.revwalk.FooterLine; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.util.SystemReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,107 +71,99 @@ .getLogger(CommitValidators.class); public enum Policy { - /** Use {@link #validateForGerritCommits}. */ + /** Use {@link Factory#forGerritCommits}. */ GERRIT, - /** Use {@link #validateForReceiveCommits}. */ + /** Use {@link Factory#forReceiveCommits}. */ RECEIVE_COMMITS, /** Do not validate commits. */ NONE } - public interface Factory { - CommitValidators create(RefControl refControl, SshInfo sshInfo, - Repository repo); - } + @Singleton + public static class Factory { + private final PersonIdent gerritIdent; + private final String canonicalWebUrl; + private final DynamicSet<CommitValidationListener> pluginValidators; + private final AllUsersName allUsers; + private final String installCommitMsgHookCommand; - private final PersonIdent gerritIdent; - private final RefControl refControl; - private final String canonicalWebUrl; - private final String installCommitMsgHookCommand; - private final SshInfo sshInfo; - private final Repository repo; - private final DynamicSet<CommitValidationListener> commitValidationListeners; - private final AllUsersName allUsers; - - @Inject - CommitValidators(@GerritPersonIdent PersonIdent gerritIdent, - @CanonicalWebUrl @Nullable String canonicalWebUrl, - @GerritServerConfig Config config, - DynamicSet<CommitValidationListener> commitValidationListeners, - AllUsersName allUsers, - @Assisted SshInfo sshInfo, - @Assisted Repository repo, - @Assisted RefControl refControl) { - this.gerritIdent = gerritIdent; - this.canonicalWebUrl = canonicalWebUrl; - this.installCommitMsgHookCommand = - config.getString("gerrit", null, "installCommitMsgHookCommand"); - this.commitValidationListeners = commitValidationListeners; - this.allUsers = allUsers; - this.sshInfo = sshInfo; - this.repo = repo; - this.refControl = refControl; - } - - public List<CommitValidationMessage> validateForReceiveCommits( - CommitReceivedEvent receiveEvent, NoteMap rejectCommits) - throws CommitValidationException { - - List<CommitValidationListener> validators = new LinkedList<>(); - - validators.add(new UploadMergesPermissionValidator(refControl)); - validators.add(new AmendedGerritMergeCommitValidationListener( - refControl, gerritIdent)); - validators.add(new AuthorUploaderValidator(refControl, canonicalWebUrl)); - validators.add(new CommitterUploaderValidator(refControl, canonicalWebUrl)); - validators.add(new SignedOffByValidator(refControl)); - if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName()) - || ReceiveCommits.NEW_PATCHSET.matcher( - receiveEvent.command.getRefName()).matches()) { - validators.add(new ChangeIdValidator(refControl, canonicalWebUrl, - installCommitMsgHookCommand, sshInfo)); + @Inject + Factory(@GerritPersonIdent PersonIdent gerritIdent, + @CanonicalWebUrl @Nullable String canonicalWebUrl, + @GerritServerConfig Config cfg, + DynamicSet<CommitValidationListener> pluginValidators, + AllUsersName allUsers) { + this.gerritIdent = gerritIdent; + this.canonicalWebUrl = canonicalWebUrl; + this.pluginValidators = pluginValidators; + this.allUsers = allUsers; + this.installCommitMsgHookCommand = cfg != null + ? cfg.getString("gerrit", null, "installCommitMsgHookCommand") : null; } - validators.add(new ConfigValidator(refControl, repo, allUsers)); - validators.add(new BannedCommitsValidator(rejectCommits)); - validators.add(new PluginCommitValidationListener(commitValidationListeners)); - List<CommitValidationMessage> messages = new LinkedList<>(); - - try { - for (CommitValidationListener commitValidator : validators) { - messages.addAll(commitValidator.onCommitReceived(receiveEvent)); + 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 NONE: + return none(); + default: + throw new IllegalArgumentException("unspported policy: " + policy); } - } catch (CommitValidationException e) { - // Keep the old messages (and their order) in case of an exception - messages.addAll(e.getMessages()); - throw new CommitValidationException(e.getMessage(), messages); } - return messages; + + 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))); + } + } + + private CommitValidators forGerritCommits(RefControl refControl, + SshInfo sshInfo, Repository repo) { + 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 PluginCommitValidationListener(pluginValidators))); + } + + private CommitValidators none() { + return new CommitValidators(ImmutableList.<CommitValidationListener>of()); + } } - public List<CommitValidationMessage> validateForGerritCommits( + private final List<CommitValidationListener> validators; + + CommitValidators(List<CommitValidationListener> validators) { + this.validators = validators; + } + + public List<CommitValidationMessage> validate( CommitReceivedEvent receiveEvent) throws CommitValidationException { - - List<CommitValidationListener> validators = new LinkedList<>(); - - validators.add(new UploadMergesPermissionValidator(refControl)); - validators.add(new AmendedGerritMergeCommitValidationListener( - refControl, gerritIdent)); - validators.add(new AuthorUploaderValidator(refControl, canonicalWebUrl)); - validators.add(new SignedOffByValidator(refControl)); - if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName()) - || ReceiveCommits.NEW_PATCHSET.matcher( - receiveEvent.command.getRefName()).matches()) { - validators.add(new ChangeIdValidator(refControl, canonicalWebUrl, - installCommitMsgHookCommand, sshInfo)); - } - validators.add(new ConfigValidator(refControl, repo, allUsers)); - validators.add(new PluginCommitValidationListener(commitValidationListeners)); - List<CommitValidationMessage> messages = new LinkedList<>(); - try { for (CommitValidationListener commitValidator : validators) { messages.addAll(commitValidator.onCommitReceived(receiveEvent)); @@ -221,6 +216,9 @@ @Override public List<CommitValidationMessage> onCommitReceived( CommitReceivedEvent receiveEvent) throws CommitValidationException { + if (!shouldValidateChangeId(receiveEvent)) { + return Collections.emptyList(); + } RevCommit commit = receiveEvent.commit; List<CommitValidationMessage> messages = new LinkedList<>(); List<String> idList = commit.getFooterLines(FooterConstants.CHANGE_ID); @@ -255,6 +253,11 @@ return Collections.emptyList(); } + private static boolean shouldValidateChangeId(CommitReceivedEvent event) { + return MagicBranch.isMagicBranch(event.command.getRefName()) + || NEW_PATCHSET.matcher(event.command.getRefName()).matches(); + } + private CommitValidationMessage getMissingChangeIdErrorMsg( final String errMsg, final RevCommit c) { StringBuilder sb = new StringBuilder();
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 bd74fff..c10b279 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
@@ -17,6 +17,7 @@ import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.gerrit.audit.AuditService; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.extensions.common.AccountInfo; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.DefaultInput; @@ -27,7 +28,6 @@ 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.client.AuthType; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.account.AccountCache;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java index 1e8bdf4..eafa74d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -50,6 +50,6 @@ @Override protected void formatChange() throws EmailException { - appendText(velocifyFile("Abandoned.vm")); + appendText(textTemplate("Abandoned")); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java index f825d1c..61ef92d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddKeySender.java
@@ -80,7 +80,8 @@ @Override protected void format() throws EmailException { - appendText(velocifyFile("AddKey.vm")); + appendText(textTemplate("AddKey")); + appendHtml(soyHtmlTemplate("AddKeyHtml")); } public String getEmail() { @@ -110,4 +111,19 @@ } return null; } + + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + soyContextEmailData.put("email", getEmail()); + soyContextEmailData.put("gpgKeys", getGpgKeys()); + soyContextEmailData.put("keyType", getKeyType()); + soyContextEmailData.put("sshKey", getSshKey()); + soyContextEmailData.put("userNameEmail", getUserNameEmail()); + } + + @Override + protected boolean useHtml() { + return true; + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java index badc706..b310feb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -55,6 +55,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -120,7 +121,7 @@ @Override protected void format() throws EmailException { formatChange(); - appendText(velocifyFile("ChangeFooter.vm")); + appendText(textTemplate("ChangeFooter")); try { TreeSet<String> names = new TreeSet<>(); for (Account.Id who : changeData.reviewers().all()) { @@ -199,7 +200,7 @@ } private void setChangeSubjectHeader() throws EmailException { - setHeader("Subject", velocifyFile("ChangeSubject.vm")); + setHeader("Subject", textTemplate("ChangeSubject")); } /** Get a link to the change; null if the server doesn't know its own address. */ @@ -435,11 +436,48 @@ velocityContext.put("patchSetInfo", patchSetInfo); } + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + + soyContext.put("changeId", change.getKey().get()); + soyContext.put("coverLetter", getCoverLetter()); + soyContext.put("fromName", getNameFor(fromId)); + + soyContextEmailData.put("unifiedDiff", getUnifiedDiff()); + soyContextEmailData.put("changeDetail", getChangeDetail()); + soyContextEmailData.put("changeUrl", getChangeUrl()); + soyContextEmailData.put("includeDiff", getIncludeDiff()); + + Map<String, String> changeData = new HashMap<>(); + changeData.put("subject", change.getSubject()); + changeData.put("originalSubject", change.getOriginalSubject()); + changeData.put("ownerEmail", getNameEmailFor(change.getOwner())); + soyContext.put("change", changeData); + + String subject = change.getSubject(); + changeData.put("subject", subject); + // shortSubject is the subject limited to 63 characters, with an ellipsis if + // it exceeds that. + if (subject.length() < 64) { + changeData.put("shortSubject", subject); + } else { + changeData.put("shortSubject", subject.substring(0, 60) + "..."); + } + + Map<String, Object> patchSetData = new HashMap<>(); + patchSetData.put("patchSetId", patchSet.getPatchSetId()); + patchSetData.put("refName", patchSet.getRefName()); + soyContext.put("patchSet", patchSetData); + + // TODO(wyatta): patchSetInfo + } + public boolean getIncludeDiff() { return args.settings.includeDiff; } - private static int HEAP_EST_SIZE = 32 * 1024; + private static final int HEAP_EST_SIZE = 32 * 1024; /** Show patch set as unified difference. */ public String getUnifiedDiff() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java index b56b737..2424a6d 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -97,12 +97,12 @@ @Override public void formatChange() throws EmailException { - appendText(velocifyFile("Comment.vm")); + appendText(textTemplate("Comment")); } @Override public void formatFooter() throws EmailException { - appendText(velocifyFile("CommentFooter.vm")); + appendText(textTemplate("CommentFooter")); } public boolean hasInlineComments() { @@ -283,4 +283,11 @@ return null; } } + + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + soyContextEmailData.put("inlineComments", getInlineComments()); + soyContextEmailData.put("hasInlineComments", hasInlineComments()); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java index 75f9f82..8a26a10 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteReviewerSender.java
@@ -65,7 +65,7 @@ @Override protected void formatChange() throws EmailException { - appendText(velocifyFile("DeleteReviewer.vm")); + appendText(textTemplate("DeleteReviewer")); } public List<String> getReviewerNames() { @@ -78,4 +78,10 @@ } return names; } + + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + soyContextEmailData.put("reviewerNames", getReviewerNames()); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java index d861109..66f8123 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/DeleteVoteSender.java
@@ -49,6 +49,6 @@ @Override protected void formatChange() throws EmailException { - appendText(velocifyFile("DeleteVote.vm")); + appendText(textTemplate("DeleteVote")); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java index 68e5e50..71c294b 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -1,4 +1,4 @@ -// Copyright (C) 2010 The Android Open Source Project +// 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. @@ -30,6 +30,7 @@ import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.config.AnonymousCowardName; import com.google.gerrit.server.config.CanonicalWebUrl; +import com.google.gerrit.server.config.SitePaths; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gerrit.server.notedb.ChangeNotes; @@ -43,6 +44,7 @@ import com.google.gerrit.server.validators.OutgoingEmailValidationListener; import com.google.inject.Inject; import com.google.inject.Provider; +import com.google.template.soy.tofu.SoyTofu; import org.apache.velocity.runtime.RuntimeInstance; import org.eclipse.jgit.lib.PersonIdent; @@ -69,11 +71,13 @@ final Provider<String> urlProvider; final AllProjectsName allProjectsName; final List<String> sshAddresses; + final SitePaths site; final ChangeQueryBuilder queryBuilder; final Provider<ReviewDb> db; final ChangeData.Factory changeDataFactory; final RuntimeInstance velocityRuntime; + final SoyTofu soyTofu; final EmailSettings settings; final DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners; final StarredChangesUtil starredChangesUtil; @@ -100,8 +104,10 @@ Provider<ReviewDb> db, ChangeData.Factory changeDataFactory, RuntimeInstance velocityRuntime, + @MailTemplates SoyTofu soyTofu, EmailSettings settings, @SshAdvertisedAddresses List<String> sshAddresses, + SitePaths site, DynamicSet<OutgoingEmailValidationListener> outgoingEmailValidationListeners, StarredChangesUtil starredChangesUtil, AccountIndexCollection accountIndexes, @@ -128,8 +134,10 @@ this.db = db; this.changeDataFactory = changeDataFactory; this.velocityRuntime = velocityRuntime; + this.soyTofu = soyTofu; this.settings = settings; this.sshAddresses = sshAddresses; + this.site = site; this.outgoingEmailValidationListeners = outgoingEmailValidationListeners; this.starredChangesUtil = starredChangesUtil; this.accountIndexes = accountIndexes;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java index 51f7ad1..0bc65bd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
@@ -32,6 +32,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.regex.Pattern; /** Creates a {@link FromAddressGenerator} from the {@link GerritServerConfig} */ @Singleton @@ -53,13 +54,15 @@ generator = new PatternGen(srvAddr, accountCache, anonymousCowardName, name, srvAddr.email); - } else if ("USER".equalsIgnoreCase(from)) { - generator = new UserGen(accountCache, srvAddr); - + String[] domains = cfg.getStringList("sendemail", null, "allowedDomain"); + Pattern domainPattern = MailUtil.glob(domains); + ParameterizedString namePattern = + new ParameterizedString("${user} (Code Review)"); + generator = new UserGen(accountCache, domainPattern, anonymousCowardName, + namePattern, srvAddr); } else if ("SERVER".equalsIgnoreCase(from)) { generator = new ServerGen(srvAddr); - } else { final Address a = Address.parse(from); final ParameterizedString name = a.name != null ? new ParameterizedString(a.name) : null; @@ -84,11 +87,31 @@ static final class UserGen implements FromAddressGenerator { private final AccountCache accountCache; - private final Address srvAddr; + private final Pattern domainPattern; + private final String anonymousCowardName; + private final ParameterizedString nameRewriteTmpl; + private final Address serverAddress; - UserGen(AccountCache accountCache, Address srvAddr) { + /** + * From address generator for USER mode + * + * @param accountCache get user account from id + * @param domainPattern allowed user domain pattern that Gerrit can send as + * the user + * @param anonymousCowardName name used when user's full name is missing + * @param nameRewriteTmpl name template used for rewriting the sender's name + * when Gerrit can not send as the user + * @param serverAddress serverAddress.name is used when fromId is null and + * serverAddress.email is used when Gerrit can not send as the user + */ + UserGen(AccountCache accountCache, Pattern domainPattern, + String anonymousCowardName, ParameterizedString nameRewriteTmpl, + Address serverAddress) { this.accountCache = accountCache; - this.srvAddr = srvAddr; + this.domainPattern = domainPattern; + this.anonymousCowardName = anonymousCowardName; + this.nameRewriteTmpl = nameRewriteTmpl; + this.serverAddress = serverAddress; } @Override @@ -98,14 +121,44 @@ @Override public Address from(final Account.Id fromId) { + String senderName; if (fromId != null) { Account a = accountCache.get(fromId).getAccount(); + String fullName = a.getFullName(); String userEmail = a.getPreferredEmail(); - return new Address( - a.getFullName(), - userEmail != null ? userEmail : srvAddr.getEmail()); + if (canRelay(userEmail)) { + return new Address(fullName, userEmail); + } + + if (fullName == null || "".equals(fullName.trim())) { + fullName = anonymousCowardName; + } + senderName = nameRewriteTmpl.replace("user", fullName).toString(); + } else { + senderName = serverAddress.name; } - return srvAddr; + + String senderEmail; + ParameterizedString senderEmailPattern = + new ParameterizedString(serverAddress.email); + if (senderEmailPattern.getParameterNames().isEmpty()) { + senderEmail = senderEmailPattern.getRawPattern(); + } else { + senderEmail = senderEmailPattern.replace("userHash", hashOf(senderName)) + .toString(); + } + return new Address(senderName, senderEmail); + } + + /** check if Gerrit is allowed to send from {@code userEmail}. */ + private boolean canRelay(String userEmail) { + if (userEmail != null) { + int index = userEmail.indexOf('@'); + if (index > 0 && index < userEmail.length() - 1) { + return domainPattern.matcher(userEmail.substring(index + 1)).matches(); + } + } + return false; } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java new file mode 100644 index 0000000..5e0d8b6 --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailSoyTofuProvider.java
@@ -0,0 +1,94 @@ +// 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; + +import com.google.common.io.CharStreams; +import com.google.common.io.Resources; +import com.google.gerrit.server.config.SitePaths; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import com.google.inject.Singleton; +import com.google.template.soy.SoyFileSet; +import com.google.template.soy.tofu.SoyTofu; + +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +/** Configures Soy Tofu object for rendering email templates. */ +@Singleton +public class MailSoyTofuProvider implements Provider<SoyTofu> { + + // Note: will fail to construct the tofu object if this array is empty. + private static final String[] TEMPLATES = { + "Abandoned.soy", + "AddKey.soy", + "AddKeyHtml.soy", + "ChangeSubject.soy", + "ChangeFooter.soy", + "Comment.soy", + "CommentFooter.soy", + "DeleteReviewer.soy", + "DeleteVote.soy", + "Footer.soy", + "FooterHtml.soy", + "Merged.soy", + "NewChange.soy", + "RegisterNewEmail.soy", + "ReplacePatchSet.soy", + "Restored.soy", + "Reverted.soy", + }; + + private final SitePaths site; + + @Inject + MailSoyTofuProvider(SitePaths site) { + this.site = site; + } + + @Override + public SoyTofu get() throws ProvisionException { + SoyFileSet.Builder builder = SoyFileSet.builder(); + for (String name : TEMPLATES) { + addTemplate(builder, name); + } + return builder.build().compileToTofu(); + } + + private void addTemplate(SoyFileSet.Builder builder, String name) + throws ProvisionException { + // Load as a file in the mail templates directory if present. + Path tmpl = site.mail_dir.resolve(name); + if (Files.isRegularFile(tmpl)) { + String content; + try (Reader r = Files.newBufferedReader(tmpl, StandardCharsets.UTF_8)) { + content = CharStreams.toString(r); + } catch (IOException err) { + throw new ProvisionException("Failed to read template file " + + tmpl.toAbsolutePath().toString(), err); + } + builder.add(content, tmpl.toAbsolutePath().toString()); + return; + } + + // Otherwise load the template as a resource. + String resourcePath = "com/google/gerrit/server/mail/" + name; + builder.add(Resources.getResource(resourcePath)); + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailTemplates.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailTemplates.java new file mode 100644 index 0000000..72fdaae --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailTemplates.java
@@ -0,0 +1,25 @@ +// 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; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.google.inject.BindingAnnotation; + +import java.lang.annotation.Retention; + +@Retention(RUNTIME) +@BindingAnnotation +public @interface MailTemplates {}
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 048a4a4..8a132cd 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
@@ -32,6 +32,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.regex.Pattern; public class MailUtil { public static MailRecipients getRecipientsFromFooters( @@ -124,4 +125,19 @@ return Collections.unmodifiableSet(all); } } + + /** allow wildcard matching for {@code domains} */ + public static Pattern glob(String[] domains) { + // if domains is not set, match anything + if (domains == null || domains.length == 0) { + return Pattern.compile(".*"); + } + + StringBuilder sb = new StringBuilder(""); + for (String domain : domains) { + String quoted = "\\Q" + domain.replace("\\E", "\\E\\\\E\\Q") + "\\E|"; + sb.append(quoted.replace("*", "\\E.*\\Q")); + } + return Pattern.compile(sb.substring(0, sb.length() - 1)); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java index f6c3d0f..c2a3cdd 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -58,7 +58,7 @@ @Override protected void formatChange() throws EmailException { - appendText(velocifyFile("Merged.vm")); + appendText(textTemplate("Merged")); } public String getApprovals() { @@ -123,4 +123,10 @@ txt.append('\n'); return txt.toString(); } + + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + soyContextEmailData.put("approvals", getApprovals()); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java index 62385d9..a7eb0af 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
@@ -67,7 +67,7 @@ @Override protected void formatChange() throws EmailException { - appendText(velocifyFile("NewChange.vm")); + appendText(textTemplate("NewChange")); } public List<String> getReviewerNames() { @@ -80,4 +80,10 @@ } return names; } + + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + soyContextEmailData.put("reviewerNames", getReviewerNames()); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java index de338ec..85dd800 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java
@@ -25,6 +25,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; +import java.util.Map; + /** * Common class for notifications that are related to a project and branch */ @@ -103,4 +106,20 @@ velocityContext.put("projectName", branch.getParentKey().get()); velocityContext.put("branch", branch); } + + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + + String projectName = branch.getParentKey().get(); + soyContext.put("projectName", projectName); + // shortProjectName is the project name with the path abbreviated. + soyContext.put("shortProjectName", projectName.replaceAll("/.*/", "...")); + + soyContextEmailData.put("sshHost", getSshHost()); + + Map<String, String> branchData = new HashMap<>(); + branchData.put("shortName", branch.getShortName()); + soyContext.put("branch", branchData); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java index 6200688..78fe631 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -1,4 +1,4 @@ -// Copyright (C) 2009 The Android Open Source Project +// 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. @@ -18,6 +18,7 @@ import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED; import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.common.io.BaseEncoding; import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.client.GeneralPreferencesInfo; @@ -28,6 +29,7 @@ import com.google.gerrit.server.validators.OutgoingEmailValidationListener; import com.google.gerrit.server.validators.ValidationException; import com.google.gwtorm.server.OrmException; +import com.google.template.soy.data.SanitizedContent; import org.apache.commons.lang.StringUtils; import org.apache.velocity.Template; @@ -43,8 +45,12 @@ import java.io.StringWriter; import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collection; +import java.util.concurrent.ThreadLocalRandom; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; @@ -63,9 +69,11 @@ private final Map<String, EmailHeader> headers; private final Set<Address> smtpRcptTo = new HashSet<>(); private Address smtpFromAddress; - private StringBuilder body; + private StringBuilder textBody; + private StringBuilder htmlBody; protected VelocityContext velocityContext; - + protected Map<String, Object> soyContext; + protected Map<String, Object> soyContextEmailData; protected final EmailArguments args; protected Account.Id fromId; protected NotifyHandling notify = NotifyHandling.ALL; @@ -102,7 +110,10 @@ init(); format(); - appendText(velocifyFile("Footer.vm")); + appendText(textTemplate("Footer")); + if (useHtml()) { + appendHtml(soyHtmlTemplate("FooterHtml")); + } if (shouldSendMessage()) { if (fromId != null) { final Account fromUser = args.accountCache.get(fromId).getAccount(); @@ -136,12 +147,29 @@ } } + String textPart = textBody.toString(); OutgoingEmailValidationListener.Args va = new OutgoingEmailValidationListener.Args(); va.messageClass = messageClass; va.smtpFromAddress = smtpFromAddress; va.smtpRcptTo = smtpRcptTo; va.headers = headers; - va.body = body.toString(); + + if (useHtml()) { + String htmlPart = htmlBody.toString(); + String boundary = generateMultipartBoundary(textPart, htmlPart); + + va.body = buildMultipartBody(boundary, textPart, htmlPart); + va.textBody = textPart; + va.htmlBody = htmlPart; + va.headers.put("Content-Type", new EmailHeader.String( + "multipart/alternative; " + + "boundary=\"" + boundary + "\"; " + + "charset=UTF-8")); + } else { + va.body = textPart; + va.textBody = textPart; + } + for (OutgoingEmailValidationListener validator : args.outgoingEmailValidationListeners) { try { validator.validateOutgoingEmail(va); @@ -154,6 +182,49 @@ } } + protected String buildMultipartBody(String boundary, String textPart, + String htmlPart) { + return + // Output the text part: + "--" + boundary + "\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "Content-Transfer-Encoding: 8bit\r\n" + + "\r\n" + + textPart + "\r\n" + + // Output the HTML part: + + "--" + boundary + "\r\n" + + "Content-Type: text/html; charset=UTF-8\r\n" + + "Content-Transfer-Encoding: 8bit\r\n" + + "\r\n" + + htmlPart + "\r\n" + + // Output the closing boundary. + + "--" + boundary + "--\r\n"; + } + + protected String generateMultipartBoundary(String textBody, String htmlBody) + throws EmailException { + byte[] bytes = new byte[8]; + ThreadLocalRandom rng = ThreadLocalRandom.current(); + + // The probability of the boundary being valid is approximately + // (2^64 - len(message)) / 2^64. + // + // The message is much shorter than 2^64 bytes, so if two tries don't + // suffice, something is seriously wrong. + for (int i = 0; i < 2; i++) { + rng.nextBytes(bytes); + String boundary = BaseEncoding.base64().encode(bytes); + String encBoundary = "--" + boundary; + if (textBody.contains(encBoundary) || htmlBody.contains(encBoundary)) { + continue; + } + return boundary; + } + throw new EmailException("Gave up generating unique MIME boundary"); + } + /** Format the message body by calling {@link #appendText(String)}. */ protected abstract void format() throws EmailException; @@ -164,6 +235,7 @@ */ protected void init() throws EmailException { setupVelocityContext(); + setupSoyContext(); smtpFromAddress = args.fromAddressGenerator.from(fromId); setHeader("Date", new Date()); @@ -185,7 +257,8 @@ } setHeader("X-Gerrit-MessageType", messageClass); - body = new StringBuilder(); + textBody = new StringBuilder(); + htmlBody = new StringBuilder(); if (fromId != null && args.fromAddressGenerator.isGenericAddress(fromId)) { appendText(getFromLine()); @@ -260,7 +333,14 @@ /** Append text to the outgoing email body. */ protected void appendText(final String text) { if (text != null) { - body.append(text); + textBody.append(text); + } + } + + /** Append html to the outgoing email body. */ + protected void appendHtml(String html) { + if (html != null) { + htmlBody.append(html); } } @@ -334,7 +414,7 @@ } protected boolean shouldSendMessage() { - if (body.length() == 0) { + if (textBody.length() == 0) { // If we have no message body, don't send. return false; } @@ -428,6 +508,18 @@ velocityContext.put("StringUtils", StringUtils.class); } + protected void setupSoyContext() { + soyContext = new HashMap<>(); + + soyContext.put("messageClass", messageClass); + + soyContextEmailData = new HashMap<>(); + soyContextEmailData.put("settingsUrl", getSettingsUrl()); + soyContextEmailData.put("gerritHost", getGerritHost()); + soyContextEmailData.put("gerritUrl", getGerritUrl()); + soyContext.put("email", soyContextEmailData); + } + protected String velocify(String template) throws EmailException { try { RuntimeInstance runtime = args.velocityRuntime; @@ -463,6 +555,37 @@ } } + private String soyTemplate(String name, SanitizedContent.ContentKind kind) { + return args.soyTofu + .newRenderer("com.google.gerrit.server.mail.template." + name) + .setContentKind(kind) + .setData(soyContext) + .render(); + } + + protected String soyTextTemplate(String name) { + return soyTemplate(name, SanitizedContent.ContentKind.TEXT); + } + + protected String soyHtmlTemplate(String name) { + return soyTemplate(name, SanitizedContent.ContentKind.HTML); + } + + /** + * Evaluate the named template according to the following priority: + * 1) Velocity file override, OR... + * 2) Soy file override, OR... + * 3) Soy resource. + */ + protected String textTemplate(String name) throws EmailException { + String velocityName = name + ".vm"; + Path filePath = args.site.mail_dir.resolve(velocityName); + if (Files.isRegularFile(filePath)) { + return velocifyFile(velocityName); + } + return soyTextTemplate(name); + } + public String joinStrings(Iterable<Object> in, String joiner) { return joinStrings(in.iterator(), joiner); } @@ -504,4 +627,9 @@ private static String safeToString(Object obj) { return obj != null ? obj.toString() : ""; } + + /** Override this method to enable HTML in a subclass. */ + protected boolean useHtml() { + return false; + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java index cfdeb8f..405d9f9 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
@@ -51,7 +51,7 @@ @Override protected void format() throws EmailException { - appendText(velocifyFile("RegisterNewEmail.vm")); + appendText(textTemplate("RegisterNewEmail")); } public String getUserNameEmail() { @@ -69,4 +69,12 @@ public boolean isAllowed() { return args.emailSender.canEmail(addr); } + + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + soyContextEmailData + .put("emailRegistrationToken", getEmailRegistrationToken()); + soyContextEmailData.put("userNameEmail", getUserNameEmail()); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java index df9f20e..d86aed3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
@@ -72,17 +72,26 @@ @Override protected void formatChange() throws EmailException { - appendText(velocifyFile("ReplacePatchSet.vm")); + appendText(textTemplate("ReplacePatchSet")); } public List<String> getReviewerNames() { - if (reviewers.isEmpty()) { - return null; - } List<String> names = new ArrayList<>(); for (Account.Id id : reviewers) { + if (id.equals(fromId)) { + continue; + } names.add(getNameFor(id)); } + if (names.isEmpty()) { + return null; + } return names; } + + @Override + protected void setupSoyContext() { + super.setupSoyContext(); + soyContextEmailData.put("reviewerNames", getReviewerNames()); + } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java index d946eb2..5b6d9bc 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -49,6 +49,6 @@ @Override protected void formatChange() throws EmailException { - appendText(velocifyFile("Restored.vm")); + appendText(textTemplate("Restored")); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java index 2c9c37e..7d1690c 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
@@ -47,6 +47,6 @@ @Override protected void formatChange() throws EmailException { - appendText(velocifyFile("Reverted.vm")); + appendText(textTemplate("Reverted")); } }
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 8272aaf..a8e39ee 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
@@ -29,12 +29,12 @@ import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_TOPIC; import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES; +import com.google.auto.value.AutoValue; import com.google.common.base.Enums; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Splitter; -import com.google.common.base.Supplier; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.HashBasedTable; import com.google.common.collect.ImmutableSet; @@ -46,6 +46,7 @@ import com.google.common.collect.Table; import com.google.common.collect.Tables; import com.google.common.primitives.Ints; +import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.LabelType; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.metrics.Timer1; @@ -85,7 +86,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.NavigableSet; import java.util.Objects; import java.util.Set; @@ -97,6 +97,20 @@ private static final RevId PARTIAL_PATCH_SET = new RevId("INVALID PARTIAL PATCH SET"); + @AutoValue + abstract static class ApprovalKey { + abstract PatchSet.Id psId(); + abstract Account.Id accountId(); + abstract String label(); + @Nullable abstract String tag(); + + private static ApprovalKey create(PatchSet.Id psId, Account.Id accountId, + String label, @Nullable String tag) { + return new AutoValue_ChangeNotesParser_ApprovalKey( + psId, accountId, label, tag); + } + } + // Private final members initialized in the constructor. private final ChangeNoteUtil noteUtil; private final NoteDbMetrics metrics; @@ -114,8 +128,7 @@ private final TreeMap<PatchSet.Id, PatchSet> patchSets; private final Set<PatchSet.Id> deletedPatchSets; private final Map<PatchSet.Id, PatchSetState> patchSetStates; - private final Map<PatchSet.Id, - Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>>> approvals; + private final Map<ApprovalKey, PatchSetApproval> approvals; private final List<ChangeMessage> allChangeMessages; private final Multimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet; @@ -142,7 +155,7 @@ this.walk = walk; this.noteUtil = noteUtil; this.metrics = metrics; - approvals = new HashMap<>(); + approvals = new LinkedHashMap<>(); reviewers = HashBasedTable.create(); allPastReviewers = new ArrayList<>(); reviewerUpdates = new ArrayList<>(); @@ -210,14 +223,15 @@ } private Multimap<PatchSet.Id, PatchSetApproval> buildApprovals() { - Multimap<PatchSet.Id, PatchSetApproval> result = - ArrayListMultimap.create(approvals.keySet().size(), 3); - for (Table<?, ?, Optional<PatchSetApproval>> curr : approvals.values()) { - for (Optional<PatchSetApproval> psa : curr.values()) { - if (psa.isPresent()) { - result.put(psa.get().getPatchSetId(), psa.get()); - } + Multimap<PatchSet.Id, PatchSetApproval> result = ArrayListMultimap.create(); + for (PatchSetApproval a : approvals.values()) { + if (patchSetStates.get(a.getPatchSetId()) == PatchSetState.DELETED) { + continue; // Patch set was explicitly deleted. + } else if (allPastReviewers.contains(a.getAccountId()) + && !reviewers.containsRow(a.getAccountId())) { + continue; // Reviewer was explicitly removed. } + result.put(a.getPatchSetId(), a); } for (Collection<PatchSetApproval> v : result.asMap().values()) { Collections.sort((List<PatchSetApproval>) v, ChangeNotes.PSA_BY_TIME); @@ -606,7 +620,7 @@ "patch set %s requires an identified user as uploader", psId.get()); } if (line.startsWith("-")) { - parseRemoveApproval(psId, accountId, line); + parseRemoveApproval(psId, accountId, ts, line); } else { parseAddApproval(psId, accountId, ts, line); } @@ -637,39 +651,37 @@ throw pe; } - Entry<String, String> label = Maps.immutableEntry(l.label(), tag); - Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> curr = - getApprovalsTableIfNoVotePresent(psId, accountId, label); - if (curr != null) { - PatchSetApproval psa = new PatchSetApproval( - new PatchSetApproval.Key( - psId, - accountId, - new LabelId(l.label())), - l.value(), - ts); - psa.setTag(tag); - curr.put(accountId, label, Optional.of(psa)); + PatchSetApproval psa = new PatchSetApproval( + new PatchSetApproval.Key( + psId, + accountId, + new LabelId(l.label())), + l.value(), + ts); + psa.setTag(tag); + ApprovalKey k = ApprovalKey.create(psId, accountId, l.label(), tag); + if (!approvals.containsKey(k)) { + approvals.put(k, psa); } } private void parseRemoveApproval(PatchSet.Id psId, Account.Id committerId, - String line) throws ConfigInvalidException { + Timestamp ts, String line) throws ConfigInvalidException { Account.Id accountId; - Entry<String, String> label; + String label; int s = line.indexOf(' '); if (s > 0) { - label = Maps.immutableEntry(line.substring(1, s), tag); + label = line.substring(1, s); PersonIdent ident = RawParseUtils.parsePersonIdent(line.substring(s + 1)); checkFooter(ident != null, FOOTER_LABEL, line); accountId = noteUtil.parseIdent(ident, id); } else { - label = Maps.immutableEntry(line.substring(1), tag); + label = line.substring(1); accountId = committerId; } try { - LabelType.checkNameInternal(label.getKey()); + LabelType.checkNameInternal(label); } catch (IllegalArgumentException e) { ConfigInvalidException pe = parseException("invalid %s: %s", FOOTER_LABEL, line); @@ -677,38 +689,25 @@ throw pe; } - Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> curr = - getApprovalsTableIfNoVotePresent(psId, accountId, label); - if (curr != null) { - curr.put(accountId, label, Optional.<PatchSetApproval> absent()); + // Store an actual 0-vote approval in the map for a removed approval, for + // several reasons: + // - This is closer to the ReviewDb representation, which leads to less + // confusion and special-casing of NoteDb. + // - More importantly, ApprovalCopier needs an actual approval in order to + // block copying an earlier approval over a later delete. + PatchSetApproval remove = new PatchSetApproval( + new PatchSetApproval.Key( + psId, + accountId, + new LabelId(label)), + (short) 0, + ts); + ApprovalKey k = ApprovalKey.create(psId, accountId, label, tag); + if (!approvals.containsKey(k)) { + approvals.put(k, remove); } } - private Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> - getApprovalsTableIfNoVotePresent(PatchSet.Id psId, Account.Id accountId, - Entry<String, String> label) { - - Table<Account.Id, Entry<String, String>, Optional<PatchSetApproval>> curr = - approvals.get(psId); - if (curr != null) { - if (curr.contains(accountId, label)) { - return null; - } - } else { - curr = Tables.newCustomTable( - Maps.<Account.Id, Map<Entry<String, String>, Optional<PatchSetApproval>>> - newHashMapWithExpectedSize(2), - new Supplier<Map<Entry<String, String>, Optional<PatchSetApproval>>>() { - @Override - public Map<Entry<String, String>, Optional<PatchSetApproval>> get() { - return new LinkedHashMap<>(); - } - }); - approvals.put(psId, curr); - } - return curr; - } - private void parseSubmitRecords(List<String> lines) throws ConfigInvalidException { SubmitRecord rec = null; @@ -787,9 +786,6 @@ Table.Cell<Account.Id, ReviewerStateInternal, Timestamp> e = rit.next(); if (e.getColumnKey() == ReviewerStateInternal.REMOVED) { rit.remove(); - for (Table<Account.Id, ?, ?> curr : approvals.values()) { - curr.rowKeySet().remove(e.getRowKey()); - } } } } @@ -832,13 +828,14 @@ // Post-process other collections to remove items corresponding to deleted // patch sets. This is safer than trying to prevent insertion, as it will // also filter out items racily added after the patch set was deleted. + // + // Approvals are filtered in buildApprovals(). NavigableSet<PatchSet.Id> all = patchSets.navigableKeySet(); if (!all.isEmpty()) { currentPatchSetId = all.last(); } else { currentPatchSetId = null; } - approvals.keySet().retainAll(all); changeMessagesByPatchSet.keys().retainAll(all); for (Iterator<ChangeMessage> it = allChangeMessages.iterator();
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 e570b3a..f8c8b49 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
@@ -14,6 +14,8 @@ package com.google.gerrit.server.patch; +import static java.nio.charset.StandardCharsets.UTF_8; + import com.google.gerrit.common.errors.NoSuchEntityException; import com.google.gerrit.reviewdb.client.Patch; @@ -151,7 +153,7 @@ return new Text(repo.open(tw.getObjectId(0), Constants.OBJ_BLOB)); } else if (tw.getFileMode(0).getObjectType() == Constants.OBJ_COMMIT) { String str = "Subproject commit " + ObjectId.toString(tw.getObjectId(0)); - return new Text(str.getBytes()); + return new Text(str.getBytes(UTF_8)); } else { return Text.EMPTY; }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java index 2ccc9f1..fab66cb 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java
@@ -21,6 +21,10 @@ super(message); } + public PatchListNotAvailableException(String message, Throwable cause) { + super(message, cause); + } + public PatchListNotAvailableException(Throwable cause) { super(cause); }
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 a7d2523..754d995 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
@@ -15,7 +15,6 @@ package com.google.gerrit.server.patch; import static com.google.common.base.Preconditions.checkArgument; -import static com.google.gerrit.server.util.GitUtil.getParent; import com.google.common.base.Optional; import com.google.gerrit.common.Nullable; @@ -214,8 +213,6 @@ bId = toObjectId(psEntityB); if (parentNum < 0) { aId = psEntityA != null ? toObjectId(psEntityA) : null; - } else { - aId = getParent(git, bId, parentNum); } try { @@ -247,7 +244,10 @@ } private PatchListKey keyFor(final Whitespace whitespace) { - return new PatchListKey(aId, bId, whitespace); + if (parentNum < 0) { + return new PatchListKey(aId, bId, whitespace); + } + return PatchListKey.againstParentNum(parentNum + 1, bId, whitespace); } private PatchList listFor(final PatchListKey key)
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 9086b6a..5bc7f40 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
@@ -269,7 +269,7 @@ /** Can this user rebase this change? */ public boolean canRebase(ReviewDb db) throws OrmException { - return (isOwner() || getRefControl().canSubmit() + return (isOwner() || getRefControl().canSubmit(isOwner()) || getRefControl().canRebase()) && !isPatchSetLocked(db); } @@ -424,7 +424,7 @@ } public boolean canSubmit() { - return getRefControl().canSubmit(); + return getRefControl().canSubmit(isOwner()); } public boolean canSubmitAs() {
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 446fa72..9e090c4 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
@@ -116,7 +116,7 @@ if (isSigned) { throw new MethodNotAllowedException( "Cannot create signed tag \"" + ref + "\""); - } else if (isAnnotated && !refControl.canPerform(Permission.PUSH_TAG)) { + } 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 + "\"");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java index 47942be..82ea155 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FileResource.java
@@ -14,16 +14,39 @@ package com.google.gerrit.server.project; +import com.google.gerrit.extensions.restapi.IdString; +import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestResource; import com.google.gerrit.extensions.restapi.RestView; +import com.google.gerrit.server.git.GitRepositoryManager; import com.google.inject.TypeLiteral; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; + +import java.io.IOException; public class FileResource implements RestResource { public static final TypeLiteral<RestView<FileResource>> FILE_KIND = new TypeLiteral<RestView<FileResource>>() {}; + public static FileResource create(GitRepositoryManager repoManager, + ProjectControl project, ObjectId rev, String path) + throws ResourceNotFoundException, IOException { + try (Repository repo = + repoManager.openRepository(project.getProject().getNameKey()); + RevWalk rw = new RevWalk(repo)) { + RevTree tree = rw.parseTree(rev); + if (TreeWalk.forPath(repo, path, tree) != null) { + return new FileResource(project, rev, path); + } + } + throw new ResourceNotFoundException(IdString.fromDecoded(path)); + } + private final ProjectControl project; private final ObjectId rev; private final String path;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java index d0460d5..dcb8747 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesCollection.java
@@ -19,19 +19,25 @@ 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.git.GitRepositoryManager; import com.google.inject.Inject; import com.google.inject.Singleton; import org.eclipse.jgit.lib.ObjectId; +import java.io.IOException; + @Singleton public class FilesCollection implements ChildCollection<BranchResource, FileResource> { private final DynamicMap<RestView<FileResource>> views; + private final GitRepositoryManager repoManager; @Inject - FilesCollection(DynamicMap<RestView<FileResource>> views) { + FilesCollection(DynamicMap<RestView<FileResource>> views, + GitRepositoryManager repoManager) { this.views = views; + this.repoManager = repoManager; } @Override @@ -40,11 +46,10 @@ } @Override - public FileResource parse(BranchResource parent, IdString id) { - return new FileResource( - parent.getControl(), - ObjectId.fromString(parent.getRevision()), - id.get()); + public FileResource parse(BranchResource parent, IdString id) + throws ResourceNotFoundException, IOException { + return FileResource.create(repoManager, parent.getControl(), + ObjectId.fromString(parent.getRevision()), id.get()); } @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java index 8e0aab8..64a5fb2 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/FilesInCommitCollection.java
@@ -19,17 +19,24 @@ 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.Patch; +import com.google.gerrit.server.git.GitRepositoryManager; import com.google.inject.Inject; import com.google.inject.Singleton; +import java.io.IOException; + @Singleton public class FilesInCommitCollection implements ChildCollection<CommitResource, FileResource> { private final DynamicMap<RestView<FileResource>> views; + private final GitRepositoryManager repoManager; @Inject - FilesInCommitCollection(DynamicMap<RestView<FileResource>> views) { + FilesInCommitCollection(DynamicMap<RestView<FileResource>> views, + GitRepositoryManager repoManager) { this.views = views; + this.repoManager = repoManager; } @Override @@ -39,8 +46,13 @@ @Override public FileResource parse(CommitResource parent, IdString id) - throws ResourceNotFoundException { - return new FileResource(parent.getProject(), parent.getCommit(), id.get()); + throws ResourceNotFoundException, IOException { + if (Patch.COMMIT_MSG.equals(id.get())) { + return new FileResource(parent.getProject(), parent.getCommit(), + id.get()); + } + return FileResource.create(repoManager, parent.getProject(), + parent.getCommit(), id.get()); } @Override
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 a862ac2..ce1bab4 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
@@ -18,8 +18,10 @@ import static com.google.gerrit.server.project.RefPattern.isRE; import com.google.auto.value.AutoValue; +import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.AccessSection; import com.google.gerrit.common.data.Permission; @@ -116,6 +118,8 @@ HashMap<String, List<PermissionRule>> permissions = new HashMap<>(); HashMap<String, List<PermissionRule>> overridden = new HashMap<>(); Map<PermissionRule, ProjectRef> ruleProps = Maps.newIdentityHashMap(); + Multimap<Project.NameKey, String> exclusivePermissionsByProject = + ArrayListMultimap.create(); for (AccessSection section : sections) { Project.NameKey project = sectionToProject.get(section); for (Permission permission : section.getPermissions()) { @@ -126,7 +130,8 @@ SeenRule s = SeenRule.create(section, permission, rule); boolean addRule; if (rule.isBlock()) { - addRule = true; + addRule = !exclusivePermissionsByProject.containsEntry(project, + permission.getName()); } else { addRule = seen.add(s) && !rule.isDeny() && !exclusivePermissionExists; } @@ -150,6 +155,7 @@ } if (permission.getExclusiveGroup()) { + exclusivePermissionsByProject.put(project, permission.getName()); exclusiveGroupPermissions.add(permission.getName()); } }
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 22e5d69..d9cc59c 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
@@ -307,8 +307,9 @@ /** Is this user a project owner? Ownership does not imply {@link #isVisible()} */ public boolean isOwner() { - return isDeclaredOwner() - || user.getCapabilities().canAdministrateServer(); + return (isDeclaredOwner() + && !controlForRef("refs/*").isBlocked(Permission.OWNER)) + || user.getCapabilities().canAdministrateServer(); } private boolean isDeclaredOwner() { @@ -327,8 +328,8 @@ /** @return true if the user can upload to at least one reference */ public Capable canPushToAtLeastOneRef() { - if (! canPerformOnAnyRef(Permission.PUSH) && - ! canPerformOnAnyRef(Permission.PUSH_TAG)) { + if (!canPerformOnAnyRef(Permission.PUSH) && + !canPerformOnAnyRef(Permission.CREATE_TAG)) { String pName = state.getProject().getName(); return new Capable("Upload denied for project '" + pName + "'"); }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java index e06fb86..52bbdf3 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutBranch.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.project; +import com.google.gerrit.extensions.api.projects.BranchInfo; import com.google.gerrit.extensions.api.projects.BranchInput; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestModifyView; @@ -23,7 +24,7 @@ public class PutBranch implements RestModifyView<BranchResource, BranchInput> { @Override - public Object apply(BranchResource rsrc, BranchInput input) + public BranchInfo apply(BranchResource rsrc, BranchInput input) throws ResourceConflictException { throw new ResourceConflictException("Branch \"" + rsrc.getBranchInfo().ref + "\" already exists");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutTag.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutTag.java index a87882e..1be4b0e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutTag.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutTag.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.project; +import com.google.gerrit.extensions.api.projects.TagInfo; import com.google.gerrit.extensions.api.projects.TagInput; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.RestModifyView; @@ -21,7 +22,7 @@ public class PutTag implements RestModifyView<TagResource, TagInput> { @Override - public Object apply(TagResource resource, TagInput input) + public TagInfo apply(TagResource resource, TagInput input) throws ResourceConflictException { throw new ResourceConflictException("Tag \"" + resource.getTagInfo().ref + "\" already exists");
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 ad41522..0365855 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
@@ -171,7 +171,7 @@ } /** @return true if this user can submit patch sets to this ref */ - public boolean canSubmit() { + public 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 @@ -180,7 +180,7 @@ // granting of powers beyond submitting to the configuration. return projectControl.isOwner(); } - return canPerform(Permission.SUBMIT) + return canPerform(Permission.SUBMIT, isChangeOwner) && canWrite(); } @@ -213,7 +213,27 @@ /** @return true if the user can rewind (force push) the reference. */ public boolean canForceUpdate() { - return (canPushWithForce() || canDelete()) && canWrite(); + if (!canWrite()) { + return false; + } + + if (canPushWithForce()) { + return true; + } + + switch (getUser().getAccessPath()) { + case GIT: + return false; + + case JSON_RPC: + case REST_API: + case SSH_COMMAND: + case UNKNOWN: + case WEB_BROWSER: + default: + return getUser().getCapabilities().canAdministrateServer() + || (isOwner() && !isForceBlocked(Permission.PUSH)); + } } public boolean canWrite() { @@ -251,43 +271,13 @@ if (!canWrite()) { return false; } - boolean owner; - boolean admin; - switch (getUser().getAccessPath()) { - case REST_API: - case JSON_RPC: - case UNKNOWN: - owner = isOwner(); - admin = getUser().getCapabilities().canAdministrateServer(); - break; - - case GIT: - case SSH_COMMAND: - case WEB_BROWSER: - default: - owner = false; - admin = false; - } if (object instanceof RevCommit) { - if (admin || (owner && !isBlocked(Permission.CREATE))) { - // Admin or project owner; bypass visibility check. - return true; - } else if (!canPerform(Permission.CREATE)) { + if (!canPerform(Permission.CREATE)) { // No create permissions. return false; - } else 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, (RevCommit) object)) { - // 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 false; + return canCreateCommit(db, repo, (RevCommit) object); } else if (object instanceof RevTag) { final RevTag tag = (RevTag) object; try (RevWalk rw = new RevWalk(repo)) { @@ -307,7 +297,18 @@ } else { valid = false; } - if (!valid && !owner && !canForgeCommitter()) { + if (!valid && !canForgeCommitter()) { + return false; + } + } + + RevObject tagObject = tag.getObject(); + if (tagObject instanceof RevCommit) { + if (!canCreateCommit(db, repo, (RevCommit) tagObject)) { + return false; + } + } else { + if (!canCreate(db, repo, tagObject)) { return false; } } @@ -316,14 +317,30 @@ // than if it doesn't have a PGP signature. // if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) { - return owner || canPerform(Permission.PUSH_SIGNED_TAG); + return canPerform(Permission.CREATE_SIGNED_TAG); } - return owner || canPerform(Permission.PUSH_TAG); + return canPerform(Permission.CREATE_TAG); } else { return false; } } + private boolean canCreateCommit(ReviewDb db, 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)) { + // 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 false; + } + private boolean isMergedIntoBranchOrTag(ReviewDb db, Repository repo, RevCommit commit) { try (RevWalk rw = new RevWalk(repo)) { @@ -359,7 +376,7 @@ switch (getUser().getAccessPath()) { case GIT: - return canPushWithForce(); + return canPushWithForce() || canPerform(Permission.DELETE); case JSON_RPC: case REST_API: @@ -369,7 +386,8 @@ default: return getUser().getCapabilities().canAdministrateServer() || (isOwner() && !isForceBlocked(Permission.PUSH)) - || canPushWithForce(); + || canPushWithForce() + || canPerform(Permission.DELETE); } } @@ -531,16 +549,21 @@ /** True if the user has this permission. Works only for non labels. */ boolean canPerform(String permissionName) { - return doCanPerform(permissionName, false); + return canPerform(permissionName, false); + } + + boolean canPerform(String permissionName, boolean isChangeOwner) { + return doCanPerform(permissionName, isChangeOwner, false); } /** True if the user is blocked from using this permission. */ public boolean isBlocked(String permissionName) { - return !doCanPerform(permissionName, true); + return !doCanPerform(permissionName, false, true); } - private boolean doCanPerform(String permissionName, boolean blockOnly) { - List<PermissionRule> access = access(permissionName); + private boolean doCanPerform(String permissionName, boolean isChangeOwner, + boolean blockOnly) { + List<PermissionRule> access = access(permissionName, isChangeOwner); List<PermissionRule> overridden = relevant.getOverridden(permissionName); Set<ProjectRef> allows = new HashSet<>(); Set<ProjectRef> blocks = new HashSet<>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java index cda548a..594763e 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
@@ -20,7 +20,9 @@ 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.Response; import com.google.gerrit.extensions.restapi.RestModifyView; +import com.google.gerrit.server.project.DashboardsCollection.DashboardInfo; import com.google.gerrit.server.project.SetDashboard.Input; import com.google.inject.Inject; import com.google.inject.Provider; @@ -44,7 +46,7 @@ } @Override - public Object apply(DashboardResource resource, Input input) + public Response<DashboardInfo> apply(DashboardResource resource, Input input) throws AuthException, BadRequestException, ResourceConflictException, MethodNotAllowedException, ResourceNotFoundException, IOException { if (resource.isProjectDefault()) {
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 a260d02..3e8b5e42 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
@@ -751,6 +751,7 @@ if (change == null) { throw new OrmException("Unable to load change " + legacyId); } + setPatchSets(null); return change; } @@ -784,8 +785,16 @@ if (c == null) { currentApprovals = Collections.emptyList(); } else { - currentApprovals = ImmutableList.copyOf(approvalsUtil.byPatchSet( - db, changeControl(), c.currentPatchSetId())); + try { + currentApprovals = ImmutableList.copyOf(approvalsUtil.byPatchSet( + db, changeControl(), c.currentPatchSetId())); + } catch (OrmException e) { + if (e.getCause() instanceof NoSuchChangeException) { + currentApprovals = Collections.emptyList(); + } else { + throw e; + } + } } } return currentApprovals; @@ -1002,9 +1011,17 @@ mergeable = true; } else { PatchSet ps = currentPatchSet(); - if (ps == null || !changeControl().isPatchVisible(ps, db)) { - return null; + try { + if (ps == null || !changeControl().isPatchVisible(ps, db)) { + return null; + } + } catch (OrmException e) { + if (e.getCause() instanceof NoSuchChangeException) { + return null; + } + throw e; } + try (Repository repo = repoManager.openRepository(project())) { Ref ref = repo.getRefDatabase().exactRef(c.getDest().get()); SubmitTypeRecord str = submitTypeRecord();
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 7c7417a..1bc878e 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
@@ -165,8 +165,9 @@ grant(config, heads, Permission.FORGE_COMMITTER, admin, owners); grant(config, heads, Permission.EDIT_TOPIC_NAME, true, admin, owners); - grant(config, tags, Permission.PUSH_TAG, admin, owners); - grant(config, tags, Permission.PUSH_SIGNED_TAG, admin, owners); + grant(config, tags, Permission.CREATE, admin, owners); + grant(config, tags, Permission.CREATE_TAG, admin, owners); + grant(config, tags, Permission.CREATE_SIGNED_TAG, admin, owners); grant(config, magic, Permission.PUSH, registered); grant(config, magic, Permission.PUSH_MERGE, registered);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java new file mode 100644 index 0000000..53ed5ac --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ProjectConfigSchemaUpdate.java
@@ -0,0 +1,109 @@ +// 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 static com.google.gerrit.server.git.ProjectConfig.ACCESS; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import com.google.gerrit.common.data.PermissionRule; +import com.google.gerrit.reviewdb.client.RefNames; +import com.google.gerrit.server.git.MetaDataUpdate; +import com.google.gerrit.server.git.ProjectConfig; +import com.google.gerrit.server.git.VersionedMetaData; +import com.google.gwtorm.server.OrmException; + +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 java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +public class ProjectConfigSchemaUpdate extends VersionedMetaData { + + private final MetaDataUpdate update; + private Config config; + private boolean updated; + + public static ProjectConfigSchemaUpdate read(MetaDataUpdate update) + throws IOException, ConfigInvalidException { + ProjectConfigSchemaUpdate r = new ProjectConfigSchemaUpdate(update); + r.load(update); + return r; + } + + private ProjectConfigSchemaUpdate(MetaDataUpdate update) { + this.update = update; + } + + @Override + protected String getRefName() { + return RefNames.REFS_CONFIG; + } + + @Override + protected void onLoad() throws IOException, ConfigInvalidException { + config = readConfig(ProjectConfig.PROJECT_CONFIG); + } + + public void removeForceFromPermission(String name) { + for (String subsection : config.getSubsections(ACCESS)) { + Set<String> names = config.getNames(ACCESS, subsection); + if (names.contains(name)) { + List<String> values = + Arrays.asList(config.getStringList(ACCESS, subsection, name)); + values = Lists.transform(values, new Function<String, String>() { + @Override + public String apply(String ruleString) { + PermissionRule rule = PermissionRule.fromString(ruleString, false); + if (rule.getForce()) { + rule.setForce(false); + updated = true; + } + return rule.asString(false); + } + }); + config.setStringList(ACCESS, subsection, name, values); + } + } + } + + @Override + protected boolean onSave(CommitBuilder commit) + throws IOException, ConfigInvalidException { + saveConfig(ProjectConfig.PROJECT_CONFIG, config); + return true; + } + + public void save(PersonIdent personIdent, String commitMessage) + throws OrmException { + if (!updated) { + return; + } + + update.getCommitBuilder().setAuthor(personIdent); + update.getCommitBuilder().setCommitter(personIdent); + update.setMessage(commitMessage); + try { + commit(update); + } catch (IOException e) { + throw new OrmException(e); + } + } +}
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 7217fd0..5d1e579 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
@@ -33,7 +33,7 @@ /** A version of the database schema. */ public abstract class SchemaVersion { /** The current schema version. */ - public static final Class<Schema_129> C = Schema_129.class; + public static final Class<Schema_131> C = Schema_131.class; public static int getBinaryVersion() { return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_119.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_119.java index 9fdec25..cd42e75 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_119.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_119.java
@@ -136,6 +136,8 @@ p.reviewCategoryStrategy = toReviewCategoryStrategy(rs.getString(14)); p.muteCommonPathPrefixes = toBoolean(rs.getString(15)); + p.defaultBaseForMerges = + GeneralPreferencesInfo.defaults().defaultBaseForMerges; imports.put(accountId, p); } }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_130.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_130.java new file mode 100644 index 0000000..d7fcc3b --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_130.java
@@ -0,0 +1,66 @@ +// 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.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.MetaDataUpdate; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; + +import java.io.IOException; + +public class Schema_130 extends SchemaVersion { + private static final String COMMIT_MSG = + "Remove force option from 'Push Annotated Tag' permission\n" + + "\n" + + "The force option on 'Push Annotated Tag' had no effect and is no longer\n" + + "supported."; + + private final GitRepositoryManager repoManager; + private final PersonIdent serverUser; + + @Inject + Schema_130(Provider<Schema_129> prior, + GitRepositoryManager repoManager, + @GerritPersonIdent PersonIdent serverUser) { + super(prior); + this.repoManager = repoManager; + this.serverUser = serverUser; + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException { + for (Project.NameKey projectName : repoManager.list()) { + try (Repository git = repoManager.openRepository(projectName); + MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, + projectName, git)) { + ProjectConfigSchemaUpdate cfg = ProjectConfigSchemaUpdate.read(md); + cfg.removeForceFromPermission("pushTag"); + cfg.save(serverUser, COMMIT_MSG); + } catch (ConfigInvalidException | IOException ex) { + throw new OrmException(ex); + } + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_131.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_131.java new file mode 100644 index 0000000..1be4c3e --- /dev/null +++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_131.java
@@ -0,0 +1,68 @@ +// 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.Project; +import com.google.gerrit.reviewdb.server.ReviewDb; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.extensions.events.GitReferenceUpdated; +import com.google.gerrit.server.git.GitRepositoryManager; +import com.google.gerrit.server.git.MetaDataUpdate; +import com.google.gerrit.server.git.ProjectConfig; +import com.google.gwtorm.server.OrmException; +import com.google.inject.Inject; +import com.google.inject.Provider; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; + +import java.io.IOException; + +public class Schema_131 extends SchemaVersion { + private static final String COMMIT_MSG = + "Rename 'Push Annotated/Signed Tag' permission to 'Create Annotated/Signed Tag'"; + + private final GitRepositoryManager repoManager; + private final PersonIdent serverUser; + + @Inject + Schema_131(Provider<Schema_130> prior, + GitRepositoryManager repoManager, + @GerritPersonIdent PersonIdent serverUser) { + super(prior); + this.repoManager = repoManager; + this.serverUser = serverUser; + } + + @Override + protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException { + for (Project.NameKey projectName : repoManager.list()) { + try (Repository git = repoManager.openRepository(projectName); + MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, + projectName, git)) { + ProjectConfig config = ProjectConfig.read(md); + if (config.hasLegacyPermissions()) { + md.getCommitBuilder().setAuthor(serverUser); + md.getCommitBuilder().setCommitter(serverUser); + md.setMessage(COMMIT_MSG); + config.commit(md); + } + } catch (ConfigInvalidException | IOException ex) { + throw new OrmException(ex); + } + } + } +}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/GitUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GitUtil.java deleted file mode 100644 index 2d1e1fa..0000000 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/GitUtil.java +++ /dev/null
@@ -1,51 +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.util; - -import org.eclipse.jgit.errors.IncorrectObjectTypeException; -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 java.io.IOException; - -public class GitUtil { - - /** - * @param git - * @param commitId - * @param parentNum - * @return the {@code paretNo} parent of given commit or {@code null} - * when {@code parentNo} exceed number of {@code commitId} parents. - * @throws IncorrectObjectTypeException - * the supplied id is not a commit or an annotated tag. - * @throws IOException - * a pack file or loose object could not be read. - */ - public static RevCommit getParent(Repository git, - ObjectId commitId, int parentNum) throws IOException { - try (RevWalk walk = new RevWalk(git)) { - RevCommit commit = walk.parseCommit(commitId); - if (commit.getParentCount() > parentNum) { - return commit.getParent(parentNum); - } - } - return null; - } - - private GitUtil() { - } -}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java index fab0b34..030383a 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/LabelVote.java
@@ -58,6 +58,16 @@ Short.parseShort(text.substring(e + 1), text.length())); } + public static StringBuilder appendTo(StringBuilder sb, String label, + short value) { + if (value == (short) 0) { + return sb.append('-').append(label); + } else if (value < 0) { + return sb.append(label).append(value); + } + return sb.append(label).append('+').append(value); + } + public static LabelVote create(String label, short value) { return new AutoValue_LabelVote(LabelType.checkNameInternal(label), value); } @@ -70,13 +80,9 @@ public abstract short value(); public String format() { - if (value() == (short) 0) { - return '-' + label(); - } else if (value() < 0) { - return label() + value(); - } else { - return label() + '+' + value(); - } + // Max short string length is "-32768".length() == 6. + return appendTo(new StringBuilder(label().length() + 6), label(), value()) + .toString(); } public String formatWithEquals() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java index b2899c1..bfec979 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/validators/OutgoingEmailValidationListener.java
@@ -14,6 +14,7 @@ package com.google.gerrit.server.validators; +import com.google.gerrit.common.Nullable; import com.google.gerrit.extensions.annotations.ExtensionPoint; import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.mail.EmailHeader; @@ -32,6 +33,8 @@ class Args { // in arguments public String messageClass; + public String textBody; + @Nullable public String htmlBody; // in/out arguments public Address smtpFromAddress;
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.soy new file mode 100644 index 0000000..50c5fc3 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.soy
@@ -0,0 +1,39 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * .Abandoned template will determine the contents of the email related to a + * change being abandoned. + * @param change + * @param coverLetter + * @param email + * @param fromName + */ +{template .Abandoned autoescape="strict" kind="text"} + {$fromName} has abandoned this change. + {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n} + {\n} + Change subject: {$change.subject}{\n} + ......................................................................{\n} + {if $coverLetter} + {\n} + {\n} + {$coverLetter} + {\n} + {/if} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm deleted file mode 100644 index accd3b8..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm +++ /dev/null
@@ -1,46 +0,0 @@ -## Copyright (C) 2010 The Android Open Source Project -## -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The Abandoned.vm template will determine the contents of the email related -## to a change being abandoned. It is a ChangeEmail: see ChangeSubject.vm and -## ChangeFooter.vm. -## -$fromName has abandoned this change.#** -*##if($email.changeUrl) ( $email.changeUrl )#end - - -Change subject: $change.subject -...................................................................... - - -#if ($coverLetter) -$coverLetter - -#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.soy new file mode 100644 index 0000000..aa2b27d --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.soy
@@ -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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .AddKey template will determine the contents of the email related to + * adding a new SSH or GPG key to an account. + * @param email + */ +{template .AddKey autoescape="strict" kind="text"} + One or more new {$email.keyType} keys have been added to Gerrit Code Review at + {sp}{$email.gerritHost}: + + {\n} + {\n} + + {if $email.sshKey} + {$email.sshKey} + {elseif $email.gpgKeys} + {$email.gpgKeys} + {/if} + + {\n} + {\n} + + If this is not expected, please contact your Gerrit Administrators + immediately. + + {\n} + {\n} + + You can also manage your {$email.keyType} keys by visiting + {\n} + {if $email.sshKey} + {$email.gerritUrl}#/settings/ssh-keys + {elseif $email.gpgKeys} + {$email.gerritUrl}#/settings/gpg-keys + {/if} + {\n} + {if $email.userNameEmail} + (while signed in as {$email.userNameEmail}) + {else} + (while signed in as {$email.email}) + {/if} + + {\n} + {\n} + + If clicking the link above does not work, copy and paste the URL in a new + browser window instead. + + {\n} + {\n} + + This is a send-only email address. Replies to this message will not be read + or answered. +{/template} \ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm deleted file mode 100644 index c60ce8b..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKey.vm +++ /dev/null
@@ -1,61 +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. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The AddKey.vm template will determine the contents of the email -## related to adding a new SSH or GPG key to an account. -## -One or more new ${email.keyType} keys have been added to Gerrit Code Review at ${email.gerritHost}: - -#if($email.sshKey) -$email.sshKey -#elseif($email.gpgKeys) -$email.gpgKeys -#end - -If this is not expected, please contact your Gerrit Administrators -immediately. - -You can also manage your ${email.keyType} keys by visiting -#if($email.sshKey) -$email.gerritUrl#/settings/ssh-keys -#elseif($email.gpgKeys) -$email.gerritUrl#/settings/gpg-keys -#end -#if($email.userNameEmail) -(while signed in as $email.userNameEmail) -#else -(while signed in as $email.email) -#end - -If clicking the link above does not work, copy and paste the URL in a -new browser window instead. - -This is a send-only email address. Replies to this message will not -be read or answered.
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKeyHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKeyHtml.soy new file mode 100644 index 0000000..017fd6d --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/AddKeyHtml.soy
@@ -0,0 +1,66 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * @param email + */ +{template .AddKeyHtml autoescape="strict" kind="html"} + <p> + One or more new {$email.keyType} keys have been added to Gerrit Code Review + at {$email.gerritHost}: + </p> + + {let $keyStyle kind="css"} + background: #f0f0f0; + border: 1px solid #ccc; + color: #555; + padding: 12px; + width: 400px; + {/let} + + {if $email.sshKey} + <pre style="{$keyStyle}">{$email.sshKey}</pre> + {elseif $email.gpgKeys} + <pre style="{$keyStyle}">{$email.gpgKeys}</pre> + {/if} + + <p> + If this is not expected, please contact your Gerrit Administrators + immediately. + </p> + + <p> + You can also manage your {$email.keyType} keys by following{sp} + {if $email.sshKey} + <a href="{$email.gerritUrl}#/settings/ssh-keys">this link</a> + {elseif $email.gpgKeys} + <a href="{$email.gerritUrl}#/settings/gpg-keys">this link</a> + {/if} + {sp} + {if $email.userNameEmail} + (while signed in as {$email.userNameEmail}) + {else} + (while signed in as {$email.email}) + {/if}. + </p> + + <p> + This is a send-only email address. Replies to this message will not be read + or answered. + </p> +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy new file mode 100644 index 0000000..9906dd8d --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy
@@ -0,0 +1,52 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .ChangeFooter template will determine the contents of the footer text + * that will be appended to ALL emails related to changes. + * @param branch + * @param change + * @param changeId + * @param email + * @param messageClass + * @param patchSet + * @param projectName + */ +{template .ChangeFooter autoescape="strict" kind="text"} + --{sp} + {\n} + + {if $email.changeUrl} + To view, visit {$email.changeUrl}{\n} + {/if} + + {if $email.settingsUrl} + To unsubscribe, visit {$email.settingsUrl}{\n} + {/if} + + {if $email.changeUrl or $email.settingsUrl} + {\n} + {/if} + + Gerrit-MessageType: {$messageClass}{\n} + Gerrit-Change-Id: {$changeId}{\n} + Gerrit-PatchSet: {$patchSet.patchSetId}{\n} + Gerrit-Project: {$projectName}{\n} + Gerrit-Branch: {$branch.shortName}{\n} + Gerrit-Owner: {$change.ownerEmail}{\n} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm deleted file mode 100644 index f1d3e90..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm +++ /dev/null
@@ -1,52 +0,0 @@ -## Copyright (C) 2010 The Android Open Source Project -## -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The ChangeFooter.vm template will determine the contents of the footer -## text that will be appended to ALL emails related to changes. -## -#set ($SPACE = " ") ---$SPACE -#if ($email.changeUrl) -To view, visit $email.changeUrl -#set ($notblank = 1) -#end -#if ($email.settingsUrl) -To unsubscribe, visit $email.settingsUrl -#set ($notblank = 1) -#end -#if ($notblank) - -#end -Gerrit-MessageType: $messageClass -Gerrit-Change-Id: $changeId -Gerrit-PatchSet: $patchSet.patchSetId -Gerrit-Project: $projectName -Gerrit-Branch: $branch.shortName -Gerrit-Owner: $email.getNameEmailFor($change.owner)
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.soy new file mode 100644 index 0000000..98de6e7 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.soy
@@ -0,0 +1,28 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .ChangeSubject template will determine the contents of the email subject + * line for ALL emails related to changes. + * @param branch + * @param change + * @param shortProjectName + */ +{template .ChangeSubject autoescape="strict" kind="text"} + Change in {$shortProjectName}[{$branch.shortName}]: {$change.shortSubject} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm deleted file mode 100644 index 4fd9a23..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm +++ /dev/null
@@ -1,42 +0,0 @@ -## Copyright (C) 2010 The Android Open Source Project -## -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The ChangeSubject.vm template will determine the contents of the email -## subject line for ALL emails related to changes. -## -## Optionally $change.originalSubject can be used for the first subject -## in a change. This allows subject based email clients such as GMail -## to thread comments together even if subsequent patch sets change the -## first line of the commit message. -## -#macro(ellipsis $length $str) -#if($str.length() > $length)#set($length = $length - 3)${str.substring(0,$length)}...#else$str#end -#end -Change in ${projectName.replaceAll('/.*/', '...')}[$branch.shortName]: #ellipsis(63, $change.subject)
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy new file mode 100644 index 0000000..781d8a0 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.soy
@@ -0,0 +1,46 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .Comment template will determine the contents of the email related to a + * user submitting comments on changes. + * @param change + * @param coverLetter + * @param email + * @param fromName + */ +{template .Comment autoescape="strict" kind="text"} + {if $coverLetter or $email.hasInlineComments} + {$fromName} has posted comments on this change. + {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n} + {\n} + Change subject: {$change.subject}{\n} + ......................................................................{\n} + {if $coverLetter} + {\n} + {\n} + {$coverLetter} + {/if} + {if $email.hasInlineComments} + {\n} + {\n} + {$email.inlineComments} + {/if} + {/if} + {\n} +{/template} \ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm deleted file mode 100644 index a442311..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm +++ /dev/null
@@ -1,55 +0,0 @@ -## Copyright (C) 2010 The Android Open Source Project -## -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The Comment.vm template will determine the contents of the email related to -## a user submitting comments on changes. It is a ChangeEmail: see -## ChangeSubject.vm, ChangeFooter.vm and CommentFooter.vm. -## -#if ($email.coverLetter || $email.hasInlineComments()) -$fromName has posted comments on this change.#** -*##if($email.changeUrl) ( $email.changeUrl )#end - - -Change subject: $change.subject -...................................................................... - - -#if ($email.coverLetter) -$email.coverLetter - -#end -## -## It is possible to increase the span of the quoted lines by using the line -## count parameter when calling $email.getInlineComments as a function. -## -## Example: #if($email.hasInlineComments())$email.getInlineComments(5)#end -## -#if($email.hasInlineComments())$email.inlineComments#end -#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy new file mode 100644 index 0000000..3fcad6b --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.soy
@@ -0,0 +1,31 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .CommentFooter template will determine the contents of the footer text + * that will be appended to emails related to a user submitting comments on + * changes. + * @param email + */ +{template .CommentFooter autoescape="strict" kind="text"} + {if $email.hasInlineComments} + Gerrit-HasComments: Yes + {else} + Gerrit-HasComments: No + {/if} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.vm deleted file mode 100644 index e0832e6..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.vm +++ /dev/null
@@ -1,40 +0,0 @@ -## Copyright (C) 2012 The Android Open Source Project -## -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The CommentFooter.vm template will determine the contents of the footer -## text that will be appended to emails related to a user submitting comments -## on changes. -## -## See ChangeSubject.vm and ChangeFooter.vm. -#if($email.hasInlineComments()) -Gerrit-HasComments: Yes -#else -Gerrit-HasComments: No -#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.soy new file mode 100644 index 0000000..888ee4b --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.soy
@@ -0,0 +1,44 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .DeleteReviewer template will determine the contents of the email related + * to removal of a reviewer (and the reviewer's votes) from reviews. + * @param change + * @param coverLetter + * @param email + * @param fromName + */ +{template .DeleteReviewer autoescape="strict" kind="text"} + {$fromName} has removed{sp} + {foreach $reviewerName in $email.reviewerNames} + {if not isFirst($reviewerName)},{sp}{/if} + {$reviewerName} + {/foreach}{sp} + from this change.{sp} + {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n} + {\n} + Change subject: {$change.subject}{\n} + ......................................................................{\n} + {if $coverLetter} + {\n} + {\n} + {$coverLetter} + {\n} + {/if} +{/template} \ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.vm deleted file mode 100644 index 635b716..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteReviewer.vm +++ /dev/null
@@ -1,47 +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. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The DeleteReviewer.vm template will determine the contents of the email -## related to removal of a reviewer (and the reviewer's votes) from reviews. -## It is a ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm. -## -$fromName has removed $email.joinStrings($email.reviewerNames, ', ') from #** -*#this change.#** -*##if($email.changeUrl) ( $email.changeUrl )#end - - -Change subject: $change.subject -...................................................................... - - -#if ($email.coverLetter) -$email.coverLetter - -#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.soy new file mode 100644 index 0000000..b249ded --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.soy
@@ -0,0 +1,37 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .DeleteVote template will determine the contents of the email related + * to removing votes on changes. + * @param change + * @param coverLetter + * @param fromName + */ +{template .DeleteVote autoescape="strict" kind="text"} + {$fromName} has removed a vote on this change.{\n} + {\n} + Change subject: {$change.subject}{\n} + ......................................................................{\n} + {if $coverLetter} + {\n} + {\n} + {$coverLetter} + {\n} + {/if} +{/template} \ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.vm deleted file mode 100644 index 294063e..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/DeleteVote.vm +++ /dev/null
@@ -1,44 +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. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The DeleteVote.vm template will determine the contents of the email related -## to removing votes on changes. It is a ChangeEmail: see ChangeSubject.vm -## and ChangeFooter.vm. -## -$fromName has removed a vote on this change. - -Change subject: $change.subject -...................................................................... - - -#if ($coverLetter) -$coverLetter - -#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy new file mode 100644 index 0000000..6467e95 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.soy
@@ -0,0 +1,25 @@ +/** + * 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. +*/ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .Footer template will determine the contents of the footer text + * appended to the end of all outgoing emails after the ChangeFooter and + * CommentFooter. + */ +{template .Footer} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm deleted file mode 100644 index 28f29fd..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Footer.vm +++ /dev/null
@@ -1,33 +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. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The Footer.vm template will determine the contents of the footer text -## appended to the end of all outgoing emails after the ChangeFooter and -## CommentFooter.
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy new file mode 100644 index 0000000..9befa51 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/FooterHtml.soy
@@ -0,0 +1,20 @@ +/** + * 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. +*/ + +{namespace com.google.gerrit.server.mail.template} + +{template .FooterHtml autoescape="strict" kind="html"} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.soy new file mode 100644 index 0000000..d483264 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.soy
@@ -0,0 +1,42 @@ + +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .Merged template will determine the contents of the email related to + * a change successfully merged to the head. + * @param change + * @param email + * @param fromName + */ +{template .Merged autoescape="strict" kind="text"} + {$fromName} has submitted this change and it was merged. + {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n} + {\n} + Change subject: {$change.subject}{\n} + ......................................................................{\n} + {\n} + {$email.changeDetail} + {$email.approvals} + {if $email.includeDiff} + {\n} + {\n} + {$email.unifiedDiff} + {\n} + {/if} +{/template} \ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm deleted file mode 100644 index 3e49e92..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm +++ /dev/null
@@ -1,47 +0,0 @@ -## Copyright (C) 2010 The Android Open Source Project -## -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The Merged.vm template will determine the contents of the email related to -## a change successfully merged to the head. It is a ChangeEmail: see -## ChangeSubject.vm and ChangeFooter.vm. -## -$fromName has submitted this change and it was merged.#** -*##if($email.changeUrl) ( $email.changeUrl )#end - - -Change subject: $change.subject -...................................................................... - - -$email.changeDetail$email.approvals - -#if($email.includeDiff) -$email.UnifiedDiff -#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.soy new file mode 100644 index 0000000..296f625 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.soy
@@ -0,0 +1,81 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .NewChange template will determine the contents of the email related to a + * user submitting a new change for review. + * @param change + * @param email + * @param fromName + * @param patchSet + * @param projectName + */ +{template .NewChange autoescape="strict" kind="text"} + {if $email.reviewerNames} + Hello{sp} + {foreach $reviewerName in $email.reviewerNames} + {if not isFirst($reviewerName)},{sp}{/if} + {$reviewerName} + {/foreach}, + + {\n} + {\n} + + I'd like you to do a code review. + + {if $email.changeUrl} + {sp}Please visit + + {\n} + {\n} + + {sp}{sp}{sp}{sp}{$email.changeUrl} + + {\n} + {\n} + + to review the following change. + {/if} + {else} + {$fromName} has uploaded a new change for review. + {if $email.changeUrl} ( {$email.changeUrl}{/if} + {/if}{\n} + + {\n} + {\n} + + Change subject: {$change.subject}{\n} + ......................................................................{\n} + + {\n} + + {$email.changeDetail}{\n} + + {if $email.sshHost} + {\n} + {sp}{sp}git pull ssh:{print '//'}{$email.sshHost}/{$projectName} + {sp}{$patchSet.refName} + {\n} + {/if} + + {if $email.includeDiff} + {\n} + {$email.unifiedDiff} + {\n} + {/if} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm deleted file mode 100644 index 8b66e81..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm +++ /dev/null
@@ -1,60 +0,0 @@ -## Copyright (C) 2010 The Android Open Source Project -## -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The NewChange.vm template will determine the contents of the email related -## to a user submitting a new change for review. It is a ChangeEmail: see -## ChangeSubject.vm and ChangeFooter.vm. -## -#if($email.reviewerNames) -Hello $email.joinStrings($email.reviewerNames, ', '), - -I'd like you to do a code review.#if($email.changeUrl) Please visit - - $email.changeUrl - -to review the following change. -#end -#else -$fromName has uploaded a new change for review.#** -*##if($email.changeUrl) ( $email.changeUrl )#end -#end - - -Change subject: $change.subject -...................................................................... - -$email.changeDetail -#if($email.sshHost) - git pull ssh://$email.sshHost/$projectName $patchSet.refName -#end -#if($email.includeDiff) - -$email.UnifiedDiff -#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy new file mode 100644 index 0000000..2b30ae6 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.soy
@@ -0,0 +1,54 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .RegisterNewEmail template will determine the contents of the email + * related to registering new email accounts. + * @param email + */ +{template .RegisterNewEmail autoescape="strict" kind="text"} + Welcome to Gerrit Code Review at {$email.gerritHost}.{\n} + + {\n} + + To add a verified email address to your user account, please{\n} + click on the following link + {if $email.userNameEmail} + {sp}while signed in as {$email.userNameEmail} + {/if}:{\n} + + {\n} + + {$email.gerritUrl}#/VE/{$email.emailRegistrationToken}{\n} + + {\n} + + If you have received this mail in error, you do not need to take any{\n} + action to cancel the account. The address will not be activated, and{\n} + you will not receive any further emails.{\n} + + {\n} + + If clicking the link above does not work, copy and paste the URL in a{\n} + new browser window instead.{\n} + + {\n} + + This is a send-only email address. Replies to this message will not{\n} + be read or answered.{\n} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm deleted file mode 100644 index 7e095fb..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm +++ /dev/null
@@ -1,49 +0,0 @@ -## Copyright (C) 2010 The Android Open Source Project -## -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The RegisterNewEmail.vm template will determine the contents of the email -## related to registering new email accounts. -## -Welcome to Gerrit Code Review at ${email.gerritHost}. - -To add a verified email address to your user account, please -click on the following link#if($email.userNameEmail) while signed in as $email.userNameEmail#end: - -$email.gerritUrl#/VE/$email.emailRegistrationToken - -If you have received this mail in error, you do not need to take any -action to cancel the account. The address will not be activated, and -you will not receive any further emails. - -If clicking the link above does not work, copy and paste the URL in a -new browser window instead. - -This is a send-only email address. Replies to this message will not -be read or answered.
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy new file mode 100644 index 0000000..2236725 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.soy
@@ -0,0 +1,59 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .ReplacePatchSet template will determine the contents of the email + * related to a user submitting a new patchset for a change. + * @param change + * @param email + * @param fromName + * @param patchSet + * @param projectName + */ +{template .ReplacePatchSet autoescape="strict" kind="text"} + {if $email.reviewerNames} + Hello{sp} + {foreach $reviewerName in $email.reviewerNames} + {$reviewerName},{sp} + {/foreach}{\n} + {\n} + I'd like you to reexamine a change. + {if $email.changeUrl} + {sp}Please visit + {\n} + {\n} + {sp}{sp}{sp}{sp}{$email.changeUrl} + {\n} + {\n} + to look at the new patch set (#{$patchSet.patchSetId}). + {/if} + {else} + {$fromName} has uploaded a new patch set (#{$patchSet.patchSetId}). + {if $email.changeUrl} ( {$email.changeUrl}{/if} + {/if}{\n} + {\n} + Change subject: {$change.subject}{\n} + ......................................................................{\n} + {\n} + {$email.changeDetail}{\n} + {if $email.sshHost} + {sp}{sp}git pull ssh:{print '//'}{$email.sshHost}/{$projectName}{sp} + {$patchSet.refName} + {\n} + {/if} +{/template} \ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm deleted file mode 100644 index e45bf30..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm +++ /dev/null
@@ -1,56 +0,0 @@ -## Copyright (C) 2010 The Android Open Source Project -## -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The ReplacePatchSet.vm template will determine the contents of the email -## related to a user submitting a new patchset for a change. It is a -## ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm. -## -#if($email.reviewerNames) -Hello $email.joinStrings($email.reviewerNames, ', '), - -I'd like you to reexamine a change.#if($email.changeUrl) Please visit - - $email.changeUrl - -to look at the new patch set (#$patchSet.patchSetId). -#end -#else -$fromName has uploaded a new patch set (#$patchSet.patchSetId).#** -*##if($email.changeUrl) ( $email.changeUrl )#end - -#end - -Change subject: $change.subject -...................................................................... - -$email.changeDetail -#if($email.sshHost) - git pull ssh://$email.sshHost/$projectName $patchSet.refName -#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.soy new file mode 100644 index 0000000..14ae0f3 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.soy
@@ -0,0 +1,39 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .Restored template will determine the contents of the email related to a + * change being restored. + * @param change + * @param coverLetter + * @param email + * @param fromName + */ +{template .Restored autoescape="strict" kind="text"} + {$fromName} has restored this change. + {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n} + {\n} + Change subject: {$change.subject}{\n} + ......................................................................{\n} + {if $coverLetter} + {\n} + {\n} + {$coverLetter} + {\n} + {/if} +{/template} \ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.vm deleted file mode 100644 index 31e1c69..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.vm +++ /dev/null
@@ -1,46 +0,0 @@ -## Copyright (C) 2011 The Android Open Source Project -## -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The Restored.vm template will determine the contents of the email related -## to a change being restored. It is a ChangeEmail: see ChangeSubject.vm and -## ChangeFooter.vm. -## -$fromName has restored this change.#** -*##if($email.changeUrl) ( $email.changeUrl )#end - - -Change subject: $change.subject -...................................................................... - - -#if ($coverLetter) -$coverLetter - -#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.soy new file mode 100644 index 0000000..7f74df9 --- /dev/null +++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.soy
@@ -0,0 +1,39 @@ +/** + * 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. + */ + +{namespace com.google.gerrit.server.mail.template} + +/** + * The .Reverted template will determine the contents of the email related + * to a change being reverted. + * @param change + * @param coverLetter + * @param email + * @param fromName + */ +{template .Reverted autoescape="strict" kind="text"} + {$fromName} has reverted this change. + {if $email.changeUrl} ( {$email.changeUrl} ){/if}{\n} + {\n} + Change subject: {$change.subject}{\n} + ......................................................................{\n} + {if $coverLetter} + {\n} + {\n} + {$coverLetter} + {\n} + {/if} +{/template}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.vm deleted file mode 100644 index 1e9e251..0000000 --- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.vm +++ /dev/null
@@ -1,46 +0,0 @@ -## Copyright (C) 2010 The Android Open Source Project -## -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. -## -## -## Template Type: -## ------------- -## This is a velocity mail template, see: http://velocity.apache.org and the -## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates. -## -## Template File Names and extensions: -## ---------------------------------- -## Gerrit will use templates ending in ".vm" but will ignore templates ending -## in ".vm.example". If a .vm template does not exist, the default internal -## gerrit template which is the same as the .vm.example will be used. If you -## want to override the default template, copy the .vm.example file to a .vm -## file and edit it appropriately. -## -## This Template: -## -------------- -## The Reverted.vm template will determine the contents of the email related -## to a change being reverted. It is a ChangeEmail: see ChangeSubject.vm and -## ChangeFooter.vm. -## -$fromName has reverted this change.#** -*##if($email.changeUrl) ( $email.changeUrl )#end - - -Change subject: $change.subject -...................................................................... - - -#if ($coverLetter) -$coverLetter - -#end
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 d51547c..5a937b6 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
@@ -97,7 +97,7 @@ in = text/x-properties ini = text/x-properties intr = text/x-dylan -jade = text/x-jade +jade = text/x-pug java = text/x-java jl = text/x-julia jruby = text/x-ruby @@ -163,6 +163,7 @@ ps1 = application/x-powershell psd1 = application/x-powershell psm1 = application/x-powershell +pug = text/x-pug py = text/x-python pyw = text/x-python pyx = text/x-cython
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java index 11f1d54..87dd3d9 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
@@ -35,8 +35,10 @@ import org.junit.Before; import org.junit.Test; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Set; public class FromAddressGeneratorProviderTest { @@ -60,6 +62,10 @@ config.setString("sendemail", null, "from", newFrom); } + private void setDomains(List<String> domains) { + config.setStringList("sendemail", null, "allowedDomain", domains); + } + @Test public void testDefaultIsMIXED() { assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.PatternGen.class); @@ -118,7 +124,7 @@ replay(accountCache); final Address r = create().from(user); assertThat(r).isNotNull(); - assertThat(r.name).isEqualTo(name); + assertThat(r.name).isEqualTo(name + " (Code Review)"); assertThat(r.email).isEqualTo(ident.getEmailAddress()); verify(accountCache); } @@ -135,6 +141,88 @@ } @Test + public void testUSERAllowDomain() { + setFrom("USER"); + setDomains(Arrays.asList("*.example.com")); + final String name = "A U. Thor"; + final String email = "a.u.thor@test.example.com"; + final Account.Id user = user(name, email); + + replay(accountCache); + final Address r = create().from(user); + assertThat(r).isNotNull(); + assertThat(r.name).isEqualTo(name); + assertThat(r.email).isEqualTo(email); + verify(accountCache); + } + + @Test + public void testUSERNoAllowDomain() { + setFrom("USER"); + setDomains(Arrays.asList("example.com")); + final String name = "A U. Thor"; + final String email = "a.u.thor@test.com"; + final Account.Id user = user(name, email); + + replay(accountCache); + final Address r = create().from(user); + assertThat(r).isNotNull(); + assertThat(r.name).isEqualTo(name + " (Code Review)"); + assertThat(r.email).isEqualTo(ident.getEmailAddress()); + verify(accountCache); + } + + @Test + public void testUSERAllowDomainTwice() { + setFrom("USER"); + setDomains(Arrays.asList("example.com")); + setDomains(Arrays.asList("test.com")); + final String name = "A U. Thor"; + final String email = "a.u.thor@test.com"; + final Account.Id user = user(name, email); + + replay(accountCache); + final Address r = create().from(user); + assertThat(r).isNotNull(); + assertThat(r.name).isEqualTo(name); + assertThat(r.email).isEqualTo(email); + verify(accountCache); + } + + @Test + public void testUSERAllowDomainTwiceReverse() { + setFrom("USER"); + setDomains(Arrays.asList("test.com")); + setDomains(Arrays.asList("example.com")); + final String name = "A U. Thor"; + final String email = "a.u.thor@test.com"; + final Account.Id user = user(name, email); + + replay(accountCache); + final Address r = create().from(user); + assertThat(r).isNotNull(); + assertThat(r.name).isEqualTo(name + " (Code Review)"); + assertThat(r.email).isEqualTo(ident.getEmailAddress()); + verify(accountCache); + } + + @Test + public void testUSERAllowTwoDomains() { + setFrom("USER"); + setDomains(Arrays.asList("example.com", "test.com")); + final String name = "A U. Thor"; + final String email = "a.u.thor@test.com"; + final Account.Id user = user(name, email); + + replay(accountCache); + final Address r = create().from(user); + assertThat(r).isNotNull(); + assertThat(r.name).isEqualTo(name); + assertThat(r.email).isEqualTo(email); + verify(accountCache); + } + + @Test public void testSelectSERVER() { setFrom("SERVER"); assertThat(create()).isInstanceOf(FromAddressGeneratorProvider.ServerGen.class);
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 0173b05..a29d397 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
@@ -322,7 +322,10 @@ update.commit(); notes = newNotes(c); - assertThat(notes.getApprovals()).isEmpty(); + assertThat(notes.getApprovals()).containsExactlyEntriesIn( + ImmutableMultimap.of( + psa.getPatchSetId(), + new PatchSetApproval(psa.getKey(), (short) 0, update.getWhen()))); } @Test @@ -344,7 +347,10 @@ update.commit(); notes = newNotes(c); - assertThat(notes.getApprovals()).isEmpty(); + assertThat(notes.getApprovals()).containsExactlyEntriesIn( + ImmutableMultimap.of( + psa.getPatchSetId(), + new PatchSetApproval(psa.getKey(), (short) 0, update.getWhen()))); // Add back approval on same label. update = newUpdate(c, otherUser);
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 d4d77bd..596ed59 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
@@ -137,13 +137,13 @@ } private void assertCanSubmit(String ref, ProjectControl u) { - assertThat(u.controlForRef(ref).canSubmit()) + assertThat(u.controlForRef(ref).canSubmit(false)) .named("can submit " + ref) .isTrue(); } private void assertCannotSubmit(String ref, ProjectControl u) { - assertThat(u.controlForRef(ref).canSubmit()) + assertThat(u.controlForRef(ref).canSubmit(false)) .named("can submit " + ref) .isFalse(); } @@ -685,6 +685,43 @@ } @Test + public void testUnblockMoreSpecificRefInLocal_Fails() { + block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*"); + allow(local, PUSH, DEVS, "refs/heads/master"); + + ProjectControl u = user(local, DEVS); + assertCannotUpdate("refs/heads/master", u); + } + + @Test + public void testUnblockMoreSpecificRefWithExclusiveFlag() { + block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*"); + allow(local, PUSH, DEVS, "refs/heads/master", true); + + ProjectControl u = user(local, DEVS); + assertCanUpdate("refs/heads/master", u); + } + + @Test + public void testUnblockMoreSpecificRefInLocalWithExclusiveFlag_Fails() { + block(parent, PUSH, ANONYMOUS_USERS, "refs/heads/*"); + allow(local, PUSH, DEVS, "refs/heads/master", true); + + ProjectControl u = user(local, DEVS); + assertCannotUpdate("refs/heads/master", u); + } + + @Test + public void testUnblockOtherPermissionWithMoreSpecificRefAndExclusiveFlag_Fails() { + block(local, PUSH, ANONYMOUS_USERS, "refs/heads/*"); + allow(local, PUSH, DEVS, "refs/heads/master"); + allow(local, SUBMIT, DEVS, "refs/heads/master", true); + + ProjectControl u = user(local, DEVS); + assertCannotUpdate("refs/heads/master", u); + } + + @Test public void testUnblockLargerScope_Fails() { block(local, PUSH, ANONYMOUS_USERS, "refs/heads/master"); allow(local, PUSH, DEVS, "refs/heads/*"); @@ -825,6 +862,14 @@ } @Test + public void testBlockOwner() { + block(parent, OWNER, ANONYMOUS_USERS, "refs/*"); + allow(local, OWNER, DEVS, "refs/*"); + + assertThat(user(local, DEVS).isOwner()).isFalse(); + } + + @Test public void testValidateRefPatternsOK() throws Exception { RefPattern.validate("refs/*"); RefPattern.validate("^refs/heads/*");
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java index 772c778..9cbdae2 100644 --- a/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java +++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/Util.java
@@ -97,6 +97,13 @@ } public static PermissionRule allow(ProjectConfig project, + String permissionName, AccountGroup.UUID group, String ref, + boolean exclusive) { + return grant(project, permissionName, newRule(project, group), ref, + exclusive); + } + + public static PermissionRule allow(ProjectConfig project, String capabilityName, AccountGroup.UUID group) { PermissionRule rule = newRule(project, group); project.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true) @@ -163,9 +170,18 @@ private static PermissionRule grant(ProjectConfig project, String permissionName, PermissionRule rule, String ref) { - project.getAccessSection(ref, true) // - .getPermission(permissionName, true) // - .add(rule); + return grant(project, permissionName, rule, ref, false); + } + + private static PermissionRule grant(ProjectConfig project, + String permissionName, PermissionRule rule, String ref, + boolean exclusive) { + Permission permission = project.getAccessSection(ref, true) + .getPermission(permissionName, true); + if (exclusive) { + permission.setExclusiveGroup(exclusive); + } + permission.add(rule); return rule; }
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 0c658bf..c5221f1 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
@@ -37,6 +37,7 @@ import com.google.gerrit.extensions.api.changes.Changes.QueryRequest; import com.google.gerrit.extensions.api.changes.DraftInput; import com.google.gerrit.extensions.api.changes.HashtagsInput; +import com.google.gerrit.extensions.api.changes.NotifyHandling; import com.google.gerrit.extensions.api.changes.ReviewInput; import com.google.gerrit.extensions.api.changes.StarsInput; import com.google.gerrit.extensions.api.groups.GroupInput; @@ -1583,7 +1584,7 @@ PatchSetInserter inserter = patchSetFactory.create( ctl, new PatchSet.Id(c.getId(), n), commit) - .setSendMail(false) + .setNotify(NotifyHandling.NONE) .setFireRevisionCreated(false) .setValidatePolicy(CommitValidators.Policy.NONE); try (BatchUpdate bu = updateFactory.create(
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 9e5b776..5fb930c 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
@@ -18,11 +18,11 @@ import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.gpg.GpgModule; import com.google.gerrit.metrics.DisabledMetricMaker; import com.google.gerrit.metrics.MetricMaker; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.GerritPersonIdentProvider;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java index ac64803..bffb114 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -15,6 +15,7 @@ package com.google.gerrit.sshd.commands; import com.google.gerrit.extensions.api.changes.AddReviewerInput; +import com.google.gerrit.extensions.api.changes.DeleteReviewerInput; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Change; @@ -111,7 +112,7 @@ ReviewerResource rsrc = reviewerFactory.create(changeRsrc, reviewer); String error = null; try { - deleteReviewer.apply(rsrc, new DeleteReviewer.Input()); + deleteReviewer.apply(rsrc, new DeleteReviewerInput()); } catch (ResourceNotFoundException e) { error = String.format("could not remove %s: not found", reviewer); } catch (Exception e) {
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml index f5b078e..acc9b86 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.13-SNAPSHOT</version> + <version>2.14-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/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java index 71eed63..5790453 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
@@ -19,6 +19,7 @@ import com.google.common.base.Splitter; import com.google.gerrit.common.EventBroker; +import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.gpg.GpgModule; import com.google.gerrit.httpd.auth.oauth.OAuthModule; import com.google.gerrit.httpd.auth.openid.OpenIdModule; @@ -29,7 +30,6 @@ import com.google.gerrit.lucene.LuceneIndexModule; import com.google.gerrit.metrics.dropwizard.DropWizardMetricMaker; import com.google.gerrit.pgm.util.LogFileCompressor; -import com.google.gerrit.reviewdb.client.AuthType; import com.google.gerrit.server.account.InternalAccountDirectory; import com.google.gerrit.server.cache.h2.DefaultCacheFactory; import com.google.gerrit.server.change.ChangeCleanupRunner;
diff --git a/lib/BUCK b/lib/BUCK index ecd30ae..6e843c4 100644 --- a/lib/BUCK +++ b/lib/BUCK
@@ -17,6 +17,7 @@ define_license(name = 'fetch') define_license(name = 'h2') define_license(name = 'highlightjs') +define_license(name = 'icu4j') define_license(name = 'jgit') define_license(name = 'jsch') define_license(name = 'MPL1.1') @@ -90,7 +91,11 @@ # Whitelist lib targets that have jsr305 as a dependency. Generally speaking # Gerrit core should not depend on these annotations, and instead use # equivalent annotations in com.google.gerrit.common. - visibility = ['//lib:guava-retrying'], + visibility = [ + '//gerrit-plugin-api:lib', + '//lib:guava-retrying', + '//lib:soy', + ], ) maven_jar( @@ -273,3 +278,34 @@ license = 'Apache2.0', repository = GERRIT, ) + +# Keep this version of Soy synchronized with the version used in Gitiles. +maven_jar( + name = 'soy', + id = 'com.google.template:soy:2016-08-09', + sha1 = '43d33651e95480d515fe26c10a662faafe3ad1e4', + license = 'Apache2.0', + deps = [ + ':args4j', + ':guava', + ':gson', + ':icu4j', + ':jsr305', + ':protobuf', + '//lib/guice:guice', + '//lib/guice:guice-assistedinject', + '//lib/guice:multibindings', + '//lib/guice:javax-inject', + '//lib/ow2:ow2-asm', + '//lib/ow2:ow2-asm-analysis', + '//lib/ow2:ow2-asm-commons', + '//lib/ow2:ow2-asm-util', + ], +) + +maven_jar( + name = 'icu4j', + id = 'com.ibm.icu:icu4j:57.1', + sha1 = '198ea005f41219f038f4291f0b0e9f3259730e92', + license = 'icu4j', +)
diff --git a/lib/BUILD b/lib/BUILD index e89e63c..a490038 100644 --- a/lib/BUILD +++ b/lib/BUILD
@@ -151,6 +151,7 @@ visibility = ['//visibility:public'], ) + java_library( name = 'h2', exports = ['@h2//jar'], @@ -202,3 +203,31 @@ exports = ['@derby//jar'], visibility = ['//visibility:public'], ) + +java_library( + name = 'soy', + exports = ['@soy//jar'], + runtime_deps = [ + ':args4j', + ':guava', + ':gson', + ':icu4j', + ':jsr305', + ':protobuf', + '//lib/guice:guice', + '//lib/guice:guice-assistedinject', + '//lib/guice:multibindings', + '//lib/guice:javax-inject', + '//lib/ow2:ow2-asm', + '//lib/ow2:ow2-asm-analysis', + '//lib/ow2:ow2-asm-commons', + '//lib/ow2:ow2-asm-util', + ], + visibility = ['//visibility:public'], +) + +java_library( + name = 'icu4j', + exports = [ '@icu4j//jar' ], + visibility = ['//visibility:public'], +)
diff --git a/lib/LICENSE-icu4j b/lib/LICENSE-icu4j new file mode 100644 index 0000000..90be7cd --- /dev/null +++ b/lib/LICENSE-icu4j
@@ -0,0 +1,385 @@ +COPYRIGHT AND PERMISSION NOTICE (ICU 58 and later) + +Copyright © 1991-2016 Unicode, Inc. All rights reserved. +Distributed under the Terms of Use in http://www.unicode.org/copyright.html + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. + +--------------------- + +Third-Party Software Licenses + +This section contains third-party software notices and/or additional +terms for licensed third-party software components included within ICU +libraries. + +1. ICU License - ICU 1.8.1 to ICU 57.1 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1995-2016 International Business Machines Corporation and others +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, provided that the above +copyright notice(s) and this permission notice appear in all copies of +the Software and that both the above copyright notice(s) and this +permission notice appear in supporting documentation. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY +SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, use +or other dealings in this Software without prior written authorization +of the copyright holder. + +All trademarks and registered trademarks mentioned herein are the +property of their respective owners. + +2. Chinese/Japanese Word Break Dictionary Data (cjdict.txt) + + # The Google Chrome software developed by Google is licensed under + # the BSD license. Other software included in this distribution is + # provided under other licenses, as set forth below. + # + # The BSD License + # http://opensource.org/licenses/bsd-license.php + # Copyright (C) 2006-2008, Google Inc. + # + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions are met: + # + # Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # Redistributions in binary form must reproduce the above + # copyright notice, this list of conditions and the following + # disclaimer in the documentation and/or other materials provided with + # the distribution. + # Neither the name of Google Inc. nor the names of its + # contributors may be used to endorse or promote products derived from + # this software without specific prior written permission. + # + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + # + # + # The word list in cjdict.txt are generated by combining three word lists + # listed below with further processing for compound word breaking. The + # frequency is generated with an iterative training against Google web + # corpora. + # + # * Libtabe (Chinese) + # - https://sourceforge.net/project/?group_id=1519 + # - Its license terms and conditions are shown below. + # + # * IPADIC (Japanese) + # - http://chasen.aist-nara.ac.jp/chasen/distribution.html + # - Its license terms and conditions are shown below. + # + # ---------COPYING.libtabe ---- BEGIN-------------------- + # + # /* + # * Copyrighy (c) 1999 TaBE Project. + # * Copyright (c) 1999 Pai-Hsiang Hsiao. + # * All rights reserved. + # * + # * Redistribution and use in source and binary forms, with or without + # * modification, are permitted provided that the following conditions + # * are met: + # * + # * . Redistributions of source code must retain the above copyright + # * notice, this list of conditions and the following disclaimer. + # * . Redistributions in binary form must reproduce the above copyright + # * notice, this list of conditions and the following disclaimer in + # * the documentation and/or other materials provided with the + # * distribution. + # * . Neither the name of the TaBE Project nor the names of its + # * contributors may be used to endorse or promote products derived + # * from this software without specific prior written permission. + # * + # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # * OF THE POSSIBILITY OF SUCH DAMAGE. + # */ + # + # /* + # * Copyright (c) 1999 Computer Systems and Communication Lab, + # * Institute of Information Science, Academia + # * Sinica. All rights reserved. + # * + # * Redistribution and use in source and binary forms, with or without + # * modification, are permitted provided that the following conditions + # * are met: + # * + # * . Redistributions of source code must retain the above copyright + # * notice, this list of conditions and the following disclaimer. + # * . Redistributions in binary form must reproduce the above copyright + # * notice, this list of conditions and the following disclaimer in + # * the documentation and/or other materials provided with the + # * distribution. + # * . Neither the name of the Computer Systems and Communication Lab + # * nor the names of its contributors may be used to endorse or + # * promote products derived from this software without specific + # * prior written permission. + # * + # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # * OF THE POSSIBILITY OF SUCH DAMAGE. + # */ + # + # Copyright 1996 Chih-Hao Tsai @ Beckman Institute, + # University of Illinois + # c-tsai4@uiuc.edu http://casper.beckman.uiuc.edu/~c-tsai4 + # + # ---------------COPYING.libtabe-----END-------------------------------- + # + # + # ---------------COPYING.ipadic-----BEGIN------------------------------- + # + # Copyright 2000, 2001, 2002, 2003 Nara Institute of Science + # and Technology. All Rights Reserved. + # + # Use, reproduction, and distribution of this software is permitted. + # Any copy of this software, whether in its original form or modified, + # must include both the above copyright notice and the following + # paragraphs. + # + # Nara Institute of Science and Technology (NAIST), + # the copyright holders, disclaims all warranties with regard to this + # software, including all implied warranties of merchantability and + # fitness, in no event shall NAIST be liable for + # any special, indirect or consequential damages or any damages + # whatsoever resulting from loss of use, data or profits, whether in an + # action of contract, negligence or other tortuous action, arising out + # of or in connection with the use or performance of this software. + # + # A large portion of the dictionary entries + # originate from ICOT Free Software. The following conditions for ICOT + # Free Software applies to the current dictionary as well. + # + # Each User may also freely distribute the Program, whether in its + # original form or modified, to any third party or parties, PROVIDED + # that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear + # on, or be attached to, the Program, which is distributed substantially + # in the same form as set out herein and that such intended + # distribution, if actually made, will neither violate or otherwise + # contravene any of the laws and regulations of the countries having + # jurisdiction over the User or the intended distribution itself. + # + # NO WARRANTY + # + # The program was produced on an experimental basis in the course of the + # research and development conducted during the project and is provided + # to users as so produced on an experimental basis. Accordingly, the + # program is provided without any warranty whatsoever, whether express, + # implied, statutory or otherwise. The term "warranty" used herein + # includes, but is not limited to, any warranty of the quality, + # performance, merchantability and fitness for a particular purpose of + # the program and the nonexistence of any infringement or violation of + # any right of any third party. + # + # Each user of the program will agree and understand, and be deemed to + # have agreed and understood, that there is no warranty whatsoever for + # the program and, accordingly, the entire risk arising from or + # otherwise connected with the program is assumed by the user. + # + # Therefore, neither ICOT, the copyright holder, or any other + # organization that participated in or was otherwise related to the + # development of the program and their respective officials, directors, + # officers and other employees shall be held liable for any and all + # damages, including, without limitation, general, special, incidental + # and consequential damages, arising out of or otherwise in connection + # with the use or inability to use the program or any product, material + # or result produced or otherwise obtained by using the program, + # regardless of whether they have been advised of, or otherwise had + # knowledge of, the possibility of such damages at any time during the + # project or thereafter. Each user will be deemed to have agreed to the + # foregoing by his or her commencement of use of the program. The term + # "use" as used herein includes, but is not limited to, the use, + # modification, copying and distribution of the program and the + # production of secondary products from the program. + # + # In the case where the program, whether in its original form or + # modified, was distributed or delivered to or received by a user from + # any person, organization or entity other than ICOT, unless it makes or + # grants independently of ICOT any specific warranty to the user in + # writing, such person, organization or entity, will also be exempted + # from and not be held liable to the user for any such damages as noted + # above as far as the program is concerned. + # + # ---------------COPYING.ipadic-----END---------------------------------- + +3. Lao Word Break Dictionary Data (laodict.txt) + + # Copyright (c) 2013 International Business Machines Corporation + # and others. All Rights Reserved. + # + # Project: http://code.google.com/p/lao-dictionary/ + # Dictionary: http://lao-dictionary.googlecode.com/git/Lao-Dictionary.txt + # License: http://lao-dictionary.googlecode.com/git/Lao-Dictionary-LICENSE.txt + # (copied below) + # + # This file is derived from the above dictionary, with slight + # modifications. + # ---------------------------------------------------------------------- + # Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell. + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, + # are permitted provided that the following conditions are met: + # + # + # Redistributions of source code must retain the above copyright notice, this + # list of conditions and the following disclaimer. Redistributions in + # binary form must reproduce the above copyright notice, this list of + # conditions and the following disclaimer in the documentation and/or + # other materials provided with the distribution. + # + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # OF THE POSSIBILITY OF SUCH DAMAGE. + # -------------------------------------------------------------------------- + +4. Burmese Word Break Dictionary Data (burmesedict.txt) + + # Copyright (c) 2014 International Business Machines Corporation + # and others. All Rights Reserved. + # + # This list is part of a project hosted at: + # github.com/kanyawtech/myanmar-karen-word-lists + # + # -------------------------------------------------------------------------- + # Copyright (c) 2013, LeRoy Benjamin Sharon + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: Redistributions of source code must retain the above + # copyright notice, this list of conditions and the following + # disclaimer. Redistributions in binary form must reproduce the + # above copyright notice, this list of conditions and the following + # disclaimer in the documentation and/or other materials provided + # with the distribution. + # + # Neither the name Myanmar Karen Word Lists, nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS + # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + # SUCH DAMAGE. + # -------------------------------------------------------------------------- + +5. Time Zone Database + + ICU uses the public domain data and code derived from Time Zone +Database for its time zone support. The ownership of the TZ database +is explained in BCP 175: Procedure for Maintaining the Time Zone +Database section 7. + + # 7. Database Ownership + # + # The TZ database itself is not an IETF Contribution or an IETF + # document. Rather it is a pre-existing and regularly updated work + # that is in the public domain, and is intended to remain in the + # public domain. Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do + # not apply to the TZ Database or contributions that individuals make + # to it. Should any claims be made and substantiated against the TZ + # Database, the organization that is providing the IANA + # Considerations defined in this RFC, under the memorandum of + # understanding with the IETF, currently ICANN, may act in accordance + # with all competent court orders. No ownership claims will be made + # by ICANN or the IETF Trust on the database or the code. Any person + # making a contribution to the database or code waives all rights to + # future claims in that contribution or in the TZ Database.
diff --git a/lib/codemirror/BUCK b/lib/codemirror/BUCK index a0e0e9a..56145ea 100644 --- a/lib/codemirror/BUCK +++ b/lib/codemirror/BUCK
@@ -1,14 +1,14 @@ include_defs('//lib/maven.defs') include_defs('//lib/codemirror/cm.defs') -VERSION = '5.17.0' +VERSION = '5.18.2' TOP = 'META-INF/resources/webjars/codemirror/%s' % VERSION TOP_MINIFIED = 'META-INF/resources/webjars/codemirror-minified/%s' % VERSION maven_jar( name = 'codemirror-minified', id = 'org.webjars.npm:codemirror-minified:' + VERSION, - sha1 = '05ad901fc9be67eb7ba8997d896488093deb898e', + sha1 = '6755af157a7eaf2401468906bef67bbacc3c97f6', attach_source = False, license = 'codemirror-minified', visibility = [], @@ -17,7 +17,7 @@ maven_jar( name = 'codemirror-original', id = 'org.webjars.npm:codemirror:' + VERSION, - sha1 = 'c025b8d9aca1061e26d1fa482bea0ecea1412e85', + sha1 = '18c721ae88eed27cddb458c42f5d221fa3d9713e', attach_source = False, license = 'codemirror-original', visibility = [],
diff --git a/lib/codemirror/cm.defs b/lib/codemirror/cm.defs index baf2ce5..a1be90f 100644 --- a/lib/codemirror/cm.defs +++ b/lib/codemirror/cm.defs
@@ -132,7 +132,6 @@ 'htmlmixed', 'http', 'idl', - 'jade', 'javascript', 'jinja2', 'jsx', @@ -160,6 +159,7 @@ 'powershell', 'properties', 'protobuf', + 'pug', 'puppet', 'python', 'q',
diff --git a/lib/guice/BUCK b/lib/guice/BUCK index 867b521..8022ac8 100644 --- a/lib/guice/BUCK +++ b/lib/guice/BUCK
@@ -12,6 +12,7 @@ exported_deps = [ ':guice_library', ':javax-inject', + ':multibindings', ], visibility = ['PUBLIC'], ) @@ -63,3 +64,16 @@ license = 'Apache2.0', visibility = ['PUBLIC'], ) + +maven_jar( + name = 'multibindings', + id = 'com.google.inject.extensions:guice-multibindings:' + VERSION, + sha1 = '3b27257997ac51b0f8d19676f1ea170427e86d51', + exclude_java_sources = True, + exclude = EXCLUDE + [ + 'META-INF/maven/com.google.guava/guava/pom.properties', + 'META-INF/maven/com.google.guava/guava/pom.xml', + ], + license = 'Apache2.0', + visibility = ['PUBLIC'] +)
diff --git a/lib/guice/BUILD b/lib/guice/BUILD index acade50..5850af2 100644 --- a/lib/guice/BUILD +++ b/lib/guice/BUILD
@@ -3,6 +3,7 @@ exports = [ ':guice_library', ':javax-inject', + ':multibindings', ], visibility = ['//visibility:public'], ) @@ -36,4 +37,11 @@ java_library( name = 'javax-inject', exports = ['@javax_inject//jar'], + visibility = ['//visibility:public'], +) + +java_library( + name = 'multibindings', + exports = [ '@multibindings//jar' ], + visibility = ['//visibility:public'], )
diff --git a/lib/lucene/BUCK b/lib/lucene/BUCK index c4a9872..dee8ce8 100644 --- a/lib/lucene/BUCK +++ b/lib/lucene/BUCK
@@ -1,6 +1,6 @@ include_defs('//lib/maven.defs') -VERSION = '5.5.0' +VERSION = '5.5.2' # core and backward-codecs both provide # META-INF/services/org.apache.lucene.codecs.Codec, so they must be merged. @@ -16,7 +16,7 @@ maven_jar( name = 'lucene-core', id = 'org.apache.lucene:lucene-core:' + VERSION, - sha1 = 'a74fd869bb5ad7fe6b4cd29df9543a34aea81164', + sha1 = 'de5e5c3161ea01e89f2a09a14391f9b7ed66cdbb', license = 'Apache2.0', exclude = [ 'META-INF/LICENSE.txt', @@ -28,7 +28,7 @@ maven_jar( name = 'lucene-analyzers-common', id = 'org.apache.lucene:lucene-analyzers-common:' + VERSION, - sha1 = '1e0e8243a4410be20c34683034fafa7bb52e55cc', + sha1 = 'f0bc3114a6b43f8e64a33c471d5b9e8ddc51564d', license = 'Apache2.0', deps = [':lucene-core-and-backward-codecs'], exclude = [ @@ -40,7 +40,7 @@ maven_jar( name = 'backward-codecs_jar', id = 'org.apache.lucene:lucene-backward-codecs:' + VERSION, - sha1 = '68480974b2f54f519763632a7c1c5d51cbff3805', + sha1 = 'c5cfcd7a8cf48a0144b61fb991c8e50a0bf868d5', license = 'Apache2.0', deps = [':lucene-core'], exclude = [ @@ -53,7 +53,7 @@ maven_jar( name = 'lucene-misc', id = 'org.apache.lucene:lucene-misc:' + VERSION, - sha1 = '504d855a1a38190622fdf990b2298c067e7d60ca', + sha1 = '37bbe5a2fb429499dfbe75d750d1778881fff45d', license = 'Apache2.0', deps = [':lucene-core-and-backward-codecs'], exclude = [ @@ -65,7 +65,7 @@ maven_jar( name = 'lucene-queryparser', id = 'org.apache.lucene:lucene-queryparser:' + VERSION, - sha1 = '0fddc49725b562fd48dff0cff004336ad2a090a4', + sha1 = '8ac921563e744463605284c6d9d2d95e1be5b87c', license = 'Apache2.0', deps = [':lucene-core-and-backward-codecs'], exclude = [
diff --git a/lib/ow2/BUCK b/lib/ow2/BUCK index fabcb25..653bd2b 100644 --- a/lib/ow2/BUCK +++ b/lib/ow2/BUCK
@@ -1,25 +1,25 @@ include_defs('//lib/maven.defs') -VERSION = '5.0.3' +VERSION = '5.1' maven_jar( name = 'ow2-asm', id = 'org.ow2.asm:asm:' + VERSION, - sha1 = 'dcc2193db20e19e1feca8b1240dbbc4e190824fa', + sha1 = '5ef31c4fe953b1fd00b8a88fa1d6820e8785bb45', license = 'ow2', ) maven_jar( name = 'ow2-asm-analysis', id = 'org.ow2.asm:asm-analysis:' + VERSION, - sha1 = 'c7126aded0e8e13fed5f913559a0dd7b770a10f3', + sha1 = '6d1bf8989fc7901f868bee3863c44f21aa63d110', license = 'ow2', ) maven_jar( name = 'ow2-asm-commons', id = 'org.ow2.asm:asm-commons:' + VERSION, - sha1 = 'a7111830132c7f87d08fe48cb0ca07630f8cb91c', + sha1 = '25d8a575034dd9cfcb375a39b5334f0ba9c8474e', deps = [':ow2-asm-tree'], license = 'ow2', ) @@ -27,14 +27,13 @@ maven_jar( name = 'ow2-asm-tree', id = 'org.ow2.asm:asm-tree:' + VERSION, - sha1 = '287749b48ba7162fb67c93a026d690b29f410bed', + sha1 = '87b38c12a0ea645791ead9d3e74ae5268d1d6c34', license = 'ow2', ) maven_jar( name = 'ow2-asm-util', id = 'org.ow2.asm:asm-util:' + VERSION, - sha1 = '1512e5571325854b05fb1efce1db75fcced54389', + sha1 = 'b60e33a6bd0d71831e0c249816d01e6c1dd90a47', license = 'ow2', ) -
diff --git a/plugins/download-commands b/plugins/download-commands index 7b41f3a..3fb4fb6 160000 --- a/plugins/download-commands +++ b/plugins/download-commands
@@ -1 +1 @@ -Subproject commit 7b41f3a413b46140b050ae5324cbbcdd467d2b3a +Subproject commit 3fb4fb63317b6004761d1fea98a8f4d288d95409
diff --git a/plugins/hooks b/plugins/hooks index 3acc14d..c1705a7 160000 --- a/plugins/hooks +++ b/plugins/hooks
@@ -1 +1 @@ -Subproject commit 3acc14d10d26678eae6489038fe0d4dad644a9b4 +Subproject commit c1705a739f117b9123e1d63aebf07d043afb0867
diff --git a/plugins/replication b/plugins/replication index c5123d6..5cac325 160000 --- a/plugins/replication +++ b/plugins/replication
@@ -1 +1 @@ -Subproject commit c5123d6a5604cc740d6f42485235c0d3ec141c4e +Subproject commit 5cac325cca171205130c53df8b3ee9ab3b115979
diff --git a/plugins/reviewnotes b/plugins/reviewnotes index 3f3d572..46079ec 160000 --- a/plugins/reviewnotes +++ b/plugins/reviewnotes
@@ -1 +1 @@ -Subproject commit 3f3d572e9618f268b19cc54856deee4c96180e4c +Subproject commit 46079ec92478ddc1e9ffd84eae22fb6af788c9fd
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 90b2e1d..a43f564 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
@@ -108,11 +108,15 @@ }, _computeProjectURL: function(project) { - return '/q/status:open+project:' + project; + // @see Issue 4255. + return '/q/status:open+project:' + + encodeURIComponent(encodeURIComponent(project)); }, _computeProjectBranchURL: function(project, branch) { - return '/q/status:open+project:' + project + '+branch:' + branch; + // @see Issue 4255. + return this._computeProjectURL(project) + + '+branch:' + encodeURIComponent(encodeURIComponent(branch)); }, }); })();
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 b7c0853..d2e6028 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
@@ -120,12 +120,12 @@ assert.equal(element._computeLabelValue( {labels: {Verified: {rejected: true}}}, 'Verified'), '✕'); - assert.equal(element._computeProjectURL('combustible-stuff'), - '/q/status:open+project:combustible-stuff'); + assert.equal(element._computeProjectURL('combustible/stuff'), + '/q/status:open+project:combustible%252Fstuff'); assert.equal(element._computeProjectBranchURL( - 'combustible-stuff', 'lemons'), - '/q/status:open+project:combustible-stuff+branch:lemons'); + 'combustible-stuff', 'le/mons'), + '/q/status:open+project:combustible-stuff+branch:le%252Fmons'); element.change = {_number: 42}; assert.equal(element.changeURL, '/c/42/');
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 b741784..09d5b84 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
@@ -32,19 +32,14 @@ <template> <style> :host { - display: block; + display: inline-block; + font-family: var(--font-family); } section { - margin-top: 1em; - } - .groupLabel { - color: #666; - margin-bottom: .15em; - text-align: center; + display: inline-block; } gr-button { - display: block; - margin-bottom: .5em; + margin-left: .5em; } gr-button:before { content: attr(data-label); @@ -53,6 +48,15 @@ content: attr(data-loading-label); } @media screen and (max-width: 50em) { + :host, + section, + gr-button { + display: block; + } + gr-button { + margin-bottom: .5em; + margin-left: 0; + } .confirmDialog { width: 90vw; } @@ -60,7 +64,6 @@ </style> <div> <section hidden$="[[!_actionCount(actions.*, _additionalActions.*)]]"> - <div class="groupLabel">Change</div> <template is="dom-repeat" items="[[_changeActionValues]]" as="action"> <gr-button title$="[[action.title]]" primary$="[[action.__primary]]" @@ -73,7 +76,6 @@ </template> </section> <section hidden$="[[!_actionCount(_revisionActions.*, _additionalActions.*)]]"> - <div class="groupLabel">Revision</div> <template is="dom-repeat" items="[[_revisionActionValues]]" as="action"> <gr-button title$="[[action.title]]" primary$="[[action.__primary]]" @@ -94,13 +96,12 @@ hidden></gr-confirm-rebase-dialog> <gr-confirm-cherrypick-dialog id="confirmCherrypick" class="confirmDialog" - commit-info="[[commitInfo]]" + message="[[commitMessage]]" on-confirm="_handleCherrypickConfirm" on-cancel="_handleConfirmDialogCancel" hidden></gr-confirm-cherrypick-dialog> <gr-confirm-revert-dialog id="confirmRevertDialog" class="confirmDialog" - commit-info="[[commitInfo]]" on-confirm="_handleRevertDialogConfirm" on-cancel="_handleConfirmDialogCancel" hidden></gr-confirm-revert-dialog>
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 3445f4e..8deab6b 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
@@ -75,7 +75,10 @@ }, changeNum: String, patchNum: String, - commitInfo: Object, + commitMessage: { + type: String, + value: '', + }, _loading: { type: Boolean, @@ -274,7 +277,7 @@ if (type === ActionType.REVISION) { this._handleRevisionAction(key); } else if (key === ChangeActions.REVERT) { - this.$.confirmRevertDialog.populateRevertMessage(); + this.$.confirmRevertDialog.populateRevertMessage(this.commitMessage); this.$.confirmRevertDialog.message = this._modifyRevertMsg(); this._showActionDialog(this.$.confirmRevertDialog); } else if (key === ChangeActions.ABANDON) {
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 8b51312..1782061 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
@@ -128,18 +128,6 @@ <span class="value">[[change.branch]]</span> </section> <section> - <span class="title">Commit</span> - <span class="value"> - <template is="dom-if" if="[[_showWebLink]]"> - <a target="_blank" - href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a> - </template> - <template is="dom-if" if="[[!_showWebLink]]"> - [[_computeShortHash(commitInfo)]] - </template> - </span> - </section> - <section> <span class="title">Topic</span> <span class="value"> <gr-editable-label
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 af19703..20c117e 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
@@ -27,17 +27,8 @@ properties: { change: Object, - commitInfo: Object, mutable: Boolean, serverConfig: Object, - _showWebLink: { - type: Boolean, - computed: '_computeShowWebLink(change, commitInfo, serverConfig)', - }, - _webLink: { - type: String, - computed: '_computeWebLink(change, commitInfo, serverConfig)', - }, _topicReadOnly: { type: Boolean, computed: '_computeTopicReadOnly(mutable, change)', @@ -52,38 +43,6 @@ Gerrit.RESTClientBehavior, ], - _computeShowWebLink: function(change, commitInfo, serverConfig) { - var webLink = commitInfo.web_links && commitInfo.web_links.length; - var gitWeb = serverConfig.gitweb && serverConfig.gitweb.url && - serverConfig.gitweb.type && serverConfig.gitweb.type.revision; - return webLink || gitWeb; - }, - - _computeWebLink: function(change, commitInfo, serverConfig) { - if (!this._computeShowWebLink(change, commitInfo, serverConfig)) { - return; - } - - if (serverConfig.gitweb && serverConfig.gitweb.url && - serverConfig.gitweb.type && serverConfig.gitweb.type.revision) { - return serverConfig.gitweb.url + - serverConfig.gitweb.type.revision - .replace('${project}', change.project) - .replace('${commit}', commitInfo.commit); - } - - var webLink = commitInfo.web_links[0].url; - if (!/^https?\:\/\//.test(webLink)) { - webLink = '../../' + webLink; - } - - return webLink; - }, - - _computeShortHash: function(commitInfo) { - return commitInfo.commit.slice(0, 7); - }, - _computeHideStrategy: function(change) { return !this.changeIsOpen(change.status); },
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 01f0649..a2d4946 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,11 +20,9 @@ <script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> <script src="../../../bower_components/web-component-tester/browser.js"></script> -<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-metadata.html"> -<script src="../../../scripts/util.js"></script> <test-fixture id="basic"> <template> @@ -68,79 +66,6 @@ assert.isTrue(element.$$('.strategy').hasAttribute('hidden')); }); - test('no web link when unavailable', function() { - element.commitInfo = {}; - element.serverConfig = {}; - element.change = {labels: []}; - - assert.isNotOk(element._computeShowWebLink(element.change, - element.commitInfo, element.serverConfig)); - }); - - test('use web link when available', function() { - element.commitInfo = {web_links: [{url: 'link-url'}]}; - element.serverConfig = {}; - - assert.isOk(element._computeShowWebLink(element.change, - element.commitInfo, element.serverConfig)); - assert.equal(element._computeWebLink(element.change, element.commitInfo, - element.serverConfig), '../../link-url'); - }); - - test('does not relativize web links that begin with scheme', function() { - element.commitInfo = {web_links: [{url: 'https://link-url'}]}; - element.serverConfig = {}; - - assert.isOk(element._computeShowWebLink(element.change, - element.commitInfo, element.serverConfig)); - assert.equal(element._computeWebLink(element.change, element.commitInfo, - element.serverConfig), 'https://link-url'); - }); - - test('use gitweb when available', function() { - element.commitInfo = {commit: 'commit-sha'}; - element.serverConfig = {gitweb: { - url: 'url-base/', - type: {revision: 'xx ${project} xx ${commit} xx'}, - }}; - element.change = { - project: 'project-name', - labels: [], - current_revision: element.commitInfo.commit - }; - - assert.isOk(element._computeShowWebLink(element.change, - element.commitInfo, element.serverConfig)); - - assert.equal(element._computeWebLink(element.change, element.commitInfo, - element.serverConfig), 'url-base/xx project-name xx commit-sha xx'); - }); - - test('prefer gitweb when both are available', function() { - element.commitInfo = { - commit: 'commit-sha', - web_links: [{url: 'link-url'}] - }; - element.serverConfig = {gitweb: { - url: 'url-base/', - type: {revision: 'xx ${project} xx ${commit} xx'}, - }}; - element.change = { - project: 'project-name', - labels: [], - current_revision: element.commitInfo.commit - }; - - assert.isOk(element._computeShowWebLink(element.change, - element.commitInfo, element.serverConfig)); - - var 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('show CC section when NoteDb enabled', function() { function hasCc() { return element._showReviewersByState;
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 e3f7fd2..54f07a0 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
@@ -29,6 +29,7 @@ <link rel="import" href="../gr-change-actions/gr-change-actions.html"> <link rel="import" href="../gr-change-metadata/gr-change-metadata.html"> +<link rel="import" href="../gr-commit-info/gr-commit-info.html"> <link rel="import" href="../gr-download-dialog/gr-download-dialog.html"> <link rel="import" href="../gr-file-list/gr-file-list.html"> <link rel="import" href="../gr-messages-list/gr-messages-list.html"> @@ -45,18 +46,16 @@ color: #666; padding: 1em var(--default-horizontal-margin); } - .headerContainer { - height: 4.1em; - margin-bottom: .5em; - } .header { align-items: center; background-color: var(--view-background-color); - border-bottom: 1px solid #ddd; display: flex; - padding: 1em var(--default-horizontal-margin); + padding: .65em var(--default-horizontal-margin); z-index: 99; /* Less than gr-overlay's backdrop */ } + .header .download { + margin-right: 1em; + } .header.pinned { border-bottom-color: transparent; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); @@ -69,24 +68,11 @@ flex: 1; font-size: 1.2em; font-weight: bold; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } gr-change-star { margin-right: .25em; vertical-align: -.425em; } - .download, - .patchSelectLabel { - margin-left: 1em; - } - .header select { - margin-left: .5em; - } - .header .reply { - margin-left: var(--default-horizontal-margin); - } gr-reply-dialog { width: 50em; } @@ -94,45 +80,46 @@ color: #999; text-transform: capitalize; } - section { - margin: 10px 0; - padding: 10px var(--default-horizontal-margin); - } /* Strong specificity here is needed due to https://github.com/Polymer/polymer/issues/2531 */ .container section.changeInfo { - border-bottom: 1px solid #ddd; display: flex; - margin-top: 0; - padding-top: 0; + padding: 0 var(--default-horizontal-margin); } .changeInfo-column:not(:last-of-type) { margin-right: 1em; padding-right: 1em; } .changeMetadata { - border-right: 1px solid #ddd; - font-size: .9em; - } - gr-change-actions { - margin-top: 1em; + font-size: .95em; } .commitMessage { font-family: var(--monospace-font-family); flex: 0 0 72ch; margin-right: 2em; margin-bottom: 1em; - overflow-x: hidden; - } - .commitMessage h4 { - font-family: var(--font-family); - font-weight: bold; - margin-bottom: .25em; } .commitMessage gr-linked-text { - --linked-text-white-space: pre; overflow: auto; } + .editCommitMessage { + margin-top: 1em; + } + .commitActions { + border-bottom: 1px solid #ddd; + display: flex; + justify-content: space-between; + margin-bottom: .5em; + padding-bottom: .5em; + } + .reply { + margin-right: .5em; + } + .mainChangeInfo { + display: flex; + flex: 1; + flex-direction: column; + } .commitAndRelated { align-content: flex-start; display: flex; @@ -144,14 +131,30 @@ font-size: .9em; overflow: hidden; } + .patchInfo { + border: 1px solid #ddd; + margin: 1em var(--default-horizontal-margin); + } + .patchInfo--oldPatchSet .patchInfo-header { + background-color: #fff9c4; + } + .patchInfo--oldPatchSet .latestPatchContainer { + display: initial; + } + .patchInfo-header, gr-file-list { - margin-bottom: 1em; - padding: 0 var(--default-horizontal-margin); + padding: .5em calc(var(--default-horizontal-margin) / 2); + } + .patchInfo-header { + background-color: #f6f6f6; + border-bottom: 1px solid #ebebeb; + display: flex; + justify-content: space-between; + } + .latestPatchContainer { + display: none; } @media screen and (max-width: 50em) { - .headerContainer { - height: 5.15em; - } .header { align-items: flex-start; flex-direction: column; @@ -163,30 +166,17 @@ .header-title { font-size: 1.1em; } - .header-actions { - align-items: center; - display: flex; - justify-content: space-between; - margin-top: .5em; - } gr-reply-dialog { min-width: initial; width: 90vw; } - .download { + .downloadContainer { display: none; } - .patchSelectLabel { - margin-left: 0; - margin-right: .5em; - } - .header select { - margin-left: 0; - margin-right: .5em; - } - .header .reply { - margin-left: 0; - margin-right: .5em; + .reply { + display: block; + margin-right: 0; + margin-bottom: .5em; } .changeInfo-column:not(:last-of-type) { margin-right: 0; @@ -207,6 +197,9 @@ margin-top: .25em; max-width: none; } + .commitActions { + flex-direction: column; + } .commitMessage { flex: initial; margin-right: 0; @@ -215,85 +208,98 @@ </style> <div class="container loading" hidden$="{{!_loading}}">Loading...</div> <div class="container" hidden$="{{_loading}}"> - <div class="headerContainer"> - <div class="header"> - <span class="header-title"> - <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star> - <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span> - <span>[[_change.subject]]</span> - <span class="changeStatus">[[_computeChangeStatus(_change, _patchRange.patchNum)]]</span> - </span> - <span class="header-actions"> - <gr-button hidden - class="reply" - primary$="[[_computeReplyButtonHighlighted(_diffDrafts.*)]]" - hidden$="[[!_loggedIn]]" - on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button> - <gr-button class="download" on-tap="_handleDownloadTap">Download</gr-button> - <span> - <label class="patchSelectLabel" for="patchSetSelect">Patch set</label> - <select id="patchSetSelect" on-change="_handlePatchChange"> - <template is="dom-repeat" items="{{_allPatchSets}}" as="patchNumber"> - <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]"> - <span>[[patchNumber]]</span> - / - <span>[[_computeLatestPatchNum(_allPatchSets)]]</span> - </option> - </template> - </select> - </span> - </span> - </div> + <div class="header"> + <span class="header-title"> + <gr-change-star change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star> + <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><span>:</span> + <span>[[_change.subject]]</span> + <span class="changeStatus">[[_computeChangeStatus(_change, _patchRange.patchNum)]]</span> + </span> </div> <section class="changeInfo"> <div class="changeInfo-column changeMetadata"> <gr-change-metadata change="{{_change}}" - commit-info="[[_commitInfo]]" server-config="[[serverConfig]]" mutable="[[_loggedIn]]" on-show-reply-dialog="_handleShowReplyDialog"> </gr-change-metadata> - <gr-change-actions id="actions" - change="[[_change]]" - actions="[[_change.actions]]" - change-num="[[_changeNum]]" - patch-num="[[_patchRange.patchNum]]" - commit-info="[[_commitInfo]]" - on-reload-change="_handleReloadChange"></gr-change-actions> </div> - <div class="changeInfo-column commitAndRelated"> - <div class="commitMessage"> - <h4> - Commit message + <div class="changeInfo-column mainChangeInfo"> + <div class="commitActions" hidden$="[[!_loggedIn]]""> + <gr-button + class="reply" + secondary + on-tap="_handleReplyTap">[[_replyButtonLabel]]</gr-button> + <gr-change-actions id="actions" + change="[[_change]]" + actions="[[_change.actions]]" + change-num="[[_changeNum]]" + patch-num="[[_computeLatestPatchNum(_allPatchSets)]]" + commit-message="[[_latestCommitMessage]]" + on-reload-change="_handleReloadChange"></gr-change-actions> + </div> + <div class="commitAndRelated"> + <div class="commitMessage"> + <gr-editable-content id="commitMessageEditor" + editing="[[_editingCommitMessage]]" + content="{{_latestCommitMessage}}"> + <gr-linked-text pre + content="[[_latestCommitMessage]]" + config="[[_projectConfig.commentlinks]]"></gr-linked-text> + </gr-editable-content> <gr-button link + class="editCommitMessage" on-tap="_handleEditCommitMessage" hidden$="[[_hideEditCommitMessage]]">Edit</gr-button> - </h4> - <gr-editable-content id="commitMessageEditor" - editing="[[_editingCommitMessage]]" - content="{{_commitInfo.message}}"> - <gr-linked-text pre - content="[[_commitInfo.message]]" - config="[[_projectConfig.commentlinks]]"></gr-linked-text> - </gr-editable-content> - </div> - <div class="relatedChanges"> - <gr-related-changes-list id="relatedChanges" - change="[[_change]]" - patch-num="[[_patchRange.patchNum]]"></gr-related-changes-list> + </div> + <div class="relatedChanges"> + <gr-related-changes-list id="relatedChanges" + change="[[_change]]" + patch-num="[[_patchRange.patchNum]]"></gr-related-changes-list> + </div> </div> </div> </section> - <gr-file-list id="fileList" - change="[[_change]]" - change-num="[[_changeNum]]" - patch-range="[[_patchRange]]" - comments="[[_comments]]" - drafts="[[_diffDrafts]]" - revisions="[[_change.revisions]]" - projectConfig="[[_projectConfig]]" - selected-index="{{viewState.selectedFileIndex}}"></gr-file-list> + <section class$="patchInfo [[_computePatchInfoClass(_patchRange.patchNum, _allPatchSets)]]"> + <div class="patchInfo-header"> + <div> + <label class="patchSelectLabel" for="patchSetSelect">Patch set</label> + <select id="patchSetSelect" on-change="_handlePatchChange"> + <template is="dom-repeat" items="[[_allPatchSets]]" as="patchNumber"> + <option value$="[[patchNumber]]" selected$="[[_computePatchIndexIsSelected(index, _patchRange.patchNum)]]"> + <span>[[patchNumber]]</span> + / + <span>[[_computeLatestPatchNum(_allPatchSets)]]</span> + </option> + </template> + </select> + <span class="downloadContainer"> + / + <gr-button link + class="download" + on-tap="_handleDownloadTap">Download</gr-button> + </span> + <span class="latestPatchContainer"> + / + <a href$="/c/[[_change._number]]">Go to latest patch set</a> + </span> + </div> + <gr-commit-info + change="[[_change]]" + server-config="[[serverConfig]]" + commit-info="[[_commitInfo]]"></gr-commit-info> + </div> + <gr-file-list id="fileList" + change="[[_change]]" + change-num="[[_changeNum]]" + patch-range="[[_patchRange]]" + comments="[[_comments]]" + drafts="[[_diffDrafts]]" + revisions="[[_change.revisions]]" + projectConfig="[[_projectConfig]]" + selected-index="{{viewState.selectedFileIndex}}"></gr-file-list> + </section> <gr-messages-list id="messageList" change-num="[[_changeNum]]" messages="[[_change.messages]]" @@ -312,12 +318,12 @@ on-close="_handleDownloadDialogClose"></gr-download-dialog> </gr-overlay> <gr-overlay id="replyOverlay" + no-cancel-on-outside-click on-iron-overlay-opened="_handleReplyOverlayOpen" with-backdrop> <gr-reply-dialog id="replyDialog" change="[[_change]]" - patch-num="[[_patchRange.patchNum]]" - revisions="[[_change.revisions]]" + patch-num="[[_computeLatestPatchNum(_allPatchSets)]]" labels="[[_change.labels]]" permitted-labels="[[_change.permitted_labels]]" diff-drafts="[[_diffDrafts]]"
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 14ac4d1..04c2c04 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
@@ -66,7 +66,11 @@ _hideEditCommitMessage: { type: Boolean, computed: '_computeHideEditCommitMessage(_loggedIn, ' + - '_editingCommitMessage, _change.*, _patchRange.patchNum)', + '_editingCommitMessage, _change)', + }, + _latestCommitMessage: { + type: String, + value: '', }, _patchRange: Object, _allPatchSets: { @@ -78,8 +82,6 @@ value: false, }, _loading: Boolean, - _headerContainerEl: Object, - _headerEl: Object, _projectConfig: Object, _replyButtonLabel: { type: String, @@ -98,10 +100,6 @@ '_paramsAndChangeChanged(params, _change)', ], - ready: function() { - this._headerEl = this.$$('.header'); - }, - attached: function() { this._getLoggedIn().then(function(loggedIn) { this._loggedIn = loggedIn; @@ -114,34 +112,6 @@ this._handleCommitMessageSave.bind(this)); this.addEventListener('editable-content-cancel', this._handleCommitMessageCancel.bind(this)); - this.listen(window, 'scroll', '_handleBodyScroll'); - }, - - detached: function() { - this.unlisten(window, 'scroll', '_handleBodyScroll'); - }, - - _handleBodyScroll: function(e) { - var containerEl = this._headerContainerEl || - this.$$('.headerContainer'); - - // Calculate where the header is relative to the window. - var top = containerEl.offsetTop; - for (var offsetParent = containerEl.offsetParent; - offsetParent; - offsetParent = offsetParent.offsetParent) { - top += offsetParent.offsetTop; - } - // The element may not be displayed yet, in which case do nothing. - if (top == 0) { return; } - - this._headerEl.classList.toggle('pinned', window.scrollY >= top); - }, - - _resetHeaderEl: function() { - var el = this._headerEl || this.$$('.header'); - this._headerEl = el; - el.classList.remove('pinned'); }, _handleEditCommitMessage: function(e) { @@ -157,7 +127,7 @@ this.$.commitMessageEditor.disabled = false; if (!resp.ok) { return; } - this.set('_commitInfo.message', message); + this._latestCommitMessage = message; this._editingCommitMessage = false; this._reloadWindow(); }.bind(this)).catch(function(err) { @@ -182,16 +152,8 @@ }.bind(this)); }, - _computeHideEditCommitMessage: function(loggedIn, editing, changeRecord, - patchNum) { - if (!changeRecord || !loggedIn || editing) { return true; } - - patchNum = parseInt(patchNum, 10); - if (isNaN(patchNum)) { return true; } - - var change = changeRecord.base; - if (!change.current_revision) { return true; } - if (change.revisions[change.current_revision]._number !== patchNum) { + _computeHideEditCommitMessage: function(loggedIn, editing, change) { + if (!loggedIn || editing || change.status === this.ChangeStatus.MERGED) { return true; } @@ -267,19 +229,7 @@ }, _handlePatchChange: function(e) { - var patchNum = e.target.value; - var currentPatchNum; - if (this._change.current_revision) { - currentPatchNum = - this._change.revisions[this._change.current_revision]._number; - } else { - currentPatchNum = this._computeLatestPatchNum(this._allPatchSets); - } - if (patchNum == currentPatchNum) { - page.show(this.changePath(this._changeNum)); - return; - } - page.show(this.changePath(this._changeNum) + '/' + patchNum); + this._changePatchNum(parseInt(e.target.value, 10)); }, _handleReplyTap: function(e) { @@ -339,9 +289,6 @@ }; this._reload().then(function() { - this.$.messageList.topMargin = this._headerEl.offsetHeight; - this.$.fileList.topMargin = this._headerEl.offsetHeight; - // Allow the message list to render before scrolling. this.async(function() { this._maybeScrollToMessage(); @@ -389,6 +336,12 @@ _resetFileListViewState: function() { this.set('viewState.selectedFileIndex', 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('viewState.changeNum', this._changeNum); this.set('viewState.patchRange', this._patchRange); }, @@ -405,6 +358,25 @@ this.fire('title-change', {title: title}); }, + /** + * Change active patch to the provided patch num. + * @param {int} patchNum the patchn number to be viewed. + */ + _changePatchNum: function(patchNum) { + var currentPatchNum; + if (this._change.current_revision) { + currentPatchNum = + this._change.revisions[this._change.current_revision]._number; + } else { + currentPatchNum = this._computeLatestPatchNum(this._allPatchSets); + } + if (patchNum === currentPatchNum) { + page.show(this.changePath(this._changeNum)); + return; + } + page.show(this.changePath(this._changeNum) + '/' + patchNum); + }, + _computeChangePermalink: function(changeNum) { return '/' + changeNum; }, @@ -426,6 +398,14 @@ return allPatchSets[allPatchSets.length - 1]; }, + _computePatchInfoClass: function(patchNum, allPatchSets) { + if (parseInt(patchNum, 10) === + this._computeLatestPatchNum(allPatchSets)) { + return ''; + } + return 'patchInfo--oldPatchSet'; + }, + _computeAllPatchSets: function(change) { var patchNums = []; for (var rev in change.revisions) { @@ -477,11 +457,6 @@ return result; }, - _computeReplyButtonHighlighted: function(changeRecord) { - var drafts = (changeRecord && changeRecord.base) || {}; - return Object.keys(drafts).length > 0; - }, - _computeReplyButtonLabel: function(changeRecord) { var drafts = (changeRecord && changeRecord.base) || {}; var draftCount = Object.keys(drafts).reduce(function(count, file) { @@ -495,9 +470,17 @@ return label; }, + _switchToMostRecentPatchNum: function() { + this._getChangeDetail().then(function() { + var patchNum = this._allPatchSets[this._allPatchSets.length - 1]; + if (patchNum !== this._patchRange.patchNum) { + this._changePatchNum(patchNum); + } + }.bind(this)); + }, + _handleKey: function(e) { if (this.shouldSupressKeyboardShortcut(e)) { return; } - switch (e.keyCode) { case 65: // 'a' if (this._loggedIn && !e.shiftKey) { @@ -505,6 +488,16 @@ this._openReplyDialog(); } break; + case 68: // 'd' + e.preventDefault(); + this.$.downloadOverlay.open(); + break; + case 82: // 'r' + if (e.shiftKey) { + e.preventDefault(); + this._switchToMostRecentPatchNum(); + } + break; case 85: // 'u' e.preventDefault(); page.show('/'); @@ -572,6 +565,14 @@ }.bind(this)); }, + _getLatestCommitMessage: function() { + return this.$.restAPI.getChangeCommitInfo(this._changeNum, + this._computeLatestPatchNum(this._allPatchSets)).then( + function(commitInfo) { + this._latestCommitMessage = commitInfo.message; + }.bind(this)); + }, + _getCommitInfo: function() { return this.$.restAPI.getChangeCommitInfo( this._changeNum, this._patchRange.patchNum).then( @@ -606,7 +607,6 @@ var reloadPatchNumDependentResources = function() { return Promise.all([ this._getCommitInfo(), - this.$.actions.reload(), this.$.fileList.reload(), ]); }.bind(this); @@ -614,13 +614,13 @@ if (!this._change) { return Promise.resolve(); } return Promise.all([ + this._getLatestCommitMessage(), + this.$.actions.reload(), this.$.relatedChanges.reload(), this._getProjectConfig(), ]); }.bind(this); - this._resetHeaderEl(); - if (this._patchRange.patchNum) { return reloadPatchNumDependentResources().then(function() { return detailCompletes;
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 c9a687b..1c36958 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
@@ -43,44 +43,96 @@ element = fixture('basic'); }); - test('keyboard shortcuts', function() { - var showStub = sinon.stub(page, 'show'); + suite('keyboard shortcuts', function() { + test('U should navigate to /', function() { + var showStub = sinon.stub(page, 'show'); + MockInteractions.pressAndReleaseKeyOn(element, 85); // 'U' + assert(showStub.lastCall.calledWithExactly('/')); + showStub.restore(); + }); - MockInteractions.pressAndReleaseKeyOn(element, 85); // 'U' - assert(showStub.lastCall.calledWithExactly('/'), - 'Should navigate to /'); - showStub.restore(); + test('A should toggle overlay', function() { + MockInteractions.pressAndReleaseKeyOn(element, 65); // 'A' + var overlayEl = element.$.replyOverlay; + assert.isFalse(overlayEl.opened); + element._loggedIn = true; - MockInteractions.pressAndReleaseKeyOn(element, 65); // '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, 'shift'); // 'A' - assert.isFalse(overlayEl.opened); + MockInteractions.pressAndReleaseKeyOn(element, 65); // 'A' + assert.isTrue(overlayEl.opened); + overlayEl.close(); + assert.isFalse(overlayEl.opened); + }); - MockInteractions.pressAndReleaseKeyOn(element, 65); // 'A' - assert.isTrue(overlayEl.opened); - overlayEl.close(); - assert.isFalse(overlayEl.opened); + 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', + revisions: { + rev1: {_number: 1}, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + actions: {}, + }; + + sinon.stub(element.$.restAPI, '_getChangeDetail', function() { + // Mock change obj. + return Promise.resolve({ + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev1: {_number: 1}, + rev13: {_number: 13}, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + actions: {}, + }); + }); + + var showStub = sinon.stub(page, 'show', function(arg) { + assert.equal(arg, '/c/42/13'); + showStub.restore(); + element.$.restAPI._getChangeDetail.restore(); + done(); + }); + + // 'shift + R' + MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift'); + }); + + test('d should open download overlay', function() { + var stub = sinon.stub(element.$.downloadOverlay, 'open'); + MockInteractions.pressAndReleaseKeyOn(element, 68); // 'd' + assert.isTrue(stub.called); + stub.restore(); + }); }); - test('reply button is highlighted when there are drafts', function() { + test('reply button has updated count when there are drafts', function() { var replyButton = element.$$('gr-button.reply'); assert.ok(replyButton); - assert.isFalse(replyButton.hasAttribute('primary')); + assert.equal(replyButton.textContent, 'Reply'); element._diffDrafts = null; - assert.isFalse(replyButton.hasAttribute('primary')); + assert.equal(replyButton.textContent, 'Reply'); element._diffDrafts = {}; - assert.isFalse(replyButton.hasAttribute('primary')); + assert.equal(replyButton.textContent, 'Reply'); element._diffDrafts = { 'file1.txt': [{}], 'file2.txt': [{}, {}], }; - assert.isTrue(replyButton.hasAttribute('primary')); assert.equal(replyButton.textContent, 'Reply (3)'); }); @@ -120,6 +172,36 @@ assert.deepEqual(element._diffDrafts, {}); }); + test('change num change', function() { + element._changeNum = null; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 2, + }; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + labels: {}, + }; + element.viewState.changeNum = null; + element.viewState.diffMode = 'UNIFIED'; + flushAsynchronousOperations(); + assert.equal(element.viewState.diffMode, 'UNIFIED'); + + element._changeNum = '1'; + element.params = {changeNum: '1'}; + element._change.newProp = '1'; + flushAsynchronousOperations(); + assert.equal(element.viewState.diffMode, 'UNIFIED'); + assert.equal(element.viewState.changeNum, '1'); + + element._changeNum = '2'; + element.params = {changeNum: '2'}; + element._change.newProp = '2'; + flushAsynchronousOperations(); + assert.isNull(element.viewState.diffMode); + assert.equal(element.viewState.changeNum, '2'); + }); + test('patch num change', function(done) { element._changeNum = '42'; element._patchRange = { @@ -138,24 +220,27 @@ status: 'NEW', labels: {}, }; + element.viewState.diffMode = 'UNIFIED'; flushAsynchronousOperations(); - var selectEl = element.$$('.header select'); + + var selectEl = element.$$('.patchInfo-header select'); assert.ok(selectEl); - var optionEls = - Polymer.dom(element.root).querySelectorAll('.header option'); + var optionEls = Polymer.dom(element.root).querySelectorAll( + '.patchInfo-header option'); assert.equal(optionEls.length, 4); - assert.isFalse( - element.$$('.header option[value="1"]').hasAttribute('selected')); - assert.isTrue( - element.$$('.header option[value="2"]').hasAttribute('selected')); - assert.isFalse( - element.$$('.header option[value="3"]').hasAttribute('selected')); + assert.isFalse(element.$$('.patchInfo-header option[value="1"]') + .hasAttribute('selected')); + assert.isTrue(element.$$('.patchInfo-header option[value="2"]') + .hasAttribute('selected')); + assert.isFalse(element.$$('.patchInfo-header option[value="3"]') + .hasAttribute('selected')); assert.equal(optionEls[3].value, 13); var showStub = sinon.stub(page, 'show'); var numEvents = 0; selectEl.addEventListener('change', function(e) { + assert.equal(element.viewState.diffMode, 'UNIFIED'); numEvents++; if (numEvents == 1) { assert(showStub.lastCall.calledWithExactly('/c/42/1'), @@ -191,17 +276,17 @@ labels: {}, }; flushAsynchronousOperations(); - var selectEl = element.$$('.header select'); + var selectEl = element.$$('.patchInfo-header select'); assert.ok(selectEl); - var optionEls = - Polymer.dom(element.root).querySelectorAll('.header option'); + var optionEls = Polymer.dom(element.root).querySelectorAll( + '.patchInfo-header option'); assert.equal(optionEls.length, 4); - assert.isFalse( - element.$$('.header option[value="1"]').hasAttribute('selected')); - assert.isTrue( - element.$$('.header option[value="2"]').hasAttribute('selected')); - assert.isFalse( - element.$$('.header option[value="3"]').hasAttribute('selected')); + assert.isFalse(element.$$('.patchInfo-header option[value="1"]') + .hasAttribute('selected')); + assert.isTrue(element.$$('.patchInfo-header option[value="2"]') + .hasAttribute('selected')); + assert.isFalse(element.$$('.patchInfo-header option[value="3"]') + .hasAttribute('selected')); assert.equal(optionEls[3].value, 13); var showStub = sinon.stub(page, 'show'); @@ -287,33 +372,26 @@ }); test('show commit message edit button', function() { - var changeRecord = { - base: { - revisions: { - rev1: {_number: 1}, - rev2: {_number: 2}, - }, - current_revision: 'rev2', - }, + var _change = { + status: element.ChangeStatus.MERGED, }; - assert.isTrue(element._computeHideEditCommitMessage( - false, false, changeRecord, '2')); - assert.isTrue(element._computeHideEditCommitMessage( - true, true, changeRecord, '2')); - assert.isTrue(element._computeHideEditCommitMessage( - true, false, changeRecord, '1')); - assert.isFalse(element._computeHideEditCommitMessage( - true, false, changeRecord, '2')); + assert.isTrue(element._computeHideEditCommitMessage(false, false, {})); + assert.isTrue(element._computeHideEditCommitMessage(true, true, {})); + assert.isTrue(element._computeHideEditCommitMessage(false, true, {})); + assert.isFalse(element._computeHideEditCommitMessage(true, false, {})); + assert.isTrue(element._computeHideEditCommitMessage(true, false, + _change)); }); - test('topic is coalesced to null', function() { + test('topic is coalesced to null', function(done) { sinon.stub(element, '_changeChanged'); - sinon.stub(element.$.restAPI, 'getChangeDetail', function(num) { + sinon.stub(element.$.restAPI, 'getChangeDetail', function() { return Promise.resolve({id: '123456789', labels: {}}); }); element._getChangeDetail().then(function() { assert.isNull(element._change.topic); + done(); }); }); @@ -331,5 +409,14 @@ assert(openSpy.lastCall.calledWithExactly(FocusTarget.CCS), '_openReplyDialog should have been passed CCS'); }); + + test('class is applied to file list on old patch set', function() { + var allPatcheSets = [1, 2, 4]; + assert.equal(element._computePatchInfoClass('1', allPatcheSets), + 'patchInfo--oldPatchSet'); + assert.equal(element._computePatchInfoClass('2', allPatcheSets), + 'patchInfo--oldPatchSet'); + assert.equal(element._computePatchInfoClass('4', allPatcheSets), ''); + }); }); </script>
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 new file mode 100644 index 0000000..5cd65fa --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.html
@@ -0,0 +1,35 @@ +<!-- +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. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> + +<dom-module id="gr-commit-info"> + <template> + <style> + :host { + display: inline-block; + } + </style> + <template is="dom-if" if="[[_showWebLink]]"> + <a target="_blank" + href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a> + </template> + <template is="dom-if" if="[[!_showWebLink]]"> + [[_computeShortHash(commitInfo)]] + </template> + </template> + <script src="gr-commit-info.js"></script> +</dom-module>
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 new file mode 100644 index 0000000..5aa8601 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -0,0 +1,98 @@ +// 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. +(function() { + 'use strict'; + + Polymer({ + is: 'gr-commit-info', + + properties: { + change: Object, + commitInfo: Object, + serverConfig: Object, + _showWebLink: { + type: Boolean, + computed: '_computeShowWebLink(change, commitInfo, serverConfig)', + }, + _webLink: { + type: String, + computed: '_computeWebLink(change, commitInfo, serverConfig)', + }, + }, + + _isWebLink: function(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) { + if (serverConfig.gitweb && serverConfig.gitweb.url && + serverConfig.gitweb.type && serverConfig.gitweb.type.revision) { + return true; + } + + if (!commitInfo.web_links) { + return false; + } + + for (var i = 0; i < commitInfo.web_links.length; i++) { + if (this._isWebLink(commitInfo.web_links[i])) { + return true; + } + } + + return false; + }, + + _computeWebLink: function(change, commitInfo, serverConfig) { + if (!this._computeShowWebLink(change, commitInfo, serverConfig)) { + return; + } + + if (serverConfig.gitweb && serverConfig.gitweb.url && + serverConfig.gitweb.type && serverConfig.gitweb.type.revision) { + return serverConfig.gitweb.url + + serverConfig.gitweb.type.revision + .replace('${project}', change.project) + .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; + break; + } + } + + if (!webLink) { + return; + } + + if (!/^https?\:\/\//.test(webLink)) { + webLink = '../../' + webLink; + } + + return webLink; + }, + + _computeShortHash: function(commitInfo) { + if (!commitInfo || !commitInfo.commit) { + return; + } + return commitInfo.commit.slice(0, 7); + }, + }); +})();
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 new file mode 100644 index 0000000..36b1628 --- /dev/null +++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
@@ -0,0 +1,145 @@ +<!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-commit-info</title> + +<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="gr-commit-info.html"> + +<test-fixture id="basic"> + <template> + <gr-commit-info></gr-commit-info> + </template> +</test-fixture> + +<script> + suite('gr-commit-info tests', function() { + var element; + + setup(function() { + element = fixture('basic'); + }); + + test('no web link when unavailable', function() { + element.commitInfo = {}; + element.serverConfig = {}; + element.change = {labels: []}; + + assert.isNotOk(element._computeShowWebLink(element.change, + element.commitInfo, element.serverConfig)); + }); + + test('use web link when available', function() { + element.commitInfo = {web_links: [{name: 'gitweb', url: 'link-url'}]}; + element.serverConfig = {}; + + assert.isOk(element._computeShowWebLink(element.change, + element.commitInfo, element.serverConfig)); + assert.equal(element._computeWebLink(element.change, element.commitInfo, + element.serverConfig), '../../link-url'); + }); + + test('does not relativize web links that begin with scheme', function() { + element.commitInfo = { + web_links: [{name: 'gitweb', url: 'https://link-url'}] + }; + element.serverConfig = {}; + + assert.isOk(element._computeShowWebLink(element.change, + element.commitInfo, element.serverConfig)); + assert.equal(element._computeWebLink(element.change, element.commitInfo, + element.serverConfig), 'https://link-url'); + }); + + test('use gitweb when available', function() { + element.commitInfo = {commit: 'commit-sha'}; + element.serverConfig = {gitweb: { + url: 'url-base/', + type: {revision: 'xx ${project} xx ${commit} xx'}, + }}; + element.change = { + project: 'project-name', + labels: [], + current_revision: element.commitInfo.commit + }; + + assert.isOk(element._computeShowWebLink(element.change, + element.commitInfo, element.serverConfig)); + + assert.equal(element._computeWebLink(element.change, element.commitInfo, + element.serverConfig), 'url-base/xx project-name xx commit-sha xx'); + }); + + test('prefer gitweb when both are available', function() { + element.commitInfo = { + commit: 'commit-sha', + web_links: [{url: 'link-url'}] + }; + element.serverConfig = {gitweb: { + url: 'url-base/', + type: {revision: 'xx ${project} xx ${commit} xx'}, + }}; + element.change = { + project: 'project-name', + labels: [], + current_revision: element.commitInfo.commit + }; + + assert.isOk(element._computeShowWebLink(element.change, + element.commitInfo, element.serverConfig)); + + var 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() { + element.commitInfo = { + commit: 'commit-sha', + web_links: [ + { + name: 'ignore', + url: 'ignore', + }, + { + name: 'gitiles', + url: 'https://link-url', + } + ], + }; + element.serverConfig = {}; + + assert.isOk(element._computeShowWebLink(element.change, + element.commitInfo, element.serverConfig)); + assert.equal(element._computeWebLink(element.change, element.commitInfo, + element.serverConfig), 'https://link-url'); + + // Remove gitiles link. + element.commitInfo.web_links.splice(1, 1); + assert.isNotOk(element._computeShowWebLink(element.change, + element.commitInfo, element.serverConfig)); + assert.isNotOk(element._computeWebLink(element.change, element.commitInfo, + element.serverConfig)); + }); + }); +</script>
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 97342d1..f27e4e2 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
@@ -32,16 +32,6 @@ properties: { branch: String, message: String, - commitInfo: { - type: Object, - readOnly: true, - observer: '_commitInfoChanged', - }, - }, - - _commitInfoChanged: function(commitInfo) { - // Pre-populate cherry-pick message for editing from commit info. - this.message = commitInfo.message; }, _handleConfirmTap: function(e) {
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 b4baa26..2eb6646 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
@@ -30,26 +30,22 @@ */ properties: { - branch: String, message: String, - commitInfo: Object, }, - populateRevertMessage: function() { + populateRevertMessage: function(message) { // Figure out what the revert title should be. - var originalTitle = this.commitInfo.message.split('\n')[0]; - var revertTitle = 'Revert of ' + originalTitle; - if (originalTitle.startsWith('Revert of ')) { - revertTitle = 'Reland of ' + - originalTitle.substring('Revert of '.length); - } else if (originalTitle.startsWith('Reland of ')) { - revertTitle = 'Revert of ' + - originalTitle.substring('Reland of '.length); - } + var originalTitle = message.split('\n')[0]; + var revertTitle = 'Revert "' + originalTitle + '"'; + // Figure out what the revert commit message should be. + var commitRegex = /\n{1,2}\nChange-Id: (\w+)\n/gm; + var match = commitRegex.exec(message); + var revertCommitText = 'This reverts commit ' + match[1] + '.'; // Add '> ' in front of the original commit text. - var originalCommitText = this.commitInfo.message.replace(/^/gm, '> '); + var originalCommitText = message.replace(/^/gm, '> '); this.message = revertTitle + '\n\n' + + revertCommitText + '\n\n' + 'Reason for revert: <INSERT REASONING HERE>\n\n' + 'Original issue\'s description:\n' + originalCommitText; },
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 1d53eef..521aeef 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
@@ -40,25 +40,39 @@ test('single line', function() { assert.isNotOk(element.message); - element.commitInfo = {message: 'one line commit'}; - assert.isNotOk(element.message); - element.populateRevertMessage(); - var expected = 'Revert of one line commit\n\n' + + element.populateRevertMessage('one line commit\n\nChange-Id: abcdefg\n'); + var expected = 'Revert "one line commit"\n\n' + + 'This reverts commit abcdefg.\n\n' + 'Reason for revert: <INSERT REASONING HERE>\n\n' + 'Original issue\'s description:\n' + - '> one line commit'; + '> one line commit\n> \n' + + '> Change-Id: abcdefg\n> '; assert.equal(element.message, expected); }); test('multi line', function() { assert.isNotOk(element.message); - element.commitInfo = {message: 'many lines\ncommit\n\nmessage\n'}; - assert.isNotOk(element.message); - element.populateRevertMessage(); - var expected = 'Revert of many lines\n\n' + + element.populateRevertMessage( + 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n'); + var expected = 'Revert "many lines"\n\n' + + 'This reverts commit abcdefg.\n\n' + 'Reason for revert: <INSERT REASONING HERE>\n\n' + 'Original issue\'s description:\n' + - '> many lines\n> commit\n> \n> message\n> '; + '> many lines\n> commit\n> \n> message\n> \n' + + '> Change-Id: abcdefg\n> '; + assert.equal(element.message, expected); + }); + + test('revert a revert', function () { + assert.isNotOk(element.message); + element.populateRevertMessage( + 'Revert "one line commit"\n\nChange-Id: abcdefg\n'); + var expected = 'Revert "Revert "one line commit""\n\n' + + 'This reverts commit abcdefg.\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n\n' + + 'Original issue\'s description:\n' + + '> Revert "one line commit"\n> \n' + + '> Change-Id: abcdefg\n> '; assert.equal(element.message, expected); }); });
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 ef5ceed..a47c27c 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
@@ -100,6 +100,20 @@ color: #C62828; font-weight: bold; } + .show-hide { + margin-left: .4em; + } + input.show-hide { + display: none; + } + label.show-hide { + color: #00f; + cursor: pointer; + display: block; + font-size: .8em; + min-width: 2em; + margin-top: .1em; + } gr-diff { box-shadow: 0 1px 3px rgba(0, 0, 0, .3); display: block; @@ -146,7 +160,8 @@ <div class="row" selected$="[[_computeFileSelected(index, selectedIndex)]]"> <div class="reviewed" hidden$="[[!_loggedIn]]" hidden> <input type="checkbox" checked$="[[_computeReviewed(file, _reviewed)]]" - data-path$="[[file.__path]]" on-change="_handleReviewedChange"> + data-path$="[[file.__path]]" on-change="_handleReviewedChange" + class="reviewed"> </div> <div class$="[[_computeClass('status', file.__path)]]"> [[_computeFileStatus(file.status)]] @@ -168,8 +183,17 @@ <span class="added">+[[file.lines_inserted]]</span> <span class="removed">-[[file.lines_deleted]]</span> </div> + <div class="show-hide"> + <label class="show-hide"> + <input type="checkbox" class="show-hide" + checked$="[[!file.__expanded]]" data-path$="[[file.__path]]" + on-change="_handleHiddenChange"> + [[_computeShowHideText(file.__expanded)]] + </label> + </div> </div> - <gr-diff hidden + <gr-diff + hidden$="[[_computeHiddenState(file.__expanded)]]" project="[[change.project]]" commit="[[change.current_revision]]" change-num="[[changeNum]]" @@ -181,9 +205,7 @@ </template> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-storage id="storage"></gr-storage> - <gr-diff-cursor - id="cursor" - fold-offset-top="[[topMargin]]"></gr-diff-cursor> + <gr-diff-cursor id="cursor"></gr-diff-cursor> </template> <script src="gr-file-list.js"></script> </dom-module>
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 225d8b3..eece232 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
@@ -27,7 +27,6 @@ drafts: Object, revisions: Object, projectConfig: Object, - topMargin: Number, selectedIndex: { type: Number, notify: true, @@ -121,6 +120,11 @@ return parseInt(patchNum, 10) === parseInt(basePatchNum, 10); }, + _handleHiddenChange: function(e) { + var model = e.model; + model.set('file.__expanded', !model.file.__expanded); + }, + _handlePatchChange: function(e) { this.set('patchRange.basePatchNum', Polymer.dom(e).rootTarget.value); page.show('/c/' + encodeURIComponent(this.changeNum) + '/' + @@ -136,10 +140,9 @@ _expandAllDiffs: function(e) { this._showInlineDiffs = true; - this._forEachDiff(function(diff) { - diff.hidden = false; - diff.reload(); - }); + for (var index in this._files) { + this.set(['_files', index, '__expanded'], true); + } if (e && e.target) { e.target.blur(); } @@ -147,9 +150,9 @@ _collapseAllDiffs: function(e) { this._showInlineDiffs = false; - this._forEachDiff(function(diff) { - diff.hidden = true; - }); + for (var index in this._files) { + this.set(['_files', index, '__expanded'], false); + } this.$.cursor.handleDiffUpdate(); if (e && e.target) { e.target.blur(); @@ -212,12 +215,17 @@ _getFiles: function() { return this.$.restAPI.getChangeFilesAsSpeciallySortedArray( - this.changeNum, this.patchRange); + this.changeNum, this.patchRange).then(function(files) { + // Append UI-specific properties. + return files.map(function(file) { + file.__expanded = false; + return file; + }); + }); }, _handleKey: function(e) { if (this.shouldSupressKeyboardShortcut(e)) { return; } - switch (e.keyCode) { case 37: // left if (e.shiftKey && this._showInlineDiffs) { @@ -232,9 +240,14 @@ } break; case 73: // 'i' - if (!e.shiftKey) { return; } - e.preventDefault(); - this._toggleInlineDiffs(); + if (e.shiftKey) { + e.preventDefault(); + this._toggleInlineDiffs(); + } else if (this.selectedIndex !== undefined) { + e.preventDefault(); + var expanded = this._files[this.selectedIndex].__expanded; + this.set(['_files', this.selectedIndex, '__expanded'], !expanded); + } break; case 40: // down case 74: // 'j' @@ -352,7 +365,7 @@ } // Don't scroll if it's already in view. - if (top > window.pageYOffset + this.topMargin && + if (top > window.pageYOffset && top < window.pageYOffset + window.innerHeight - el.clientHeight) { return; } @@ -369,12 +382,13 @@ }, _computeDiffURL: function(changeNum, patchRange, path) { + // @see Issue 4255 regarding double-encoding. return '/c/' + encodeURIComponent(changeNum) + '/' + encodeURIComponent(this._patchRangeStr(patchRange)) + '/' + - path; + encodeURIComponent(encodeURIComponent(path)); }, _patchRangeStr: function(patchRange) { @@ -395,6 +409,14 @@ return classes.join(' '); }, + _computeShowHideText: function(expanded) { + return expanded ? 'â–¼' : 'â—€'; + }, + + _computeHiddenState: function(expanded) { + return !expanded; + }, + _filesChanged: function() { this.async(function() { var diffElements = Polymer.dom(this.root).querySelectorAll('gr-diff');
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 f61566a..3fe785e 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
@@ -60,16 +60,19 @@ lines_inserted: 9, lines_deleted: 0, __path: '/COMMIT_MSG', + __expanded: false, }); assert.deepEqual(files[1], { lines_inserted: 0, lines_deleted: 0, __path: 'about.txt', + __expanded: false, }); assert.deepEqual(files[2], { lines_inserted: 0, lines_deleted: 123, __path: 'tags.html', + __expanded: false, }); getChangeFilesStub.restore(); @@ -77,62 +80,77 @@ }); }); - test('toggle left diff via shortcut', function() { - var toggleLeftDiffStub = sinon.stub(); - sinon.stub(element, 'diffs', {get: function() { - return [{toggleLeftDiff: toggleLeftDiffStub}]; - }}); - MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift'); // 'A' - assert.isTrue(toggleLeftDiffStub.calledOnce); - }); + suite('keyboard shortcuts', function() { + setup(function() { + element._files = [ + {__path: '/COMMIT_MSG', __expanded: false}, + {__path: 'file_added_in_rev2.txt', __expanded: false}, + {__path: 'myfile.txt', __expanded: false}, + ]; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + element.selectedIndex = 0; + }); - test('keyboard shortcuts', function() { - var toggleInlineDiffsStub = sinon.stub(element, '_toggleInlineDiffs'); - MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift'); // 'I' - assert.isTrue(toggleInlineDiffsStub.calledOnce); - toggleInlineDiffsStub.restore(); + test('toggle left diff via shortcut', function() { + var toggleLeftDiffStub = sinon.stub(); + sinon.stub(element, 'diffs', {get: function() { + return [{toggleLeftDiff: toggleLeftDiffStub}]; + }}); + MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift'); // 'A' + assert.isTrue(toggleLeftDiffStub.calledOnce); + }); - element._files = [ - {__path: '/COMMIT_MSG'}, - {__path: 'file_added_in_rev2.txt'}, - {__path: 'myfile.txt'}, - ]; - element.changeNum = '42'; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '2', - }; - element.selectedIndex = 0; + test('keyboard shortcuts', function() { + var toggleInlineDiffsStub = sinon.stub(element, '_toggleInlineDiffs'); + MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift'); // 'I' + assert.isTrue(toggleInlineDiffsStub.calledOnce); + toggleInlineDiffsStub.restore(); - flushAsynchronousOperations(); - var elementItems = Polymer.dom(element.root).querySelectorAll( - '.row:not(.header)'); - assert.equal(elementItems.length, 3); - assert.isTrue(elementItems[0].hasAttribute('selected')); - assert.isFalse(elementItems[1].hasAttribute('selected')); - assert.isFalse(elementItems[2].hasAttribute('selected')); - MockInteractions.pressAndReleaseKeyOn(element, 74); // 'J' - assert.equal(element.selectedIndex, 1); - MockInteractions.pressAndReleaseKeyOn(element, 74); // 'J' + flushAsynchronousOperations(); + var elementItems = Polymer.dom(element.root).querySelectorAll( + '.row:not(.header)'); + assert.equal(elementItems.length, 3); + assert.isTrue(elementItems[0].hasAttribute('selected')); + assert.isFalse(elementItems[1].hasAttribute('selected')); + assert.isFalse(elementItems[2].hasAttribute('selected')); + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'J' + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'J' - var showStub = sinon.stub(page, 'show'); - assert.equal(element.selectedIndex, 2); - MockInteractions.pressAndReleaseKeyOn(element, 13); // 'ENTER' - assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'), - 'Should navigate to /c/42/2/myfile.txt'); + var showStub = sinon.stub(page, 'show'); + assert.equal(element.selectedIndex, 2); + MockInteractions.pressAndReleaseKeyOn(element, 13); // 'ENTER' + assert(showStub.lastCall.calledWith('/c/42/2/myfile.txt'), + 'Should navigate to /c/42/2/myfile.txt'); - MockInteractions.pressAndReleaseKeyOn(element, 75); // 'K' - assert.equal(element.selectedIndex, 1); - MockInteractions.pressAndReleaseKeyOn(element, 79); // 'O' - assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'), - 'Should navigate to /c/42/2/file_added_in_rev2.txt'); + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'K' + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 79); // 'O' + assert(showStub.lastCall.calledWith('/c/42/2/file_added_in_rev2.txt'), + 'Should navigate to /c/42/2/file_added_in_rev2.txt'); - MockInteractions.pressAndReleaseKeyOn(element, 75); // 'K' - MockInteractions.pressAndReleaseKeyOn(element, 75); // 'K' - MockInteractions.pressAndReleaseKeyOn(element, 75); // 'K' - assert.equal(element.selectedIndex, 0); + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'K' + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'K' + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'K' + assert.equal(element.selectedIndex, 0); - showStub.restore(); + showStub.restore(); + }); + + test('i key shows/hides selected inline diff', function() { + element.selectedIndex = 0; + MockInteractions.pressAndReleaseKeyOn(element, 73); // 'I' + assert.isTrue(element._files[0].__expanded); + MockInteractions.pressAndReleaseKeyOn(element, 73); // 'I' + assert.isFalse(element._files[0].__expanded); + element.selectedIndex = 1; + MockInteractions.pressAndReleaseKeyOn(element, 73); // 'I' + assert.isTrue(element._files[1].__expanded); + }); }); test('comment filtering', function() { @@ -187,9 +205,9 @@ test('file review status', function() { element._files = [ - {__path: '/COMMIT_MSG'}, - {__path: 'file_added_in_rev2.txt'}, - {__path: 'myfile.txt'}, + {__path: '/COMMIT_MSG', __expanded: false}, + {__path: 'file_added_in_rev2.txt', __expanded: false}, + {__path: 'myfile.txt', __expanded: false}, ]; element._reviewed = ['/COMMIT_MSG', 'myfile.txt']; element.changeNum = '42'; @@ -202,9 +220,12 @@ flushAsynchronousOperations(); var fileRows = Polymer.dom(element.root).querySelectorAll('.row:not(.header)'); - var commitMsg = fileRows[0].querySelector('input[type="checkbox"]'); - var fileAdded = fileRows[1].querySelector('input[type="checkbox"]'); - var myFile = fileRows[2].querySelector('input[type="checkbox"]'); + var commitMsg = fileRows[0].querySelector( + 'input.reviewed[type="checkbox"]'); + var fileAdded = fileRows[1].querySelector( + 'input.reviewed[type="checkbox"]'); + var myFile = fileRows[2].querySelector( + 'input.reviewed[type="checkbox"]'); assert.isTrue(commitMsg.checked); assert.isFalse(fileAdded.checked); @@ -267,5 +288,42 @@ element.fire('change', {}, {node: selectEl}); }); }); + + test('checkbox shows/hides diff inline', function() { + element._files = [ + {__path: 'myfile.txt', __expanded: false}, + ]; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + element.selectedIndex = 0; + flushAsynchronousOperations(); + var fileRows = + Polymer.dom(element.root).querySelectorAll('.row:not(.header)'); + // Prevent diff from making API call. + var diffStub = sinon.stub(element.diffs[0], 'reload'); + var showHideCheck = fileRows[0].querySelector( + 'input.show-hide[type="checkbox"]'); + assert.isTrue(showHideCheck.checked); + MockInteractions.tap(showHideCheck); + assert.isFalse(element.diffs[0].hidden); + diffStub.restore(); + }); + + test('file name should be double-escaped', function() { + element._files = [ + {__path: 'my+file.txt'}, + ]; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + flushAsynchronousOperations(); + assert.equal( + element.$$('a').getAttribute('href'), '/c/42/2/my%252Bfile.txt'); + }); }); </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 66254d0..958e00f 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message.html +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.html
@@ -50,12 +50,15 @@ } .showAvatar.collapsed .contentContainer { margin-left: calc(var(--default-horizontal-margin) + 1.75em); - padding: .75em 2em .75em 0; } .hideAvatar.collapsed .contentContainer, .hideAvatar.expanded .contentContainer { margin-left: 0; - padding: .75em 2em .75em 0; + } + .showAvatar.collapsed .contentContainer, + .hideAvatar.collapsed .contentContainer, + .hideAvatar.expanded .contentContainer { + padding: .75em 5em .75em 0; } .collapsed gr-avatar { top: .5em;
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 c92ad07..acb95c1 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -56,15 +56,22 @@ }, showReplyButton: { type: Boolean, - computed: '_computeShowReplyButton(message)', + computed: '_computeShowReplyButton(message, _loggedIn)', }, projectConfig: Object, + _loggedIn: { + type: Boolean, + value: false, + }, }, ready: function() { this.$.restAPI.getConfig().then(function(config) { this.config = config; }.bind(this)); + this.$.restAPI.getLoggedIn().then(function(loggedIn) { + this._loggedIn = loggedIn; + }.bind(this)); }, _computeAuthor: function(message) { @@ -75,8 +82,8 @@ return !!(author && config && config.plugin && config.plugin.has_avatars); }, - _computeShowReplyButton: function(message) { - return !!message.message; + _computeShowReplyButton: function(message, loggedIn) { + return !!message.message && loggedIn; }, _commentsChanged: function(value) {
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 c90f58a..ff8610b 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
@@ -85,5 +85,12 @@ assert.equal(0, content.textContent.trim().indexOf(updatedBy.name)); }); + test('reply button hidden unless logged in', function() { + var message = { + 'message': 'Uploaded patch set 1.', + }; + assert.isFalse(element._computeShowReplyButton(message, false)); + assert.isTrue(element._computeShowReplyButton(message, true)); + }); }); </script>
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 e7a0573..875bf2b 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
@@ -29,7 +29,6 @@ }, comments: Object, projectConfig: Object, - topMargin: Number, showReplyButtons: { type: Boolean, value: false, @@ -52,7 +51,7 @@ offsetParent = offsetParent.offsetParent) { top += offsetParent.offsetTop; } - window.scrollTo(0, top - this.topMargin); + window.scrollTo(0, top); this._highlightEl(el); },
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 cec1e90..1432d8c 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
@@ -97,9 +97,6 @@ border: none; width: 100%; } - .labelsNotShown { - color: #666; - } .labelContainer:not(:first-of-type) { margin-top: .5em; } @@ -211,28 +208,20 @@ </iron-autogrow-textarea> </section> <section class="labelsContainer"> - <template is="dom-if" if="[[_computeShowLabels(patchNum, revisions)]]"> - <template is="dom-repeat" - items="[[_computeLabelArray(permittedLabels)]]" as="label"> - <div class="labelContainer"> - <span class="labelName">[[label]]</span> - <iron-selector data-label$="[[label]]" - selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]"> - <template is="dom-repeat" - items="[[_computePermittedLabelValues(permittedLabels, label)]]" - as="value"> - <gr-button has-tooltip data-value$="[[value]]" - title$="[[_computeLabelValueTitle(labels, label, value)]]">[[value]]</gr-button> - </template> - </iron-selector> - </div> - </template> - </template> - <template is="dom-if" if="[[!_computeShowLabels(patchNum, revisions)]]"> - <span class="labelsNotShown"> - Labels are not shown because this is not the most recent patch set. - <a href$="/c/[[change._number]]">Go to the latest patch set.</a> - </span> + <template is="dom-repeat" + items="[[_computeLabelArray(permittedLabels)]]" as="label"> + <div class="labelContainer"> + <span class="labelName">[[label]]</span> + <iron-selector data-label$="[[label]]" + selected="[[_computeIndexOfLabelValue(labels, permittedLabels, label, _account)]]"> + <template is="dom-repeat" + items="[[_computePermittedLabelValues(permittedLabels, label)]]" + as="value"> + <gr-button has-tooltip data-value$="[[value]]" + title$="[[_computeLabelValueTitle(labels, label, value)]]">[[value]]</gr-button> + </template> + </iron-selector> + </div> </template> </section> <section class="draftsContainer" hidden$="[[_computeHideDraftList(diffDrafts)]]">
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 d2b279d..b54fc85 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
@@ -48,7 +48,6 @@ properties: { change: Object, patchNum: String, - revisions: Object, disabled: { type: Boolean, value: false, @@ -153,8 +152,8 @@ var selectorEl = this.$$('iron-selector[data-label="' + label + '"]'); - // The selector may not be present if it’s not at the latest patch set. - if (!selectorEl) { continue; } + // The user may have not voted on this label. + if (!selectorEl.selectedItem) { continue; } var selectedVal = selectorEl.selectedItem.getAttribute('data-value'); selectedVal = parseInt(selectedVal, 10); @@ -259,16 +258,6 @@ }.bind(this)); }, - _computeShowLabels: function(patchNum, revisions) { - var num = parseInt(patchNum, 10); - for (var rev in revisions) { - if (revisions[rev]._number > num) { - return false; - } - } - return true; - }, - _computeHideDraftList: function(drafts) { return Object.keys(drafts || {}).length == 0; }, @@ -295,7 +284,7 @@ labels, permittedLabels, labelName, account) { var t = labels[labelName]; if (!t) { return null; } - var labelValue = t.default_value; + var labelValue = null; // Is there an existing vote for the current user? If so, use that. var votes = labels[labelName];
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 8fb4e45..639aeef 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
@@ -107,18 +107,7 @@ MockInteractions.tap(element.$$('.cancel')); }); - test('show/hide labels', function() { - var revisions = { - rev1: {_number: 1}, - rev2: {_number: 2}, - }; - assert.isFalse(element._computeShowLabels('1', revisions)); - assert.isTrue(element._computeShowLabels('2', revisions)); - }); - test('label picker', function(done) { - var showLabelsStub = sinon.stub(element, '_computeShowLabels', - function() { return true; }); element.revisions = {}; element.patchNum = ''; @@ -156,7 +145,6 @@ 'Element should be enabled when done sending reply.'); assert.equal(element.draft.length, 0); saveReviewStub.restore(); - showLabelsStub.restore(); done(); }); @@ -312,7 +300,10 @@ assert.equal(body, 'first error, second error'); }); }); - element.send().then(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); }); }); test('ccs are displayed if NoteDb is enabled', function() {
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 7291199..6e88267 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
@@ -155,7 +155,11 @@ </tr> <tr> <td><span class="key">a</span></td> - <td>Review and publish comments</td> + <td>Open reply dialog to publish comments and add reviewers</td> + </tr> + <tr> + <td><span class="key">d</span></td> + <td>Open download overlay</td> </tr> <tr> <td></td><td class="header">File list</td> @@ -173,6 +177,10 @@ <td>Show selected file</td> </tr> <tr> + <td><span class="key">i</span></td> + <td>Show/hide selected inline diff</td> + </tr> + <tr> <td></td><td class="header">Diffs</td> </tr> <tr> @@ -297,7 +305,7 @@ </tr> <tr> <td><span class="key">a</span></td> - <td>Review and publish comments</td> + <td>Open reply dialog to publish comments and add reviewers</td> </tr> <tr> <td><span class="key">,</span></td>
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 930c8cf..916726fd 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
@@ -45,34 +45,38 @@ .links { margin-left: 1em; } - .links ul { + .links .menuContainer { display: none; } .links > li { cursor: default; display: inline-block; margin-left: 1em; - padding: .4em 0; + padding: .5em 0; position: relative; } - .links li:hover ul { + .links li:hover .menuContainer, + .links li:active .menuContainer { background-color: #fff; + border-radius: 3px; box-shadow: 0 1px 1px rgba(0, 0, 0, .3); display: block; - left: -.75em; + left: -.5em; + padding: .5em 0; position: absolute; - top: 2em; + top: 2.45em; z-index: 1000; } .links li ul li a:link, .links li ul li a:visited { color: #00e; display: block; - padding: .5em .75em; + padding: .3em 1em; text-decoration: none; white-space: nowrap; } - .links li ul li:hover a { + .links li ul li:hover a, + .links li ul li:active a { background-color: var(--selection-background-color); } .linksTitle { @@ -87,7 +91,8 @@ height: 0; position: absolute; right: 0; - top: calc(50% - .1em); + top: calc(50% - .05em); + transition: border-top-color 200ms; width: 0; } .links li:hover .downArrow { @@ -137,11 +142,13 @@ <span class="linksTitle"> [[linkGroup.title]] <i class="downArrow"></i> </span> - <ul> - <template is="dom-repeat" items="[[linkGroup.links]]" as="link"> - <li><a href$="[[link.url]]">[[link.name]]</a></li> - </template> - </ul> + <div class="menuContainer"> + <ul> + <template is="dom-repeat" items="[[linkGroup.links]]" as="link"> + <li><a href$="[[link.url]]">[[link.name]]</a></li> + </template> + </ul> + </div> </li> </template> </ul>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html new file mode 100644 index 0000000..7653655 --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
@@ -0,0 +1,21 @@ +<!-- +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. +--> + +<link rel="import" href="../../../bower_components/polymer/polymer.html"> + +<dom-module id="gr-reporting"> + <script src="gr-reporting.js"></script> +</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js new file mode 100644 index 0000000..5236a1c --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -0,0 +1,129 @@ +// 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. +(function() { + 'use strict'; + + // Latency reporting constants. + var TIMING = { + TYPE: 'timing-report', + CATEGORY: 'UI Latency', + // Reported events - alphabetize below. + APP_STARTED: 'App Started', + PAGE_LOADED: 'Page Loaded', + }; + + // Navigation reporting constants. + var NAVIGATION = { + TYPE: 'nav-report', + CATEGORY: 'Location Changed', + PAGE: 'Page', + }; + + var CHANGE_VIEW_REGEX = /^\/c\/\d+\/?\d*$/; + var DIFF_VIEW_REGEX = /^\/c\/\d+\/\d+\/.+$/; + + Polymer({ + is: 'gr-reporting', + + properties: { + _baselines: { + type: Array, + value: function() { return {}; }, + } + }, + + get performanceTiming() { + return window.performance.timing; + }, + + now: function() { + return Math.round(10 * window.performance.now()) / 10; + }, + + reporter: function(type, category, eventName, eventValue) { + eventValue = eventValue; + var detail = { + type: type, + category: category, + name: eventName, + value: eventValue, + }; + document.dispatchEvent(new CustomEvent(type, {detail: detail})); + console.log(eventName + ': ' + eventValue); + }, + + /** + * User-perceived app start time, should be reported when the app is ready. + */ + appStarted: function() { + var startTime = + new Date().getTime() - this.performanceTiming.navigationStart; + this.reporter( + TIMING.TYPE, TIMING.CATEGORY, TIMING.APP_STARTED, startTime); + }, + + /** + * Page load time, should be reported at any time after navigation. + */ + pageLoaded: function() { + 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 - + this.performanceTiming.navigationStart; + this.reporter( + TIMING.TYPE, TIMING.CATEGORY, TIMING.PAGE_LOADED, loadTime); + } + }, + + locationChanged: function() { + var page = ''; + var pathname = this._getPathname(); + if (pathname.startsWith('/q/')) { + page = '/q/'; + } else if (pathname.match(CHANGE_VIEW_REGEX)) { // change view + page = '/c/'; + } else if (pathname.match(DIFF_VIEW_REGEX)) { // diff view + page = '/c//COMMIT_MSG'; + } else { + // Ignore other page changes. + return; + } + this.reporter( + NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page); + }, + + _getPathname: function() { + return window.location.pathname; + }, + + /** + * Reset named timer. + */ + time: function(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; + this.reporter(TIMING.TYPE, TIMING.CATEGORY, name, time); + delete this._baselines[name]; + }, + }); +})();
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 new file mode 100644 index 0000000..b9d07fc --- /dev/null +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -0,0 +1,132 @@ +<!DOCTYPE html> +<!-- +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. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-reporting</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="gr-reporting.html"> + +<test-fixture id="basic"> + <template> + <gr-reporting></gr-reporting> + </template> +</test-fixture> + +<script> + suite('gr-reporting tests', function() { + var element; + var sandbox; + var clock; + var fakePerformance; + + var NOW_TIME = 100; + + setup(function() { + sandbox = sinon.sandbox.create(); + clock = sinon.useFakeTimers(NOW_TIME); + element = fixture('basic'); + fakePerformance = { + navigationStart: 1, + loadEventEnd: 2, + }; + sinon.stub(element, 'performanceTiming', + {get: function() {return fakePerformance;}}); + sandbox.stub(element, 'reporter'); + }); + teardown(function() { + sandbox.restore(); + clock.restore(); + }); + + test('appStarted', function() { + element.appStarted(); + assert.isTrue( + element.reporter.calledWithExactly( + 'timing-report', 'UI Latency', 'App Started', + NOW_TIME - fakePerformance.navigationStart + )); + }); + + test('pageLoaded', function() { + element.pageLoaded(); + assert.isTrue( + element.reporter.calledWithExactly( + 'timing-report', 'UI Latency', 'Page Loaded', + fakePerformance.loadEventEnd - fakePerformance.navigationStart) + ); + }); + + test('time and timeEnd', function() { + var nowStub = sinon.stub(element, 'now').returns(0); + element.time('foo'); + nowStub.returns(1); + element.time('bar'); + nowStub.returns(2); + element.timeEnd('bar'); + nowStub.returns(3.123); + element.timeEnd('foo'); + assert.isTrue(element.reporter.calledWithExactly( + 'timing-report', 'UI Latency', 'foo', 3.123 + )); + assert.isTrue(element.reporter.calledWithExactly( + 'timing-report', 'UI Latency', 'bar', 1 + )); + }); + + suite('location changed', function() { + var pathnameStub; + setup(function() { + pathnameStub = sinon.stub(element, '_getPathname'); + }); + + teardown(function() { + pathnameStub.restore(); + }); + + test('search', function() { + pathnameStub.returns('/q/foo'); + element.locationChanged(); + assert.isTrue(element.reporter.calledWithExactly( + 'nav-report', 'Location Changed', 'Page', '/q/')); + }); + + test('change view', function() { + pathnameStub.returns('/c/42/'); + element.locationChanged(); + assert.isTrue(element.reporter.calledWithExactly( + 'nav-report', 'Location Changed', 'Page', '/c/')); + }); + + test('change view', function() { + pathnameStub.returns('/c/41/2'); + element.locationChanged(); + assert.isTrue(element.reporter.calledWithExactly( + 'nav-report', 'Location Changed', 'Page', '/c/')); + }); + + test('diff view', function() { + pathnameStub.returns('/c/41/2/file.txt'); + element.locationChanged(); + assert.isTrue(element.reporter.calledWithExactly( + 'nav-report', 'Location Changed', 'Page', '/c//COMMIT_MSG')); + }); + }); + }); +</script>
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 2971ed2..4ad2a37 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router.html +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
@@ -15,6 +15,7 @@ --> <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-reporting/gr-reporting.html"> <script src="../../../bower_components/page/page.js"></script> <script src="gr-router.js"></script>
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 d11d438..d9bf8ac 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -17,9 +17,18 @@ // 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'); - var restAPI = document.createElement('gr-rest-api-interface'); + if (!app) { + console.log('No gr-app found (running tests)'); + return; + } window.addEventListener('WebComponentsReady', function() { + var restAPI = document.createElement('gr-rest-api-interface'); + var reporting = document.createElement('gr-reporting'); + + reporting.timeEnd('WebComponentsReady'); + reporting.pageLoaded(); + // Middleware page(function(ctx, next) { document.body.scrollTop = 0; @@ -28,6 +37,7 @@ // is processed. app.async(function() { app.fire('location-change'); + reporting.locationChanged(); }, 1); next(); }); @@ -124,12 +134,13 @@ }; // Don't allow diffing the same patch number against itself. if (params.basePatchNum === params.patchNum) { + // @see Issue 4255 regarding double-encoding. page.redirect('/c/' + encodeURIComponent(params.changeNum) + '/' + encodeURIComponent(params.patchNum) + '/' + - encodeURIComponent(params.path)); + encodeURIComponent(encodeURIComponent(params.path))); return; } normalizePatchRangeParams(params);
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 fecb376..2b05d99 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
@@ -18,6 +18,8 @@ <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html"> <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"> + <dom-module id="gr-search-bar"> <template> @@ -53,6 +55,7 @@ multi borderless></gr-autocomplete> <gr-button id="searchButton">Search</gr-button> + <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> </form> </template> <script src="gr-search-bar.js"></script>
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 8e52f8f..d8121b7 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
@@ -78,6 +78,10 @@ 'tr', ]; + var MAX_AUTOCOMPLETE_RESULTS = 10; + + var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g; + Polymer({ is: 'gr-search-bar', @@ -117,41 +121,169 @@ this._preventDefaultAndNavigateToInputVal(e); }, + /** + * This function is called in a few different cases: + * - e.target is the search button + * - e.target is the gr-autocomplete widget (#searchInput) + * - e.target is the input element wrapped within #searchInput + * + * @param {!Event} e + */ _preventDefaultAndNavigateToInputVal: function(e) { e.preventDefault(); - Polymer.dom(e).rootTarget.blur(); - // @see Issue 4255. - page.show('/q/' + encodeURIComponent(encodeURIComponent(this._inputVal))); + var 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) { + target.$.input.blur(); + } else { + target.blur(); + } + if (this._inputVal) { + // @see Issue 4255. + page.show('/q/' + + encodeURIComponent(encodeURIComponent(this._inputVal))); + } }, - // TODO(kaspern): Flesh this out better. - _makeSuggestion: function(str) { - return { - name: str, - value: str, - }; + /** + * Fetch from the API the predicted accounts. + * @param {string} predicate - The first part of the search term, e.g. + * 'owner' + * @param {string} expression - The second part of the search term, e.g. + * 'kasp' + * @return {!Promise} This returns a promise that resolves to an array of + * strings. + */ + _fetchAccounts: function(predicate, expression) { + if (expression.length === 0) { return Promise.resolve([]); } + return this.$.restAPI.getSuggestedAccounts( + expression, + MAX_AUTOCOMPLETE_RESULTS) + .then(function(accounts) { + if (!accounts) { return []; } + return accounts.map(function(acct) { + return predicate + ':"' + acct.name + ' <' + acct.email + '>"'; + }); + }); }, - // TODO(kaspern): Expand support for more complicated autocomplete features. + /** + * Fetch from the API the predicted groups. + * @param {string} predicate - The first part of the search term, e.g. + * 'ownerin' + * @param {string} expression - The second part of the search term, e.g. + * 'polyger' + * @return {!Promise} This returns a promise that resolves to an array of + * strings. + */ + _fetchGroups: function(predicate, expression) { + if (expression.length === 0) { return Promise.resolve([]); } + return this.$.restAPI.getSuggestedGroups( + expression, + MAX_AUTOCOMPLETE_RESULTS) + .then(function(groups) { + if (!groups) { return []; } + var keys = Object.keys(groups); + return keys.map(function(key) { return predicate + ':' + key; }); + }); + }, + + /** + * Fetch from the API the predicted projects. + * @param {string} predicate - The first part of the search term, e.g. + * 'project' + * @param {string} expression - The second part of the search term, e.g. + * 'gerr' + * @return {!Promise} This returns a promise that resolves to an array of + * strings. + */ + _fetchProjects: function(predicate, expression) { + return this.$.restAPI.getSuggestedProjects( + expression, + MAX_AUTOCOMPLETE_RESULTS) + .then(function(projects) { + if (!projects) { return []; } + var keys = Object.keys(projects); + return keys.map(function(key) { return predicate + ':' + key; }); + }); + }, + + /** + * Determine what array of possible suggestions should be provided + * to _getSearchSuggestions. + * @param {string} input - The full search term, in lowercase. + * @return {!Promise} This returns a promise that resolves to an array of + * strings. + */ + _fetchSuggestions: function(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] || ''; + // Switch on the predicate to determine what to autocomplete. + switch (predicate) { + case 'ownerin': + case 'reviewerin': + // Fetch groups. + return this._fetchGroups(predicate, expression); + + case 'parentproject': + case 'project': + // Fetch projects. + return this._fetchProjects(predicate, expression); + + case 'author': + case 'commentby': + case 'committer': + case 'from': + case 'owner': + case 'reviewedby': + case 'reviewer': + // Fetch accounts. + return this._fetchAccounts(predicate, expression); + + default: + return Promise.resolve(SEARCH_OPERATORS + .filter(function(operator) { + return operator.indexOf(input) !== -1; + })); + } + }, + + /** + * Get the sorted, pruned list of suggestions for the current search query. + * @param {string} input - The complete search query. + * @return {!Promise} This returns a promise that resolves to an array of + * strings. + */ _getSearchSuggestions: function(input) { - return Promise.resolve(SEARCH_OPERATORS).then(function(operators) { - if (!operators) { return []; } - var lowerCaseInput = input - .substring(input.lastIndexOf(' ') + 1) - .toLowerCase(); - return operators - .filter(function(operator) { - // Disallow autocomplete values that exactly match the whole str. - var opContainsInput = operator.indexOf(lowerCaseInput) !== -1; - var inputContainsOp = lowerCaseInput.indexOf(operator) !== -1; - return opContainsInput && !inputContainsOp; - }) - // Prioritize results that start with the input. - .sort(function(operator) { - return operator.indexOf(lowerCaseInput); - }) - .map(this._makeSuggestion); - }.bind(this)); + // Allow spaces within quoted terms. + var tokens = input.match(TOKENIZE_REGEX); + var trimmedInput = tokens[tokens.length - 1].toLowerCase(); + + return this._fetchSuggestions(trimmedInput) + .then(function(operators) { + if (!operators) { return []; } + return operators + // Disallow autocomplete values that exactly match the str. + .filter(function(operator) { + return input.indexOf(operator.toLowerCase()) == -1; + }) + // Prioritize results that start with the input. + .sort(function(operator) { + return operator.indexOf(trimmedInput); + }) + // 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) { + return { + name: operator, + value: operator, + }; + }); + }); }, _handleKey: function(e) {
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 0c16774..a6f1817 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
@@ -58,6 +58,7 @@ assert.notEqual(getActiveElement(), element.$.searchButton); done(); }); + element.value = 'test'; MockInteractions.tap(element.$.searchButton); }); @@ -68,6 +69,7 @@ assert.notEqual(getActiveElement(), element.$.searchButton); done(); }); + element.value = 'test'; MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13); }); @@ -79,22 +81,99 @@ showStub.restore(); }); - test('_getSearchSuggestions returns proper set of suggestions', - function(done) { - element._getSearchSuggestions('is:o') - .then(function(suggestions) { - assert.equal(suggestions[0].name, 'is:open'); - assert.equal(suggestions[0].value, 'is:open'); - assert.equal(suggestions[1].name, 'is:owner'); - assert.equal(suggestions[1].value, 'is:owner'); - }) - .then(function() { - element._getSearchSuggestions('asdasdasdasd') - .then(function(suggestions) { - assert.equal(suggestions.length, 0); - done(); - }); + test('input blurred after commit', function() { + var showStub = sinon.stub(page, 'show'); + var blurSpy = sinon.spy(element.$.searchInput.$.input, 'blur'); + element.$.searchInput.text = 'fate/stay'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13); + assert.isTrue(blurSpy.called); + showStub.restore(); + blurSpy.restore(); + }); + + test('empty search query does not trigger nav', function() { + var showSpy = sinon.spy(page, 'show'); + element.value = ''; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13); + assert.isFalse(showSpy.called); + }); + + suite('_getSearchSuggestions', + function() { + setup(function() { + sinon.stub(element.$.restAPI, 'getSuggestedAccounts', function() { + return Promise.resolve([ + { + name: 'fred', + email: 'fred@goog.co', + }, + ]); + }); + sinon.stub(element.$.restAPI, 'getSuggestedGroups', function() { + return Promise.resolve({ + Polygerrit: 0, }); + }); + sinon.stub(element.$.restAPI, 'getSuggestedProjects', function() { + return Promise.resolve({ + Polygerrit: 0, + }); + }); + }); + + teardown(function() { + element.$.restAPI.getSuggestedAccounts.restore(); + element.$.restAPI.getSuggestedGroups.restore(); + element.$.restAPI.getSuggestedProjects.restore(); + }); + + test('Autocompletes accounts', + function(done) { + return element._getSearchSuggestions('owner:fr') + .then(function(suggestions) { + assert.equal(suggestions[0].value, 'owner:"fred <fred@goog.co>"'); + done(); + }); + }); + + test('Autocompletes groups', + function(done) { + return element._getSearchSuggestions('ownerin:pol') + .then(function(suggestions) { + assert.equal(suggestions[0].value, 'ownerin:Polygerrit'); + done(); + }); + }); + + test('Autocompletes projects', + function(done) { + return element._getSearchSuggestions('project:pol') + .then(function(suggestions) { + assert.equal(suggestions[0].value, 'project:Polygerrit'); + done(); + }); + }); + + test('Autocompletes simple searches', + function(done) { + return element._getSearchSuggestions('is:o') + .then(function(suggestions) { + assert.equal(suggestions[0].name, 'is:open'); + assert.equal(suggestions[0].value, 'is:open'); + assert.equal(suggestions[1].name, 'is:owner'); + assert.equal(suggestions[1].value, 'is:owner'); + done(); + }); + }); + + test('Does not autocomplete with no match', + function(done) { + return element._getSearchSuggestions('asdasdasdasd') + .then(function(suggestions) { + assert.equal(suggestions.length, 0); + done(); + }); + }); }); }); </script>
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 ec19a2d..40a90b7 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
@@ -14,10 +14,12 @@ limitations under the License. --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> <link rel="import" href="../gr-diff-comment-thread/gr-diff-comment-thread.html"> <link rel="import" href="../gr-diff-processor/gr-diff-processor.html"> <link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html"> <link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html"> + <dom-module id="gr-diff-builder"> <template> <div class="contentWrapper"> @@ -32,6 +34,7 @@ <gr-diff-processor id="processor" groups="{{_groups}}"></gr-diff-processor> + <gr-reporting id="reporting"></gr-reporting> </template> <script src="../gr-diff/gr-diff-line.js"></script> <script src="../gr-diff/gr-diff-group.js"></script> @@ -74,6 +77,7 @@ _builder: Object, _groups: Array, _layers: Array, + _showTabs: Boolean, }, get diffElement() { @@ -89,6 +93,7 @@ this._layers = [ this.$.syntaxLayer, this._createIntralineLayer(), + this._createTabIndicatorLayer(), this.$.rangeLayer, ]; @@ -99,6 +104,7 @@ render: function(comments, prefs) { this.$.syntaxLayer.enabled = prefs.syntax_highlighting; + this._showTabs = !!prefs.show_tabs; // Stop the processor (if it's running). this.$.processor.cancel(); @@ -111,17 +117,19 @@ this._clearDiffContent(); - console.time(TimingLabel.TOTAL); - console.time(TimingLabel.CONTENT); + var reporting = this.$.reporting; + + reporting.time(TimingLabel.TOTAL); + reporting.time(TimingLabel.CONTENT); return this.$.processor.process(this.diff.content).then(function() { if (this.isImageDiff) { this._builder.renderDiffImages(); } - console.timeEnd(TimingLabel.CONTENT); - console.time(TimingLabel.SYNTAX); + reporting.timeEnd(TimingLabel.CONTENT); + reporting.time(TimingLabel.SYNTAX); this.$.syntaxLayer.process().then(function() { - console.timeEnd(TimingLabel.SYNTAX); - console.timeEnd(TimingLabel.TOTAL); + reporting.timeEnd(TimingLabel.SYNTAX); + reporting.timeEnd(TimingLabel.TOTAL); }); this.fire('render'); }.bind(this)); @@ -325,6 +333,31 @@ }; }, + _createTabIndicatorLayer: function() { + var show = (function() { return this._showTabs; }).bind(this); + return { + addListener: function() {}, + annotate: function(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'); + if (!split) { return; } + for (var i = 0, pos = 0; i < split.length - 1; i++) { + // Skip forward by the length of the content + pos += split[i].length; + + GrAnnotation.annotateElement(el, pos, 1, + 'style-scope gr-diff tab-indicator'); + + // Skip forward by one tab character. + pos++; + } + }, + }; + }, + /** * In pages with large diffs, creating the first comment thread can be * slow because nested Polymer elements (particularly
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 2090e98..6844416 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
@@ -340,6 +340,9 @@ side, this._comments.meta.projectConfig); threadEl.comments = comments; + if (opt_side) { + threadEl.setAttribute('data-side', opt_side); + } return threadEl; }; @@ -363,11 +366,14 @@ GrDiffBuilder.prototype._createTextEl = function(line, opt_side) { var td = this._createElement('td'); + var text = line.text; if (line.type !== GrDiffLine.Type.BLANK) { td.classList.add('content'); + if (!text) { + text = '\xa0'; + } } td.classList.add(line.type); - var text = line.text; var html = util.escapeHTML(text); html = this._addTabWrappers(html, this._prefs.tab_size); @@ -493,7 +499,7 @@ for (var i = 0; i < split.length - 1; i++) { offset += split[i].length; width = tabSize - (offset % tabSize); - result += split[i] + this._getTabWrapper(width, this._prefs.show_tabs); + result += split[i] + this._getTabWrapper(width); offset += width; } if (split.length) { @@ -503,7 +509,7 @@ return result; }; - GrDiffBuilder.prototype._getTabWrapper = function(tabSize, showTabs) { + GrDiffBuilder.prototype._getTabWrapper = function(tabSize) { // Force this to be a number to prevent arbitrary injection. tabSize = +tabSize; if (isNaN(tabSize)) { @@ -511,9 +517,6 @@ } var str = '<span class="style-scope gr-diff tab '; - if (showTabs) { - str += 'withIndicator'; - } str += '" style="'; // TODO(andybons): CSS tab-size is not supported in IE. str += 'tab-size:' + tabSize + ';';
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 e8b1453..af44629 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
@@ -414,6 +414,117 @@ }); }); + suite('tab indicators', function() { + var sandbox; + var element; + var layer; + + setup(function() { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element._showTabs = true; + layer = element._createTabIndicatorLayer(); + }); + + teardown(function() { + sandbox.restore(); + }); + + test('does nothing with empty line', function() { + var line = {text: ''}; + var el = document.createElement('div'); + var 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'); + el.textContent = str; + var 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'); + el.textContent = str; + var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + + layer.annotate(el, line); + + assert.equal(annotateElementStub.callCount, 1); + var 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() { + element._showTabs = false; + + var str = '\tlorem upsum'; + var line = {text: str}; + var el = document.createElement('div'); + el.textContent = str; + var 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'); + el.textContent = str; + var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + + layer.annotate(el, line); + + assert.equal(annotateElementStub.callCount, 2); + + var 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'); + + args = annotateElementStub.getCalls()[1].args; + assert.equal(args[0], el); + assert.equal(args[1], 1, 'offset of tab indicator'); + assert.equal(args[2], 1, 'length of tab indicator'); + assert.include(args[3], 'tab-indicator'); + }); + + test('annotates intermediate tabs', function() { + var str = 'lorem\tupsum'; + var line = {text: str}; + var el = document.createElement('div'); + el.textContent = str; + var annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + + layer.annotate(el, line); + + assert.equal(annotateElementStub.callCount, 1); + var 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'); + assert.include(args[3], 'tab-indicator'); + }); + }); + suite('rendering', function() { var content; var outputEl; @@ -437,6 +548,10 @@ ] }, ]; + stub('gr-reporting', { + time: sinon.stub(), + timeEnd: sinon.stub(), + }); element = fixture('basic'); outputEl = element.queryEffectiveChildren('#diffTable'); element.addEventListener('render', function() { @@ -458,6 +573,20 @@ element.render({left: [], right: []}, prefs); }); + test('reporting', function(done) { + var timeStub = element.$.reporting.time; + var timeEndStub = element.$.reporting.timeEnd; + flush(function() { + assert.isTrue(timeStub.calledWithExactly('Diff Total Render')); + assert.isTrue(timeStub.calledWithExactly('Diff Content Render')); + assert.isTrue(timeStub.calledWithExactly('Diff Syntax Render')); + assert.isTrue(timeEndStub.calledWithExactly('Diff Total Render')); + assert.isTrue(timeEndStub.calledWithExactly('Diff Content Render')); + assert.isTrue(timeEndStub.calledWithExactly('Diff Syntax Render')); + done(); + }); + }); + test('renderSection', function() { var section = outputEl.querySelector('stub:nth-of-type(2)'); var prevInnerHTML = section.innerHTML;
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 1b30bde..07badbf 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
@@ -198,8 +198,15 @@ }, _handleTextareaKeydown: function(e) { - if (e.keyCode == 27) { // 'esc' - this._handleCancel(e); + switch (e.keyCode) { + case 27: // 'esc' + this._handleCancel(e); + break; + case 83: // 's' + if (e.ctrlKey) { + this._handleSave(e); + } + break; } },
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 fcf8b41..bddc3ab 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
@@ -211,6 +211,18 @@ MockInteractions.pressAndReleaseKeyOn(element.$.editTextarea, 27); // esc }); + test('ctrl+s saves comment', function(done) { + var stub = sinon.stub(element, 'save', function() { + 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' + }); + test('draft saving/editing', function(done) { var fireStub = sinon.stub(element, 'fire');
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 5a41709..491eded 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,7 +23,6 @@ id="cursorManager" scroll="keep-visible" cursor-target-class="target-row" - fold-offset-top="[[foldOffsetTop]]" 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 99a0b5c..dd11f2c 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
@@ -54,11 +54,6 @@ }, }, - foldOffsetTop: { - type: Number, - value: 0, - }, - /** * If set, the cursor will attempt to move to the line number (instead of * the first chunk) the next time the diff renders. It is set back to null
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 bfe103b..62e58ad 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
@@ -94,6 +94,43 @@ } }, + _getContentTextParent: function(target) { + var element = target; + if (element.nodeName === '#text') { + element = element.parentElement; + } + while (!element.classList.contains('contentText')) { + if (element.parentElement === null) { + return target; + } + element = element.parentElement; + } + return element; + }, + + /** + * 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 + * highlighting, the containers are remapped to the .contentText divs that + * contain the entire line of code. + * + * @param {Object} range - the standard DOM selector range. + * @return {Object} A modified version of the range that correctly accounts + * for syntax highlighting. + */ + _normalizeRange: 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, + range.endContainer); + return { + start: this._normalizeSelectionSide(startContainer, startOffset), + end: this._normalizeSelectionSide(endContainer, endOffset), + }; + }, + /** * Convert DOM Range selection to concrete numbers (line, column, side). * Moves range end if it's not inside td.content. @@ -160,13 +197,12 @@ if (range.collapsed) { return; } - var start = - this._normalizeSelectionSide(range.startContainer, range.startOffset); + var normalizedRange = this._normalizeRange(range); + var start = normalizedRange.start; if (!start) { return; } - var end = - this._normalizeSelectionSide(range.endContainer, range.endOffset); + var end = normalizedRange.end; if (!end) { return; } @@ -270,5 +306,36 @@ return GrAnnotation.getLength(node); } }, + + /** + * Gets the character offset of the child within the parent. + * Performs a synchronous in-order traversal from top to bottom of the node + * element, counting the length of the syntax until child is found. + * + * @param {!Element} The root DOM element to be searched through. + * @param {!Element} The child element being searched for. + * @return {number} + */ + _getTextOffset: function(node, child) { + var count = 0; + var stack = [node]; + while (stack.length) { + var 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]); + } + arr.reverse(); + stack = stack.concat(arr); + } else { + count += this._getLength(n); + } + } + return count; + }, }); })();
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 5f84e4f..2612f9a 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
@@ -487,6 +487,27 @@ assert.equal(getActionSide(), 'left'); }); + test('properly accounts for syntax highlighting', function() { + var content = stubContent(140, 'left'); + var 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); + assert.notDeepEqual(spyCall.returnValue, range); + }); + + test('_getTextOffset computes text offset', function() { + var content = stubContent(140, 'left'); + var child = content.lastChild.lastChild; + var result = element._getTextOffset(content, child); + assert.equal(result, 73); + content = stubContent(146, 'right'); + child = content.lastChild; + result = element._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.
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 2a1e880..2dd4c91 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
@@ -78,6 +78,22 @@ }, _nextStepHandle: Number, + _isScrolling: Boolean, + }, + + attached: function() { + this.listen(window, 'scroll', '_handleWindowScroll'); + }, + + detached: function() { + this.unlisten(window, 'scroll', '_handleWindowScroll'); + }, + + _handleWindowScroll: function() { + this._isScrolling = true; + this.debounce('resetIsScrolling', function() { + this._isScrolling = false; + }, 50); }, /** @@ -100,6 +116,11 @@ var currentBatch = 0; var nextStep = function() { + + if (this._isScrolling) { + this.async(nextStep, 100); + return; + } // If we are done, resolve the promise. if (state.sectionIndex >= content.length) { resolve(this.groups); @@ -201,11 +222,11 @@ /** * Take rows of a shared diff section and produce an array of corresponding * (potentially collapsed) groups. - * @param {Array<String>} rows - * @param {Number} context - * @param {Number} startLineNumLeft - * @param {Number} startLineNumRight - * @param {String} opt_sectionEnd String representing whether this is the + * @param {Array<String>} rows + * @param {Number} context + * @param {Number} startLineNumLeft + * @param {Number} startLineNumRight + * @param {String} opt_sectionEnd String representing whether this is the * first section or the last section or neither. Use the values 'first', * 'last' and null respectively. * @return {Array<GrDiffGroup>} @@ -264,10 +285,10 @@ /** * Take the rows of a delta diff section and produce the corresponding * group. - * @param {Array<String>} rowsAdded - * @param {Array<String>} rowsRemoved - * @param {Number} startLineNumLeft - * @param {Number} startLineNumRight + * @param {Array<String>} rowsAdded + * @param {Array<String>} rowsRemoved + * @param {Number} startLineNumLeft + * @param {Number} startLineNumRight * @return {GrDiffGroup} */ _deltaGroupFromRows: function(rowsAdded, rowsRemoved, startLineNumLeft, @@ -325,7 +346,7 @@ * In order to show comments out of the bounds of the selected context, * treat them as separate chunks within the model so that the content (and * context surrounding it) renders correctly. - * @param {Object} content The diff content object. + * @param {Object} content The diff content object. * @return {Object} A new diff content object with regions split up. */ _splitCommonGroupsWithComments: function(content) { @@ -477,8 +498,8 @@ /** * Given an array and a size, return an array of arrays where no inner array * is larger than that size, preserving the original order. - * @param {!Array<T>} - * @param {number} + * @param {!Array<T>} array + * @param {number} size * @return {!Array<!Array<T>>} * @template T */ @@ -489,7 +510,7 @@ var head = array.slice(0, array.length - size); var tail = array.slice(array.length - size); - return this._breakdown(head, size).concat([tail]) + 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 9d687ac..4f8c532 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
@@ -40,6 +40,15 @@ 'fugit assum per.'; var element; + var sandbox; + + setup(function() { + sandbox = sinon.sandbox.create(); + }); + + teardown(function() { + sandbox.restore(); + }); suite('not logged in', function() { @@ -409,6 +418,23 @@ ]); }); + test('scrolling pauses rendering', function() { + var contentRow = { + ab: [ + '<!DOCTYPE html>', + '<meta charset="utf-8">', + ] + }; + var content = _.times(200, _.constant(contentRow)); + sandbox.stub(element, 'async'); + element._isScrolling = true; + element.process(content); + assert.equal(element.groups.length, 1); + element._isScrolling = false; + element.process(content); + assert.equal(element.groups.length, 33); + }); + suite('gr-diff-processor helpers', function() { var rows; @@ -512,15 +538,6 @@ }); suite('_breakdown*', function() { - var sandbox; - setup(function() { - sandbox = sinon.sandbox.create(); - }); - - teardown(function() { - sandbox.restore(); - }); - test('_breakdownGroup ignores shared groups', function() { sandbox.stub(element, '_breakdown'); var chunk = {ab: ['blah', 'blah', 'blah']};
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 09cab0b..3c24291 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
@@ -18,17 +18,21 @@ <dom-module id="gr-diff-selection"> <template> <style> - .contentWrapper ::content .content { + .contentWrapper ::content .content, + .contentWrapper ::content .contextControl { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } - :host.selected-right .contentWrapper ::content .right + .content, - :host.selected-left .contentWrapper ::content .left + .content, - :host.selected-right .contentWrapper ::content .unified .right ~ .content, - :host.selected-left .contentWrapper ::content .unified .left ~ .content { + :host-context(.selected-left:not(.selected-comment)) .contentWrapper ::content .side-by-side .left + .content .contentText, + :host-context(.selected-right:not(.selected-comment)) .contentWrapper ::content .side-by-side .right + .content .contentText, + :host-context(.selected-left:not(.selected-comment)) .contentWrapper ::content .unified .left.lineNum ~ .content:not(.both) .contentText, + :host-context(.selected-right:not(.selected-comment)) .contentWrapper ::content .unified .right.lineNum ~ .content .contentText, + :host-context(.selected-left.selected-comment) .contentWrapper ::content .side-by-side .left + .content .message, + :host-context(.selected-right.selected-comment) .contentWrapper ::content .side-by-side .right + .content .message, + :host-context(.selected-comment) .contentWrapper ::content .unified .message { -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;
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 7d0b7ea..35da901 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,7 +18,12 @@ is: 'gr-diff-selection', properties: { + diff: Object, _cachedDiffBuilder: Object, + _linesCache: { + type: Object, + value: function() { return {left: null, right: null}; }, + }, }, listeners: { @@ -56,8 +61,12 @@ }, _handleCopy: function(e) { - if (!e.target.classList.contains('content')) { - return; + var el = e.target; + while (!el.classList.contains('content')) { + if (!el.parentElement) { + return; + } + el = el.parentElement; } var lineEl = this.diffBuilder.getLineElByChild(e.target); if (!lineEl) { @@ -69,27 +78,53 @@ e.preventDefault(); }, - _getSelectedText: function(opt_side) { + _getSelectedText: function(side) { var sel = window.getSelection(); if (sel.rangeCount != 1) { return; // No multi-select support yet. } var range = sel.getRangeAt(0); - var fragment = range.cloneContents(); - var selector = '.content,td.content:nth-of-type(1)'; - if (opt_side) { - selector = '.' + opt_side + ' + ' + selector; + 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); + + return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum, + range.endOffset, side); + }, + + _getRangeFromDiff: function(startLineNum, startOffset, endLineNum, + endOffset, side) { + var lines = this._getDiffLines(side).slice(startLineNum - 1, endLineNum); + if (lines.length) { + lines[0] = lines[0].substring(startOffset); + lines[lines.length - 1] = lines[lines.length - 1] + .substring(0, endOffset); } - var contentEls = Polymer.dom(fragment).querySelectorAll(selector); - if (contentEls.length === 0) { - return fragment.textContent; + return lines.join('\n'); + }, + + _getDiffLines: function(side) { + if (this._linesCache[side]) { + return this._linesCache[side]; } - var text = ''; - for (var i = 0; i < contentEls.length; i++) { - text += contentEls[i].textContent + '\n'; + 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]; + if (chunk.ab) { + lines = lines.concat(chunk.ab); + } else if (chunk[key]) { + lines = lines.concat(chunk[key]); + } } - return text; + + this._linesCache[side] = lines; + return lines; }, }); })();
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 f99e373..cdc7483 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
@@ -27,24 +27,36 @@ <test-fixture id="basic"> <template> <gr-diff-selection> - <table> + <table class="side-by-side"> <tr> - <td class="lineNum left">1</td> - <td class="content">ba ba</td> - <td class="lineNum right">1</td> - <td class="content">some other text</td> + <td class="lineNum left" data-value="1">1</td> + <td class="content"> + <div class="contentText" data-side="left">ba ba</div> + </td> + <td class="lineNum right" data-value="1">1</td> + <td class="content"> + <div class="contentText" data-side="right">some other text</div> + </td> </tr> <tr> - <td class="lineNum left">2</td> - <td class="content">zin</td> - <td class="lineNum right">2</td> - <td class="content">more more more</td> + <td class="lineNum left" data-value="2">2</td> + <td class="content"> + <div class="contentText" data-side="left">zin</div> + </td> + <td class="lineNum right" data-value="2">2</td> + <td class="content"> + <div class="contentText" data-side="right">more more more</div> + </td> </tr> <tr> - <td class="lineNum left">2</td> - <td class="content">ga ga</td> - <td class="lineNum right">3</td> - <td class="other">some other text</td> + <td class="lineNum left" data-value="3">3</td> + <td class="content"> + <div class="contentText" data-side="left">ga ga</div> + </td> + <td class="lineNum right" data-value="3">3</td> + <td class="other"> + <div class="contentText" data-side="right">some other text</div> + </td> </tr> </table> </gr-diff-selection> @@ -73,6 +85,22 @@ getLineElByChild: sinon.stub().returns({}), getSideByLineEl: sinon.stub(), }; + element.diff = { + content: [ + { + a: ['ba ba'], + b: ['some other text'], + }, + { + a: ['zin'], + b: ['more more more'], + }, + { + a: ['ga ga'], + b: ['some other text'], + }, + ], + }; }); test('applies selected-left on left side click', function() { @@ -105,38 +133,43 @@ test('asks for text for right side Elements', function() { element._cachedDiffBuilder.getSideByLineEl.returns('left'); sinon.stub(element, '_getSelectedText'); - emulateCopyOn(element.querySelector('td.content')); + emulateCopyOn(element.querySelector('div.contentText')); assert.deepEqual(['left'], element._getSelectedText.lastCall.args); }); test('reacts to copy for content Elements', function() { sinon.stub(element, '_getSelectedText'); - emulateCopyOn(element.querySelector('td.content')); + emulateCopyOn(element.querySelector('div.contentText')); assert.isTrue(element._getSelectedText.called); }); test('copy event is prevented for content Elements', function() { sinon.stub(element, '_getSelectedText'); - var event = emulateCopyOn(element.querySelector('td.content')); + var event = emulateCopyOn(element.querySelector('div.contentText')); assert.isTrue(event.preventDefault.called); }); test('inserts text into clipboard on copy', function() { sinon.stub(element, '_getSelectedText').returns('the text'); - var event = emulateCopyOn(element.querySelector('td.content')); + var event = emulateCopyOn(element.querySelector('div.contentText')); assert.deepEqual( ['Text', 'the text'], event.clipboardData.setData.lastCall.args); }); test('copies content correctly', function() { element.classList.add('selected-left'); + element.classList.remove('selected-right'); + // Fetch the line number. + element._cachedDiffBuilder.getLineElByChild = function(child) { + return child.parentElement.parentElement.previousElementSibling; + }; var selection = window.getSelection(); var range = document.createRange(); - range.setStart(element.querySelector('td.content').firstChild, 3); + range.setStart(element.querySelector('div.contentText').firstChild, 3); range.setEnd( - element.querySelectorAll('td.content')[4].firstChild, 2); + element.querySelectorAll('div.contentText')[4].firstChild, 2); selection.addRange(range); - assert.equal('ba\nzin\nga\n', element._getSelectedText('left')); + assert.equal(element._getSelectedText('left'), 'ba\nzin\nga'); }); }); </script>
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 2573ad1..76f7aec 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
@@ -33,9 +33,18 @@ background-color: var(--view-background-color); display: block; } - h3 { + header, + .subHeader { + align-items: center; + display: flex; + justify-content: space-between; + } + header { padding: .75em var(--default-horizontal-margin); } + .navLink:not([href]) { + color: #999; + } .reviewed { display: inline-block; margin: 0 .25em; @@ -97,10 +106,7 @@ padding: 0 var(--default-horizontal-margin) 1em; color: #666; } - .header { - align-items: center; - display: flex; - justify-content: space-between; + .subHeader { margin: 0 var(--default-horizontal-margin) .75em; } .prefsButton { @@ -125,49 +131,56 @@ } } </style> - <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"> - <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler"> - <span>[[_computeFileDisplayName(_path)]]</span> - <span class="downArrow">▼</span> - </gr-button> - <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25"> - <div class="dropdown-content"> + <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"> + <gr-button link class="dropdown-trigger" id="trigger" on-tap="_showDropdownTapHandler"> + <span>[[_computeFileDisplayName(_path)]]</span> + <span class="downArrow">▼</span> + </gr-button> + <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="25"> + <div class="dropdown-content"> + <template is="dom-repeat" items="[[_fileList]]" as="path"> + <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"> + <select on-change="_handleMobileSelectChange"> <template is="dom-repeat" items="[[_fileList]]" as="path"> - <a href$="[[_computeDiffURL(_changeNum, _patchRange.*, path)]]" - selected$="[[_computeFileSelected(path, _path)]]" - data-key-nav$="[[_computeKeyNav(path, _path, _fileList)]]" - on-tap="_handleFileTap"> - [[_computeFileDisplayName(path)]] - </a> + <option + value$="[[path]]" + selected$="[[_computeFileSelected(path, _path)]]"> + [[_computeFileDisplayName(path)]] + </option> </template> - </div> - </iron-dropdown> + </select> + </div> + </h3> + <div> + <a class="navLink" + href$="[[_computeNavLinkURL(_path, _fileList, -1, 1)]]">Prev</a> + / + <a class="navLink" + href$="[[_computeNavLinkURL(_path, _fileList, 1, 1)]]">Next</a> </div> - <div class="mobileJumpToFileContainer"> - <select on-change="_handleMobileSelectChange"> - <template is="dom-repeat" items="[[_fileList]]" as="path"> - <option - value$="[[path]]" - selected$="[[_computeFileSelected(path, _path)]]"> - [[_computeFileDisplayName(path)]] - </option> - </template> - </select> - </div> - </h3> + </header> <div class="loading" hidden$="[[!_loading]]">Loading...</div> <div hidden$="[[_loading]]" hidden> - <div class="header"> + <div class="subHeader"> <gr-patch-range-select path="[[_path]]" change-num="[[_changeNum]]" @@ -180,7 +193,8 @@ id="modeSelect" is="gr-select" bind-value="{{changeViewState.diffMode}}" - hidden$="[[_computeModeSelectHidden(_isImageDiff)]]"> + hidden$="[[_computeModeSelectHidden(_isImageDiff)]]" + on-change="_handleDropdownChange"> <option value="SIDE_BY_SIDE">Side By Side</option> <option value="UNIFIED_DIFF">Unified</option> </select>
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 d6a3bc0..40b0ee1 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
@@ -105,6 +105,15 @@ } }.bind(this)); + if (this.changeViewState.diffMode === null) { + // Initialize with user's diff mode preference. Default to + // SIDE_BY_SIDE in the meantime. + this.set('changeViewState.diffMode', DiffViewMode.SIDE_BY_SIDE); + this.$.restAPI.getPreferences().then(function(prefs) { + this.set('changeViewState.diffMode', prefs.diff_view); + }.bind(this)); + } + if (this._path) { this.fire('title-change', {title: this._computeFileDisplayName(this._path)}); @@ -113,11 +122,6 @@ this.$.cursor.push('diffs', this.$.diff); }, - detached: function() { - // Reset the diff mode to null so that it reverts to the user preference. - this.changeViewState.diffMode = null; - }, - _getLoggedIn: function() { return this.$.restAPI.getLoggedIn(); }, @@ -207,11 +211,11 @@ break; case 219: // '[' e.preventDefault(); - this._navToFile(this._fileList, -1); + this._navToFile(this._path, this._fileList, -1); break; case 221: // ']' e.preventDefault(); - this._navToFile(this._fileList, 1); + this._navToFile(this._path, this._fileList, 1); break; case 78: // 'n' e.preventDefault(); @@ -256,20 +260,41 @@ } }, - _navToFile: function(fileList, direction) { - if (fileList.length == 0) { return; } + _navToFile: function(path, fileList, direction) { + var url = this._computeNavLinkURL(path, fileList, direction); + if (!url) { return; } - var idx = fileList.indexOf(this._path) + direction; + page.show(this._computeNavLinkURL(path, fileList, direction)); + }, + + /** + * @param {?string} path The path of the current file being shown. + * @param {Array.<string>} fileList The list of files in this change and + * patch range. + * @param {number} direction Either 1 (next file) or -1 (prev file). + * @param {(number|boolean)} opt_noUp Whether to return to the change view + * when advancing the file goes outside the bounds of fileList. + * + * @return {?string} The next URL when proceeding in the specified + * direction. + */ + _computeNavLinkURL: function(path, fileList, direction, opt_noUp) { + if (!path || fileList.length === 0) { return null; } + + var idx = fileList.indexOf(path); + if (idx === -1) { return null; } + + idx += direction; + // Redirect to the change view if opt_noUp isn’t truthy and idx falls + // outside the bounds of [0, fileList.length). if (idx < 0 || idx > fileList.length - 1) { - page.show(this._getChangePath( + if (opt_noUp) { return null; } + return this._getChangePath( this._changeNum, this._patchRange, - this._change && this._change.revisions)); - return; + this._change && this._change.revisions); } - page.show(this._getDiffURL(this._changeNum, - this._patchRange, - fileList[idx])); + return this._getDiffURL(this._changeNum, this._patchRange, fileList[idx]); }, _paramsChanged: function(value) { @@ -484,5 +509,9 @@ this.$.cursor.moveToLineNumber(detail.number, detail.side); history.pushState(null, null, '#' + this.$.cursor.getAddress()); }, + + _handleDropdownChange: function(e) { + e.target.blur(); + }, }); })();
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 0a4d6b6..979ed55 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
@@ -32,6 +32,12 @@ </template> </test-fixture> +<test-fixture id="blank"> + <template> + <div></div> + </template> +</text-fixture> + <script> suite('gr-diff-view tests', function() { var element; @@ -338,6 +344,52 @@ assert.equal(linkEls[2].getAttribute('href'), '/c/42/5..10/wheatley.md'); }); + test('prev/next links', function() { + element._changeNum = '42'; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: '10', + }; + element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; + element._path = 'glados.txt'; + flushAsynchronousOperations(); + var linkEls = Polymer.dom(element.root).querySelectorAll('.navLink'); + assert.equal(linkEls.length, 2); + assert.equal(linkEls[0].getAttribute('href'), '/c/42/10/chell.go'); + assert.equal(linkEls[1].getAttribute('href'), '/c/42/10/wheatley.md'); + element._path = 'wheatley.md'; + flushAsynchronousOperations(); + assert.equal(linkEls[0].getAttribute('href'), '/c/42/10/glados.txt'); + assert.isFalse(linkEls[1].hasAttribute('href')); + element._path = 'chell.go'; + flushAsynchronousOperations(); + assert.isFalse(linkEls[0].hasAttribute('href')); + assert.equal(linkEls[1].getAttribute('href'), '/c/42/10/glados.txt'); + }); + + test('prev/next links with patch range', function() { + element._changeNum = '42'; + element._patchRange = { + basePatchNum: '5', + patchNum: '10', + }; + element._fileList = ['chell.go', 'glados.txt', 'wheatley.md']; + element._path = 'glados.txt'; + flushAsynchronousOperations(); + var linkEls = Polymer.dom(element.root).querySelectorAll('.navLink'); + assert.equal(linkEls.length, 2); + assert.equal(linkEls[0].getAttribute('href'), '/c/42/5..10/chell.go'); + assert.equal(linkEls[1].getAttribute('href'), '/c/42/5..10/wheatley.md'); + element._path = 'wheatley.md'; + flushAsynchronousOperations(); + assert.equal(linkEls[0].getAttribute('href'), '/c/42/5..10/glados.txt'); + assert.isFalse(linkEls[1].hasAttribute('href')); + element._path = 'chell.go'; + flushAsynchronousOperations(); + assert.isFalse(linkEls[0].hasAttribute('href')); + assert.equal(linkEls[1].getAttribute('href'), '/c/42/5..10/glados.txt'); + }); + test('file review status', function(done) { element._loggedIn = true; element._changeNum = '42'; @@ -371,7 +423,7 @@ test('diff mode selector correctly toggles the diff', function() { var select = element.$.modeSelect; var diffDisplay = element.$.diff; - + var blurSpy = sinon.spy(select, 'blur'); element._userPrefs = {diff_view: 'SIDE_BY_SIDE'}; // The mode selected in the view state reflects the selected option. @@ -392,6 +444,30 @@ assert.equal(element._getDiffViewMode(), newMode); assert.equal(element._getDiffViewMode(), select.value); assert.equal(element._getDiffViewMode(), diffDisplay.viewMode); + assert(blurSpy.called, 'select should be blurred after selection'); + }); + + test('diff mode selector initializes from preferences', function() { + var resolvePrefs; + var prefsPromise = new Promise(function(resolve) { + resolvePrefs = resolve; + }); + var getPreferencesStub = sinon.stub(element.$.restAPI, 'getPreferences', + function() { return 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; + fixture('blank').appendChild(view); + flushAsynchronousOperations(); + + // At this point the diff mode doesn't yet have the user's preference. + assert.equal(select.value, 'SIDE_BY_SIDE'); + + // Receive the overriding preference. + resolvePrefs({diff_view: 'UNIFIED'}); + flushAsynchronousOperations(); + assert.equal(select.value, 'SIDE_BY_SIDE'); }); test('_loadHash', function() {
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 46612a0..4672b31 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.html
@@ -91,13 +91,6 @@ vertical-align: top; white-space: pre; } - .contentText:empty:before { - /** - * Insert glyph to prevent empty diff content from collapsing. - * "\200B" is a 'ZERO WIDTH SPACE' (U+200B) - */ - content: "\200B"; - } .contextLineNum:before, .lineNum:before { display: inline-block; @@ -152,17 +145,17 @@ } .tab { display: inline-block; - position: relative; } - .tab.withIndicator { - color: #D68E47; - text-decoration: line-through; + .tab-indicator:before { + color: #C62828; + /* >> character */ + content: '\00BB'; } </style> <style include="gr-theme-default"></style> <div class$="[[_computeContainerClass(_loggedIn, viewMode)]]" on-tap="_handleTap"> - <gr-diff-selection> + <gr-diff-selection diff="[[_diff]]"> <gr-diff-highlight id="highlights" logged-in="[[_loggedIn]]"
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 dbcbb38..7b93086 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -34,6 +34,10 @@ properties: { changeNum: String, + hidden: { + type: Boolean, + observer: '_handleShowDiff', + }, patchRange: Object, path: String, prefs: { @@ -86,6 +90,12 @@ }.bind(this)); }, + _handleShowDiff: function(hidden) { + if (!hidden) { + this.reload(); + } + }, + reload: function() { this._clearDiffContent();
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 c33eadb..2f0b6bd 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
@@ -467,6 +467,17 @@ assert.equal(drafts.length, 1); assert.equal(drafts[0].id, id); }); + + test('_handleShowDiff reloads when hidden is made false', + function(done) { + var stub = sinon.stub(element, 'reload', function() { + assert.isTrue(stub.called); + stub.restore(); + done(); + }); + var spy = sinon.spy(element, '_handleShowDiff'); + element.set('hidden', false); + }); }); }); });
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 24d36c4..b50043e 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
@@ -33,6 +33,7 @@ rangeStr = leftPatch + '..' + rangeStr; } page.show('/c/' + this.changeNum + '/' + rangeStr + '/' + this.path); + e.target.blur(); }, _computeLeftSelected: function(patchNum, patchRange) {
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 c7e1196..95789bc 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
@@ -63,6 +63,7 @@ var showStub = sinon.stub(page, 'show'); var leftSelectEl = element.$.leftPatchSelect; var rightSelectEl = element.$.rightPatchSelect; + var blurSpy = sinon.spy(leftSelectEl, 'blur'); element.changeNum = '42'; element.path = 'path/to/file.txt'; element.availablePatches = ['1', '2', '3']; @@ -77,6 +78,7 @@ 'Should navigate to /c/42/3/path/to/file.txt'); leftSelectEl.value = '1'; element.fire('change', {}, {node: leftSelectEl}); + assert(blurSpy.called, 'Dropdown should be blurred after selection'); } else if (numEvents == 2) { assert(showStub.lastCall.calledWithExactly( '/c/42/1..3/path/to/file.txt'),
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html index c5c9377..9c5d6bf 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html +++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
@@ -14,7 +14,12 @@ limitations under the License. --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../gr-syntax-lib-loader/gr-syntax-lib-loader.html"> + <dom-module id="gr-syntax-layer"> + <template> + <gr-syntax-lib-loader id="libLoader"></gr-syntax-lib-loader> + </template> <script src="../gr-diff/gr-diff-line.js"></script> <script src="../gr-diff-highlight/gr-annotation.js"></script> <script src="gr-syntax-layer.js"></script>
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 478bcc8..c0ee1fa 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
@@ -106,6 +106,7 @@ value: function() { return []; }, }, _processHandle: Number, + _hljs: Object, }, addListener: function(fn) { @@ -278,7 +279,6 @@ _processNextLine: function(state) { var baseLine = undefined; var revisionLine = undefined; - var hljs = this._getHighlightLib(); var section = this.diff.content[state.sectionIndex]; if (section.ab) { @@ -301,15 +301,15 @@ var result; if (this._baseLanguage && baseLine !== undefined) { - result = hljs.highlight(this._baseLanguage, baseLine, true, + result = this._hljs.highlight(this._baseLanguage, baseLine, true, state.baseContext); this.push('_baseRanges', this._rangesFromString(result.value)); state.baseContext = result.top; } if (this._revisionLanguage && revisionLine !== undefined) { - result = hljs.highlight(this._revisionLanguage, revisionLine, true, - state.revisionContext); + result = this._hljs.highlight(this._revisionLanguage, revisionLine, + true, state.revisionContext); this.push('_revisionRanges', this._rangesFromString(result.value)); state.revisionContext = result.top; } @@ -358,45 +358,10 @@ }); }, - _getHighlightLib: function() { - return window.hljs; - }, - - _isHighlightLibLoaded: function() { - return !!this._getHighlightLib(); - }, - - _configureHighlightLib: function() { - this._getHighlightLib().configure( - {classPrefix: 'gr-diff gr-syntax gr-syntax-'}); - }, - - _getLibRoot: function() { - if (this._cachedLibRoot) { return this._cachedLibRoot; } - - return this._cachedLibRoot = document.head - .querySelector('link[rel=import][href$="gr-app.html"]') - .href - .match(/(.+\/)elements\/gr-app\.html/)[1]; - }, - _cachedLibRoot: null, - - /** - * Load and configure the HighlightJS library. If the library is already - * loaded, then do nothing and resolve. - * @return {Promise} - */ _loadHLJS: function() { - if (this._isHighlightLibLoaded()) { return Promise.resolve(); } - return new Promise(function(resolve) { - var script = document.createElement('script'); - script.src = this._getLibRoot() + HLJS_PATH; - script.onload = function() { - this._configureHighlightLib(); - resolve(); - }.bind(this); - Polymer.dom(this.root).appendChild(script); + return this.$.libLoader.get().then(function(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 5106671..178f61b 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
@@ -182,8 +182,8 @@ var mockHLJS = getMockHLJS(); var highlightSpy = sinon.spy(mockHLJS, 'highlight'); - sandbox.stub(element, '_getHighlightLib', - function() { return mockHLJS; }); + sandbox.stub(element.$.libLoader, 'get', + function() { return Promise.resolve(mockHLJS); }); var processNextSpy = sandbox.spy(element, '_processNextLine'); var processPromise = element.process();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html new file mode 100644 index 0000000..fedd22a --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html
@@ -0,0 +1,20 @@ +<!-- +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. +--> +<link rel="import" href="../../../bower_components/polymer/polymer.html"> + +<dom-module id="gr-syntax-lib-loader"> + <script src="gr-syntax-lib-loader.js"></script> +</dom-module>
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 new file mode 100644 index 0000000..520f24d --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js
@@ -0,0 +1,93 @@ +// 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. +(function() { + 'use strict'; + + var HLJS_PATH = 'bower_components/highlightjs/highlight.min.js'; + var LIB_ROOT_PATTERN = /(.+\/)elements\/gr-app\.html/; + + Polymer({ + is: 'gr-syntax-lib-loader', + + properties: { + _state: { + type: Object, + + // NOTE: intended singleton. + value: { + loaded: false, + loading: false, + callbacks: [], + }, + } + }, + + get: function() { + return new Promise(function(resolve) { + // If the lib is totally loaded, resolve immediately. + if (this._state.loaded) { + resolve(this._getHighlightLib()); + return; + } + + // If the library is not currently being loaded, then start loading it. + if (!this._state.loading) { + this._state.loading = true; + this._loadHLJS().then(this._onLibLoaded.bind(this)); + } + + this._state.callbacks.push(resolve); + }.bind(this)); + }, + + _onLibLoaded: function() { + var lib = this._getHighlightLib(); + this._state.loaded = true; + this._state.loading = false; + this._state.callbacks.forEach(function(cb) { cb(lib); }); + this._state.callbacks = []; + }, + + _getHighlightLib: function() { + return window.hljs; + }, + + _configureHighlightLib: function() { + this._getHighlightLib().configure( + {classPrefix: 'gr-diff gr-syntax gr-syntax-'}); + }, + + _getLibRoot: function() { + if (this._cachedLibRoot) { return this._cachedLibRoot; } + + return this._cachedLibRoot = document.head + .querySelector('link[rel=import][href$="gr-app.html"]') + .href + .match(LIB_ROOT_PATTERN)[1]; + }, + _cachedLibRoot: null, + + _loadHLJS: function() { + return new Promise(function(resolve) { + var 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 new file mode 100644 index 0000000..13bea04 --- /dev/null +++ b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html
@@ -0,0 +1,95 @@ +<!DOCTYPE html> +<!-- +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. +--> + +<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> +<title>gr-syntax-lib-loader</title> + +<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script> +<script src="../../../bower_components/web-component-tester/browser.js"></script> + +<link rel="import" href="gr-syntax-lib-loader.html"> + +<test-fixture id="basic"> + <template> + <gr-syntax-lib-loader></gr-syntax-lib-loader> + </template> +</test-fixture> + +<script> + suite('gr-syntax-lib-loader tests', function() { + var element; + var resolveLoad; + var loadStub; + + setup(function() { + element = fixture('basic'); + + loadStub = sinon.stub(element, '_loadHLJS', function() { + return new Promise(function(resolve) { + resolveLoad = resolve; + }); + }); + + // Assert preconditions: + assert.isFalse(element._state.loaded); + assert.isFalse(element._state.loading); + }); + + teardown(function() { + if (window.hljs) { + delete window.hljs; + } + loadStub.restore(); + + // Because the element state is a singleton, clean it up. + element._state.loading = false; + element._state.loaded = false; + element._state.callbacks = []; + }); + + test('only load once', function(done) { + var firstCallHandler = sinon.stub(); + element.get().then(firstCallHandler); + + // It should now be in the loading state. + assert.isTrue(loadStub.called); + assert.isTrue(element._state.loading); + assert.isFalse(element._state.loaded); + assert.isFalse(firstCallHandler.called); + + var secondCallHandler = sinon.stub(); + element.get().then(secondCallHandler); + + // No change in state. + assert.isTrue(element._state.loading); + assert.isFalse(element._state.loaded); + assert.isFalse(firstCallHandler.called); + assert.isFalse(secondCallHandler.called); + + // Now load the library. + resolveLoad(); + flush(function() { + // The state should be loaded and both handlers called. + assert.isFalse(element._state.loading); + assert.isTrue(element._state.loaded); + assert.isTrue(firstCallHandler.called); + assert.isTrue(secondCallHandler.called); + done(); + }); + }); + }); +</script>
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html index c20795b..25dfaff 100644 --- a/polygerrit-ui/app/elements/gr-app.html +++ b/polygerrit-ui/app/elements/gr-app.html
@@ -22,6 +22,7 @@ <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"> @@ -136,6 +137,7 @@ </gr-overlay> <gr-error-manager></gr-error-manager> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> + <gr-reporting id="reporting"></gr-reporting> </template> <script src="gr-app.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js index 0833a72..f24ecfd 100644 --- a/polygerrit-ui/app/elements/gr-app.js +++ b/polygerrit-ui/app/elements/gr-app.js
@@ -72,6 +72,7 @@ }, ready: function() { + this.$.reporting.appStarted(); this._viewState = { changeView: { changeNum: null,
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html new file mode 100644 index 0000000..0c23c4f --- /dev/null +++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -0,0 +1,53 @@ +<!DOCTYPE html> +<!-- +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. +--> + +<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/web-component-tester/browser.js"></script> + +<link rel="import" href="gr-app.html"> + +<test-fixture id="basic"> + <template> + <gr-app id="app"></gr-app> + </template> +</test-fixture> + +<script> + suite('gr-app tests', function() { + var sandbox; + var element; + + setup(function(done) { + sandbox = sinon.sandbox.create(); + stub('gr-reporting', { + appStarted: sandbox.stub(), + }); + element = fixture('basic'); + flush(done); + }); + teardown(function() { + sandbox.restore(); + }); + + test('reporting', function() { + assert.isTrue(element.$.reporting.appStarted.calledOnce); + }); + }); +</script>
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 0fc6b07..60c7f25 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -14,6 +14,8 @@ (function() { 'use strict'; + var TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g; + Polymer({ is: 'gr-autocomplete', @@ -181,6 +183,12 @@ this._cancel(); break; case 9: // Tab + if (this._suggestions.length > 0) { + e.preventDefault(); + this._commit(); + this._suggestions = []; + } + break; case 13: // Enter e.preventDefault(); this._commit(); @@ -199,8 +207,10 @@ var completed = suggestions[index].value; if (this.multi) { // Append the completed text to the end of the string. - var shortStr = this.text.substring(0, this.text.lastIndexOf(' ') + 1); - this.value = shortStr + completed; + // Allow spaces within quoted terms. + var tokens = this.text.match(TOKENIZE_REGEX); + tokens[tokens.length - 1] = completed; + this.value = tokens.join(' '); } else { this.value = completed; } @@ -226,7 +236,7 @@ if (this._suggestions.length > 0) { this._updateValue(this._suggestions, this._index); } else { - this.value = this.text; + this.value = this.text || ''; } var value = this.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 f8b16b7..ccc578c 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
@@ -241,5 +241,16 @@ done(); }); }); + + test('tab key completes only when suggestions exist', function() { + var commitStub = sinon.stub(element, '_commit'); + element._suggestions = []; + MockInteractions.pressAndReleaseKeyOn(element.$.input, 9); // tab + assert.isFalse(commitStub.called); + element._suggestions = ['tunnel snakes rule!']; + MockInteractions.pressAndReleaseKeyOn(element.$.input, 9); // tab + assert.isTrue(commitStub.called); + commitStub.restore(); + }); }); </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 c815ffd..164bb2d 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.html +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.html
@@ -22,7 +22,7 @@ <template strip-whitespace> <style> :host { - background-color: #fff; + background-color: #f5f5f5; border: 1px solid #d1d2d3; border-radius: 2px; box-sizing: border-box; @@ -30,10 +30,10 @@ cursor: pointer; display: inline-block; font-family: var(--font-family); - font-size: 13px; + font-size: 12px; font-weight: bold; outline-width: 0; - padding: .3em .65em; + padding: .4em .85em; position: relative; text-align: center; -moz-user-select: none; @@ -44,10 +44,17 @@ :host([hidden]) { display: none; } + :host([primary]), + :host([secondary]) { + color: #fff; + } :host([primary]) { background-color: #4d90fe; border-color: #3079ed; - color: #fff; + } + :host([secondary]) { + background-color: #d14836; + border-color: transparent; } :host([small]) { font-size: 12px; @@ -74,32 +81,53 @@ :host([loading][disabled]) { cursor: wait; } - :host(:focus), - :host(:hover) { - border-color: #666; + :host(:focus:not([primary]:not[secondary])), + :host(:hover:not([primary]:not[secondary])) { + background-color: #f8f8f8; + border-color: #aaa; } :host(:active) { border-color: #d1d2d3; color: #aaa; } + :host([primary]:focus), + :host([secondary]:focus), + :host([primary]:active), + :host([secondary]:active) { + color: #fff; + } :host([primary]:focus) { - border-color: #fff; box-shadow: 0 0 1px #00f; } :host([primary]:hover) { + background-color: #4d90fe; border-color: #00F; } + :host([primary]:active), + :host([secondary]:active) { + box-shadow: none; + } :host([primary]:active) { border-color: #0c2188; - box-shadow: none; - color: #fff; } - :host([primary][loading]), - :host([primary][disabled]) { + :host([secondary]:focus) { + box-shadow: 0 0 1px #f00; + } + :host([secondary]:hover) { + background-color: #c53727; + border: 1px solid #b0281a; + } + :host([secondary]:active) { + border-color: #941c0c; + } + :host([primary][loading]) { background-color: #7caeff; border-color: transparent; color: #fff; } + :host([primary][disabled]) { + background-color: #888; + } </style> <content></content> </template>
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 0d3ea3d..7b3bc23 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
@@ -63,15 +63,6 @@ type: String, value: ScrollBehavior.NEVER, }, - - /** - * When using the 'keep-visible' scroll behavior, set an offset to the top - * of the window for what is considered above the upper fold. - */ - foldOffsetTop: { - type: Number, - value: 0, - }, }, detached: function() { @@ -214,7 +205,7 @@ } if (this.scroll === ScrollBehavior.KEEP_VISIBLE && - top > window.pageYOffset + this.foldOffsetTop && + top > window.pageYOffset && top < window.pageYOffset + window.innerHeight) { return; } // Scroll the element to the middle of the window. Dividing by a third
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 2f109c9..632ba06 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
@@ -426,7 +426,7 @@ var bLastDotIndex = b.lastIndexOf('.'); var bExt = b.substr(bLastDotIndex + 1); - var bFile = a.substr(0, bLastDotIndex); + var bFile = b.substr(0, bLastDotIndex); // Sort header files above others with the same base name. var headerExts = ['h', 'hxx', 'hpp']; @@ -442,8 +442,7 @@ return 1; } } - - return a.localeCompare(b); + return aFile.localeCompare(bFile) || a.localeCompare(b); }, getChangeRevisionActions: function(changeNum, patchNum) { @@ -467,8 +466,26 @@ }); }, - getSuggestedProjects: function(inputVal, opt_errFn, opt_ctx) { - return this.fetchJSON('/projects/', opt_errFn, opt_ctx, {p: inputVal}); + getSuggestedGroups: function(inputVal, opt_n, opt_errFn, opt_ctx) { + return this.fetchJSON('/groups/', opt_errFn, opt_ctx, { + s: inputVal, + n: opt_n, + }); + }, + + getSuggestedProjects: function(inputVal, opt_n, opt_errFn, opt_ctx) { + return this.fetchJSON('/projects/', opt_errFn, opt_ctx, { + p: inputVal, + n: opt_n, + }); + }, + + getSuggestedAccounts: function(inputVal, opt_n, opt_errFn, opt_ctx) { + return this.fetchJSON('/accounts/', opt_errFn, opt_ctx, { + q: inputVal, + n: opt_n, + suggest: null, + }); }, addChangeReviewer: function(changeNum, reviewerID) {
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 8dda2ce..8f994f0 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
@@ -237,6 +237,32 @@ ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort( element._specialFilePathCompare), ['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), + [ + 'minidump/minidump_memory_writer.h', + 'minidump/minidump_memory_writer.cc', + 'minidump/minidump_thread_writer.h', + 'minidump/minidump_thread_writer.cc', + ]); + + // Regression test for Issue 4545. + assert.deepEqual([ + 'task_test.go', + 'task.go', + ] + .sort(element._specialFilePathCompare), + [ + 'task.go', + 'task_test.go', + ]); }); test('rebase always enabled', function(done) {
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html index ecf4ac6..faf45d8 100644 --- a/polygerrit-ui/app/styles/app-theme.html +++ b/polygerrit-ui/app/styles/app-theme.html
@@ -20,7 +20,7 @@ --selection-background-color: #ebf5fb; --default-text-color: #000; --view-background-color: #fff; - --default-horizontal-margin: 1.25rem; + --default-horizontal-margin: 1rem; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; --monospace-font-family: 'Source Code Pro', Menlo, 'Lucida Console', Monaco, monospace;
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index d3cb316..156c873 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html
@@ -25,15 +25,15 @@ var basePath = '../elements/'; [ - 'change-list/gr-change-list-item/gr-change-list-item_test.html', - 'change-list/gr-change-list/gr-change-list_test.html', 'change/gr-account-entry/gr-account-entry_test.html', '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-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-rebase-dialog/gr-confirm-rebase-dialog_test.html', + 'change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html', 'change/gr-download-dialog/gr-download-dialog_test.html', 'change/gr-file-list/gr-file-list_test.html', 'change/gr-message/gr-message_test.html', @@ -41,26 +41,31 @@ 'change/gr-related-changes-list/gr-related-changes-list_test.html', 'change/gr-reply-dialog/gr-reply-dialog_test.html', 'change/gr-reviewer-list/gr-reviewer-list_test.html', + 'change-list/gr-change-list/gr-change-list_test.html', + 'change-list/gr-change-list-item/gr-change-list-item_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-reporting/gr-reporting_test.html', 'core/gr-search-bar/gr-search-bar_test.html', + 'diff/gr-diff/gr-diff-group_test.html', + 'diff/gr-diff/gr-diff_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/gr-diff-comment_test.html', + 'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html', 'diff/gr-diff-cursor/gr-diff-cursor_test.html', - 'diff/gr-diff-highlight/gr-diff-highlight_test.html', 'diff/gr-diff-highlight/gr-annotation_test.html', + 'diff/gr-diff-highlight/gr-diff-highlight_test.html', 'diff/gr-diff-preferences/gr-diff-preferences_test.html', 'diff/gr-diff-processor/gr-diff-processor_test.html', 'diff/gr-diff-selection/gr-diff-selection_test.html', 'diff/gr-diff-view/gr-diff-view_test.html', - 'diff/gr-diff/gr-diff-group_test.html', - 'diff/gr-diff/gr-diff_test.html', 'diff/gr-patch-range-select/gr-patch-range-select_test.html', 'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html', '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', 'settings/gr-account-info/gr-account-info_test.html', 'settings/gr-email-editor/gr-email-editor_test.html', 'settings/gr-group-list/gr-group-list_test.html', @@ -69,10 +74,10 @@ 'settings/gr-settings-view/gr-settings-view_test.html', 'settings/gr-ssh-editor/gr-ssh-editor_test.html', 'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html', - 'shared/gr-autocomplete/gr-autocomplete_test.html', 'shared/gr-account-label/gr-account-label_test.html', '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-avatar/gr-avatar_test.html', 'shared/gr-change-star/gr-change-star_test.html', 'shared/gr-confirm-dialog/gr-confirm-dialog_test.html',
diff --git a/tools/java_doc.defs b/tools/java_doc.defs index 41a8730..2bf27cf 100644 --- a/tools/java_doc.defs +++ b/tools/java_doc.defs
@@ -2,22 +2,20 @@ name, title, pkgs, - paths, + source_jar, srcs = [], deps = [], visibility = [], - do_it_wrong = False, external_docs = [], ): - if do_it_wrong: - sourcepath = paths - else: - sourcepath = ['$SRCDIR/' + n for n in paths] + # TODO(davido): Actually we shouldn't need to extract the source + # archive, javadoc should just work with provided archive. external_docs.insert(0, 'http://docs.oracle.com/javase/7/docs/api') genrule( name = name, cmd = ' '.join([ - 'while ! test -f .buckconfig; do cd ..; done;', + 'mkdir $TMP/sourcepath &&', + 'unzip $(location %s) -d $TMP/sourcepath &&' % source_jar, 'javadoc', '-quiet', '-protected', @@ -28,8 +26,7 @@ ' '.join(['-link %s' % url for url in external_docs]), '-subpackages ', ':'.join(pkgs), - '-sourcepath ', - ':'.join(sourcepath), + '-sourcepath $TMP/sourcepath', ' -classpath ', ':'.join(['$(classpath %s)' % n for n in deps]), '-d $TMP', @@ -37,4 +34,4 @@ srcs = srcs, out = name + '.jar', visibility = visibility, -) + )
diff --git a/tools/jgit-snapshot-deploy-pom.diff b/tools/jgit-snapshot-deploy-pom.diff new file mode 100644 index 0000000..01f50e4 --- /dev/null +++ b/tools/jgit-snapshot-deploy-pom.diff
@@ -0,0 +1,43 @@ +diff --git a/pom.xml b/pom.xml +index d256bbb..7e523fd 100644 +--- a/pom.xml ++++ b/pom.xml +@@ -226,6 +226,10 @@ + + <pluginRepositories> + <pluginRepository> ++ <id>gerrit-maven</id> ++ <url>https://gerrit-maven.commondatastorage.googleapis.com</url> ++ </pluginRepository> ++ <pluginRepository> + <id>repo.eclipse.org.cbi-releases</id> + <url>https://repo.eclipse.org/content/repositories/cbi-releases/</url> + </pluginRepository> +@@ -236,6 +240,13 @@ + </pluginRepositories> + + <build> ++ <extensions> ++ <extension> ++ <groupId>com.googlesource.gerrit</groupId> ++ <artifactId>gs-maven-wagon</artifactId> ++ <version>3.3</version> ++ </extension> ++ </extensions> + <pluginManagement> + <plugins> + <plugin> +@@ -649,9 +660,10 @@ + + <distributionManagement> + <repository> +- <id>repo.eclipse.org</id> +- <name>JGit Maven Repository - Releases</name> +- <url>https://repo.eclipse.org/content/repositories/jgit-releases/</url> ++ <id>gerrit-maven-repository</id> ++ <name>Gerrit Maven Repository</name> ++ <url>gs://gerrit-maven</url> ++ <uniqueVersion>true</uniqueVersion> + </repository> + <snapshotRepository> + <id>repo.eclipse.org</id>